# 节点状态持久化与人工介入规范

> **代码实现**：`sync_system_new/common/state_reset.py`

---

## 1. 持久化规则

### 1.1 持久化目标

| 目标 | 说明 |
|------|------|
| **可恢复性** | 系统重启后能从上次状态继续 |
| **可观测性** | 失败和异常状态要保留，供人工查看 |
| **可追溯性** | 失败信息持久化，不自动重试，需人工确认 |
| **绑定稳定性** | 手工绑定一旦建立，永不自动改变 |

### 1.2 分阶段重置设计

状态重置分为两个阶段执行：

#### 阶段1：加载时重置（Collection.load_from_persistence）

| 字段 | 性质 | 持久化 | 阶段1重置 |
|------|------|--------|-----------|
| `node_id` | 恒定标识 | ✅ 必须 | ❌ 不重置 |
| `data_id` | 业务主键 | ✅ 必须 | ❌ 不重置 |
| `data` | 业务数据 | ✅ 必须 | ✅ **重置**（每次重新取数） |
| `origin_data` | 原始快照 | ✅ 必须 | ✅ **重置**（每次重新取数） |
| `depend_ids` | 依赖列表 | ❌ **不持久化** | ✅ **初始化为[]**（bind 阶段从 data 实时计算） |
| `binding_status` | 绑定状态 | ✅ 必须 | ✅ **重置为 UNCHECKED** |
| `action` | 同步意图 | ✅ 保存 | ❌ **保留**（用于阶段2判断） |
| `status` | 执行状态 | ✅ 保存 | ❌ **保留**（全部保留，不重置） |
| `error` | 错误信息 | ✅ 保存 | ✅ **重置为None**（清空上次错误） |
| `context` | 扩展上下文 | ✅ 保存 | ❌ 不重置 |

#### 阶段2：数据加载后清理（Pipeline._cleanup_create_failed_zombies）

在数据加载完成后、策略执行前，执行CREATE失败节点清理：

1. **检查条件**：
   - 本地节点 `action == CREATE`（上次尝试创建）
   - 本地节点 `status == FAILED`（创建失败）

2. **清理动作**：
   - **删除本地失败节点**（从 collection 中移除）
   - 如果有绑定记录：
     - 解除绑定关系
     - 删除远程 collection 中的僵尸节点

3. **最后重置 action**：
   - 所有剩余节点的 `action` 重置为 NONE
   - 准备进入策略阶段

4. **下次同步的自动恢复**：
   - 被删除的节点会从数据源重新加载
   - 如果远程实际已创建成功（网络波动导致未获取到结果）→ 绑定阶段会自动绑定上
   - 如果远程实际也失败了 → 重新尝试创建

5. **UPDATE 阶段保护**：
   - UPDATE 操作会跳过所有 `status == FAILED` 的节点
   - 防止 UPDATE 阶段覆盖 CREATE/UPDATE 失败的状态和错误信息
   - 确保失败节点的 action 和 error 信息保持不变，直到下次同步清理

> **说明**：
> - `data` / `origin_data`：每次运行从数据源重新获取，保证数据最新
> - `depend_ids`：**不持久化**，每次 bind 阶段从 data 的业务字段（如 contract_id, project_id）实时提取
> - `binding_status`：持久化用于审计，加载时重置为 `UNCHECKED`，由 bind() 重新判定
> - `error`：**阶段1清空**，避免上次错误影响本次运行，新错误会在各阶段重新生成
> - `action`：**阶段1保留**用于识别 CREATE 失败的节点，**阶段2清理后重置**
> - `status`：**阶段1全部保留**（包括 FAILED、PRECONDITION_FAILED 等），不自动重置
> - **CREATE 失败节点会被自动删除并重新加载**，UPDATE/DELETE 失败保留需人工处理
> - **UPDATE 操作会跳过 FAILED 节点**，避免覆盖失败状态和错误信息

### 1.3 绑定记录 vs binding_status

| 概念 | 存储位置 | 持久性 | 如何改变 |
|------|---------|--------|---------|
| **绑定记录** | `bindings` 表 | **永久保留** | 只能手工 bind/unbind，或 CREATE 失败自动清理 |
| **binding_status** | `nodes` 表 | 保存但可重判 | 每次 bind() 重新判定 |

### 1.4 CREATE 失败的自动恢复

当 CREATE 操作失败时，会产生"僵尸绑定"：
- 绑定记录已建立（local_id → remote_node_id）
- 但远程节点的 `data_id` 为空（API 未返回真实 ID）

**下次同步时的自动恢复流程**：
1. 阶段1加载：保留 `action=CREATE`
2. 阶段2清理：检测到僵尸绑定，自动清理
3. 本地节点恢复为 MISSING 状态
4. 策略阶段重新判定，再次尝试 CREATE

---

## 2. 人工介入规范

### 2.1 介入时机

| 时机 | 场景 | 操作 |
|------|------|------|
| **运行前** | 查看上次失败记录 | 查看持久化的 error，决定是否修复后重试 |
| **bind() 后** | 出现 ABNORMAL/WARNING | 检查数据一致性，手工修复或确认 |
| **bind() 后** | 1:N / N:N 映射 | 手工绑定指定远程记录 |
| **sync() 后** | 批量失败 | 分析失败原因，决定重试策略 |

### 2.2 binding_status 与介入需求

| binding_status | 含义 | 需要介入 | 说明 |
|----------------|------|---------|------|
| `UNCHECKED` | 未检查 | ❌ | 等待 bind() 判定 |
| `NORMAL` | 绑定正常 | ❌ | 自动处理 |
| `MISSING` | 无绑定记录 | ❌ | 自动创建 |
| `WARNING` | 本地有绑定但远程无数据 | ⚠️ 可选 | 可能是远程删除，需确认 |
| `ABNORMAL` | 状态不一致 | ✅ **必须** | 需人工排查 |
| `DEPENDENCY_ERROR` | 依赖未就绪 | ❌ | 等依赖解决后自动重试 |

### 2.3 人工绑定操作

```python
# 手工绑定（1:N 场景）
binding_manager.bind(node_id="local_xxx", data_id="remote_xxx")

# 手工解绑
binding_manager.unbind(node_id="local_xxx")
```

绑定记录一旦建立，永不自动改变，只能手工操作。

### 2.4 职责分工

| 职责 | 系统自动 | 人工介入 |
|------|---------|---------|
| 1:1 自动绑定 | ✅ | - |
| 1:N / N:N 绑定 | ❌ | ✅ |
| NORMAL 节点同步 | ✅ | - |
| MISSING 节点创建 | ✅ | - |
| CREATE 失败重试 | ✅（自动清理僵尸） | ⚠️（需确认后手动重置） |
| UPDATE 失败重试 | ❌（保留FAILED状态） | ✅（需确认后手动重置） |
| ABNORMAL 处理 | ❌ | ✅ |
| WARNING 确认 | ❌ | ✅（可选） |

---

## 3. 总结

1. **分阶段重置**：
   - 阶段1（加载时）：只重置 binding_status，**保留 action/status/error**
   - 阶段2（数据加载后）：清理 CREATE 失败僵尸，然后重置 action
2. **失败状态持久化**：FAILED 状态和 error 信息保留，不自动重试
3. **绑定记录**：永久保留，除非 CREATE 失败自动清理或手工操作
4. **人工介入**：失败需确认后手动重置，ABNORMAL/WARNING 需人工处理，1:N 绑定需手工指定
