first commit
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# Root docs migration note
|
||||
|
||||
本目录为从项目根目录 `docs/` 迁移过来的文档快照(全量复制,不跳过)。
|
||||
|
||||
目的:
|
||||
- 保留原始设计背景与历史规范,便于在 `sync_state_machine` 目录内就地查阅;
|
||||
- 与当前 `sync_state_machine/docs/` 的“现行实现文档”并存。
|
||||
|
||||
使用建议:
|
||||
- 以 `sync_state_machine/docs/` 下现行文档(如 `state_machine.md`、`sync_flow.md`、`architecture.md`)作为运行时真义;
|
||||
- 本目录文档作为历史参考来源,遇到冲突时以现行实现与状态机配置为准:
|
||||
- `sync_state_machine/config/node_state_machine.yaml`
|
||||
- `sync_state_machine/sync_system/strategy.py`
|
||||
- `sync_state_machine/engine/semantics.py`
|
||||
@@ -0,0 +1,706 @@
|
||||
# API DataSource 架构设计
|
||||
|
||||
## 架构总览
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Pipeline │
|
||||
│ (同步流程编排) │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ApiDataSource │
|
||||
│ (数据源协调层) │
|
||||
│ │
|
||||
│ - 持有 ApiClient 实例 │
|
||||
│ - 注册和管理 Handler │
|
||||
│ - 实现 load_all / execute_batch │
|
||||
│ - 注入 api_client 到 Handler │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────┼───────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
||||
│ Handler │ │ Handler │ │ Handler │
|
||||
│ Project │ │ Contract │ │ Material │
|
||||
│ │ │ │ │ │
|
||||
│ 业务逻辑层 │ │ 业务逻辑层 │ │ 业务逻辑层 │
|
||||
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
|
||||
│ │ │
|
||||
└───────────────────┼───────────────────┘
|
||||
│ 使用 self.api_client
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ ApiClient │
|
||||
│ (HTTP 通信层) │
|
||||
│ │
|
||||
│ - 签名生成 │
|
||||
│ - HTTP 请求 │
|
||||
│ - Push Log │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
**调用流程**:
|
||||
1. Pipeline 创建 ApiDataSource,传入 base_url, uid, secret
|
||||
2. ApiDataSource 内部创建 ApiClient 实例
|
||||
3. ApiDataSource 注册各业务 Handler,并注入 api_client
|
||||
4. Handler 使用 `self.api_client.get/post/put()` 发送请求
|
||||
5. ApiClient 自动处理签名、push_log 查询、错误处理
|
||||
|
||||
**优势**:
|
||||
- ✅ **职责清晰**:ApiClient(通信)、DataSource(协调)、Handler(业务)
|
||||
- ✅ **避免循环依赖**:Handler → ApiClient(单向依赖)
|
||||
- ✅ **易于测试**:ApiClient 可 mock,Handler 独立测试
|
||||
- ✅ **易于扩展**:新业务只需实现 Handler
|
||||
|
||||
## 1. 问题分析:旧方案的复杂性
|
||||
|
||||
旧方案 `sync_system/datasource/api` 存在的问题:
|
||||
|
||||
1. **过度抽象**:引入了 `ApiDataSource` → `Repository` → `ResponseHandle` → `Cache` 多层封装,每个业务需要独立 Repository 类,Protocol 定义繁琐
|
||||
2. **状态管理混乱**:缓存策略不清晰,ResponseHandle 封装异步结果但使用困难,缺少统一的 push_log 查询机制
|
||||
3. **依赖关系困难**:Repository 之间互相传递引用,难以从 collection 获取已有数据
|
||||
4. **API 路由分散**:每个 Repository 手动实现多个 update 方法,缺少自动路由机制
|
||||
|
||||
## 2. 新方案设计原则
|
||||
|
||||
- **简单直接**:Handler 直接调用 HTTP API,不过度封装
|
||||
- **状态透明**:DataSource 统一管理 session、签名、push_log 查询
|
||||
- **依赖清晰**:通过 collection 获取依赖数据,使用 context 存储额外信息
|
||||
- **智能路由**:根据数据字段自动选择 API endpoint
|
||||
|
||||
## 3. 核心组件职责
|
||||
|
||||
### 3.1 ApiClient - HTTP 通信层
|
||||
|
||||
**职责**:
|
||||
- 封装所有 HTTP 通信逻辑
|
||||
- 管理 httpx.AsyncClient,维护连接池和 session
|
||||
- 生成请求签名(timestamp, nonce, sign)
|
||||
- 统一请求入口,自动注入 uid
|
||||
- 批量 Push Log 查询
|
||||
|
||||
**核心方法**:
|
||||
```python
|
||||
class ApiClient:
|
||||
def __init__(self, base_url: str, uid: str, secret: str):
|
||||
self._client = httpx.AsyncClient()
|
||||
self._base_url = base_url
|
||||
self._uid = uid
|
||||
self._secret = secret
|
||||
|
||||
async def request(self, method: str, url: str, **kwargs) -> httpx.Response
|
||||
"""统一请求入口,自动签名"""
|
||||
|
||||
async def get(self, url: str, params: dict = None) -> httpx.Response
|
||||
"""GET 请求"""
|
||||
|
||||
async def post(self, url: str, data: dict) -> httpx.Response
|
||||
"""POST 请求,data 中已包含 push_id/biz_id(由 Handler 生成)"""
|
||||
|
||||
async def put(self, url: str, data: dict) -> httpx.Response
|
||||
"""PUT 请求,data 中已包含 push_id/biz_id"""
|
||||
|
||||
async def delete(self, url: str, data: dict) -> httpx.Response
|
||||
"""DELETE 请求,data 中已包含 push_id/biz_id"""
|
||||
|
||||
async def get_push_logs(self, push_ids: List[str]) -> Dict[str, dict]
|
||||
"""批量查询 push_log 状态,返回 {push_id: push_log_data}
|
||||
注意:单个查询也传单元素列表
|
||||
当前实现:用 for 循环模拟,后续等后端实现批量接口
|
||||
"""
|
||||
|
||||
def _generate_signature(self, params: dict) -> dict
|
||||
"""生成签名参数(timestamp, nonce, sign)"""
|
||||
```
|
||||
|
||||
### 3.2 ApiDataSource - 数据源协调层
|
||||
|
||||
**职责**:
|
||||
- 持有 ApiClient 实例,提供给 Handler 使用
|
||||
- 实现 DataSource 接口(load_all, sync_all)
|
||||
- 协调多个 Handler 的执行顺序(通过 sync_all)
|
||||
- Handler 内部负责:生成 push_id/biz_id、调用 poll 轮询、提取 created_id
|
||||
- 统一异常处理和日志记录
|
||||
|
||||
**核心属性和方法**:
|
||||
```python
|
||||
class ApiDataSource(DataSource):
|
||||
def __init__(self, base_url: str, uid: str, secret: str):
|
||||
self.client = ApiClient(base_url, uid, secret)
|
||||
self._handlers: Dict[str, BaseApiHandler] = {}
|
||||
|
||||
def register_handler(self, node_type: str, handler: BaseApiHandler):
|
||||
"""注册业务 Handler"""
|
||||
handler.api_client = self.client # 注入 client
|
||||
self._handlers[node_type] = handler
|
||||
|
||||
async def load_all(self, collection: DataCollection, order: List[str]) -> None:
|
||||
"""调用基类的 load_all,按顺序加载数据"""
|
||||
|
||||
async def sync_all(self, collection: DataCollection, order: List[str]) -> None:
|
||||
"""调用基类的 sync_all,按顺序执行同步"""
|
||||
```
|
||||
|
||||
### 3.3 BaseApiHandler - 业务逻辑基类
|
||||
|
||||
**职责**:
|
||||
- 实现 Handler 接口(load, create, update, delete, poll)
|
||||
- 使用 self.api_client 发送 HTTP 请求
|
||||
- 生成 push_id 和 biz_id(业务标识)
|
||||
- 从 collection 获取依赖节点数据
|
||||
- 管理节点的 context(存储 project_id, contract_id 等)
|
||||
- 实现 poll 方法,调用 api_client.get_push_logs 批量查询
|
||||
- 提取 created_id(因为不同业务的 id 字段名和位置可能不同)
|
||||
- 返回 TaskResult(SUCCESS/FAILED/IN_PROGRESS)
|
||||
|
||||
**核心方法**:
|
||||
```python
|
||||
class BaseApiHandler(NodeHandler):
|
||||
def __init__(self):
|
||||
self.api_client: ApiClient = None # 由 DataSource 注入
|
||||
self.collection: DataCollection = None # 由 DataSource 注入
|
||||
|
||||
async def load(self, collection: DataCollection) -> List[dict]
|
||||
async def create(self, node: SyncNode) -> TaskResult
|
||||
async def update(self, node: SyncNode) -> TaskResult
|
||||
async def delete(self, node: SyncNode) -> TaskResult
|
||||
async def poll(self, node: SyncNode, task_id: str) -> TaskResult
|
||||
|
||||
def extract_created_id(self, push_log: dict) -> str
|
||||
"""从 push_log 提取 created_id,子类可重写"""
|
||||
|
||||
# 重试逻辑(可选,子类可重写)
|
||||
async def _execute_with_retry(self, operation, max_retries: int = 3) -> Any
|
||||
"""通用重试逻辑,子类可自定义重试策略"""
|
||||
```
|
||||
|
||||
**重试策略说明**:
|
||||
- **重试在 Handler 内部**:create/update/delete 内部可调用 _execute_with_retry
|
||||
- **返回给 DataSource**:最终只返回 SUCCESS/FAILED/IN_PROGRESS
|
||||
- **灵活控制**:不同业务可重写 _execute_with_retry 实现自定义策略
|
||||
- **默认策略**:网络错误/5xx 重试 3 次,4xx 不重试
|
||||
|
||||
**未实现 API 的处理**:
|
||||
- 部分业务可能没有实现 create 或 delete API
|
||||
- 此时 Handler 应在对应方法中直接返回 FAILED 状态,并在 error 中说明原因
|
||||
- 示例:`return TaskResult(status=TaskStatus.FAILED, error="Create API not implemented for this business")`
|
||||
|
||||
### 3.4 具体业务 Handler
|
||||
|
||||
**职责**:
|
||||
- 实现具体业务的 load 逻辑(调用 self.api_client.get())
|
||||
- 创建节点时设置 context
|
||||
- 重写 API endpoint 选择逻辑(可选)
|
||||
- 定制 created_id 提取逻辑(可选)
|
||||
|
||||
每个业务 Handler 位于 `sync_system_new/domain/{业务}/api_handler.py`
|
||||
|
||||
## 4. HTTP 请求状态管理
|
||||
|
||||
### 4.0 状态系统概述
|
||||
|
||||
**通用状态枚举**(适用于所有 DataSource):
|
||||
|
||||
```python
|
||||
class TaskStatus(Enum):
|
||||
"""Handler 返回的任务执行状态"""
|
||||
SUCCESS = "success" # 同步完成,立即成功
|
||||
FAILED = "failed" # 同步失败
|
||||
IN_PROGRESS = "in_progress" # 异步任务进行中
|
||||
|
||||
class SyncStatus(Enum):
|
||||
"""SyncNode 的同步执行状态"""
|
||||
PENDING = "PENDING"
|
||||
IN_PROGRESS = "IN_PROGRESS"
|
||||
SUCCESS = "SUCCESS"
|
||||
FAILED = "FAILED"
|
||||
SKIPPED = "SKIPPED"
|
||||
PRECONDITION_FAILED = "PRECONDITION_FAILED"
|
||||
```
|
||||
|
||||
**状态映射规则**:
|
||||
- TaskStatus 是 Handler 执行操作后的返回值
|
||||
- SyncStatus 是 SyncNode 的最终状态,由 DataSource 根据 TaskStatus 更新
|
||||
- 普通 DataSource(FileDataSource)和 API DataSource 使用相同的状态枚举
|
||||
- 区别在于:**API DataSource 的网络操作如何转化为 TaskStatus**
|
||||
|
||||
**重要说明:Load 阶段 vs 执行阶段的状态**:
|
||||
- **Load 阶段**(Handler.load()):从 API 拉取数据创建 SyncNode
|
||||
- 此时节点状态为 `PENDING`(等待后续 bind/create/update)
|
||||
- GET 请求失败不影响节点创建,只是该节点不存在于 collection
|
||||
- **执行阶段**(Handler.create/update/delete):执行同步操作
|
||||
- DataSource 在调用前设置 `node.status = IN_PROGRESS`
|
||||
- Handler 返回 TaskResult,DataSource 据此更新 node.status
|
||||
- POST/PUT/DELETE 成功后才设置为 SUCCESS
|
||||
|
||||
**核心映射逻辑**(在 DataSource 中):
|
||||
```python
|
||||
# 执行前:DataSource 设置状态
|
||||
node.status = SyncStatus.IN_PROGRESS
|
||||
|
||||
# 调用 Handler(Handler 内部可能重试多次)
|
||||
result = await handler.create(node)
|
||||
|
||||
# 执行后:DataSource 根据最终结果更新状态
|
||||
if result.status == TaskStatus.SUCCESS:
|
||||
node.status = SyncStatus.SUCCESS
|
||||
node.data_id = result.data_id
|
||||
elif result.status == TaskStatus.FAILED:
|
||||
node.status = SyncStatus.FAILED
|
||||
node.error = result.error
|
||||
elif result.status == TaskStatus.IN_PROGRESS:
|
||||
# 记录 task_id 等待轮询
|
||||
async_tasks[node.node_id] = result.task_id
|
||||
```
|
||||
|
||||
**重要**:Handler 负责重试,最终只返回确定的结果(SUCCESS/FAILED/IN_PROGRESS)
|
||||
|
||||
### 4.1 请求类型与状态流转
|
||||
|
||||
**API DataSource 的特殊性**:
|
||||
- **Load 阶段**:GET 请求拉取数据,创建 PENDING 节点(失败则不创建节点)
|
||||
- **执行阶段**:POST/PUT/DELETE 先设置 IN_PROGRESS,通过 poll() 轮询变为 SUCCESS/FAILED
|
||||
|
||||
#### Load 阶段 - 数据拉取
|
||||
|
||||
**目的**:从 API 获取数据,创建 SyncNode 放入 collection
|
||||
|
||||
| 阶段 | HTTP 操作 | 成功行为 | 失败行为 | 节点状态 |
|
||||
|------|----------|---------|---------|---------|
|
||||
| 1. 调用 load() | GET /api/list | 返回数据列表 | 返回空列表或抛异常 | - |
|
||||
| 2. 创建节点 | - | 为每条数据创建 SyncNode | 不创建节点 | PENDING |
|
||||
|
||||
**代码示例**:
|
||||
```python
|
||||
async def load(self, collection: DataCollection) -> List[dict]:
|
||||
"""加载数据 - GET 阶段不涉及状态转换"""
|
||||
try:
|
||||
response = await self.api_client.get("project/list")
|
||||
if response.status_code == 200:
|
||||
data_list = response.json().get("list", [])
|
||||
# 返回数据,由 DataSource 创建 PENDING 节点
|
||||
return data_list
|
||||
else:
|
||||
# 失败:返回空列表,不创建节点
|
||||
logger.error(f"Load failed: {response.status_code}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Load exception: {e}")
|
||||
return [] # 或者抛出异常,由调用方处理
|
||||
```
|
||||
|
||||
#### Mutation 请求(POST/PUT/DELETE) - 执行阶段
|
||||
|
||||
**状态变化时机**:
|
||||
1. **执行前**:DataSource 设置 `node.status = IN_PROGRESS`
|
||||
2. **Handler 执行**:内部可能重试,最终返回 SUCCESS/FAILED/IN_PROGRESS
|
||||
3. **轮询完成**:poll() 返回 SUCCESS/FAILED
|
||||
|
||||
| 阶段 | Push Log | TaskStatus | SyncStatus | 说明 |
|
||||
|------|---------|-----------|-----------|------|
|
||||
| 执行前 | - | - | PENDING → IN_PROGRESS | DataSource 设置 |
|
||||
| 提交 | pending | IN_PROGRESS | IN_PROGRESS | 任务已提交 |
|
||||
| 轮询 | processing | IN_PROGRESS | IN_PROGRESS | 后端处理中 |
|
||||
| 成功 | success | SUCCESS | SUCCESS | 同步完成 |
|
||||
| 失败 | failed | FAILED | FAILED | 同步失败 |
|
||||
|
||||
**示例**:
|
||||
```python
|
||||
async def create(self, node: SyncNode) -> TaskResult:
|
||||
"""Handler 内部可重试,最终返回确定结果"""
|
||||
response = await self.api_client.post(url, node.data)
|
||||
if response.status_code == 202:
|
||||
return TaskResult.in_progress(task_id=response.json()["push_id"])
|
||||
else:
|
||||
return TaskResult.failed(error=f"HTTP {response.status_code}")
|
||||
|
||||
async def poll(self, node: SyncNode, task_id: str) -> TaskResult:
|
||||
push_logs = await self.api_client.get_push_logs([task_id])
|
||||
status = push_logs.get(task_id, {}).get("status")
|
||||
if status == "success":
|
||||
return TaskResult.success(data_id=self.extract_created_id(push_logs[task_id]))
|
||||
elif status == "failed":
|
||||
return TaskResult.failed(error="Backend processing failed")
|
||||
else:
|
||||
return TaskResult.in_progress(task_id=task_id)
|
||||
```
|
||||
|
||||
### 4.2 状态转换完整流程
|
||||
|
||||
```
|
||||
Load 阶段:
|
||||
Handler.load() → 返回数据列表 → DataSource 创建 PENDING 节点
|
||||
|
||||
执行阶段(同步操作):
|
||||
DataSource: node.status = IN_PROGRESS
|
||||
→ Handler.create() → TaskResult.success/failed()
|
||||
→ DataSource: node.status = SUCCESS/FAILED
|
||||
|
||||
执行阶段(异步操作):
|
||||
DataSource: node.status = IN_PROGRESS
|
||||
→ Handler.create() → TaskResult.in_progress(push_id)
|
||||
→ DataSource 轮询: Handler.poll(push_id)
|
||||
→ TaskResult.success/failed()
|
||||
→ DataSource: node.status = SUCCESS/FAILED
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- Handler 负责重试,DataSource 只处理最终结果
|
||||
- 不同业务可在 BaseApiHandler._execute_with_retry 中自定义重试策略
|
||||
- FileDataSource 和 ApiDataSource 使用相同的状态枚举和转换逻辑
|
||||
|
||||
### 4.3 Push Log 状态机
|
||||
|
||||
```
|
||||
pending → processing → success/failed
|
||||
```
|
||||
|
||||
- 轮询间隔:0.5秒
|
||||
- 超时:30秒
|
||||
- 成功:提取 created_id
|
||||
- 失败:获取 error_message
|
||||
|
||||
### 4.4 状态总结表
|
||||
|
||||
**Load 阶段(数据拉取)**:
|
||||
|
||||
| HTTP 操作 | 响应码 | 行为 | 创建节点 | 节点状态 | 说明 |
|
||||
|---------|-------|-----|---------|---------|------|
|
||||
| GET /list | 200 | 返回数据列表 | ✅ 是 | PENDING | 等待后续 bind/create/update |
|
||||
| GET /list | 4xx/5xx | 返回空列表 | ❌ 否 | - | 不创建节点 |
|
||||
|
||||
**执行阶段(POST/PUT/DELETE)**:
|
||||
|
||||
| HTTP 操作 | 响应码 | Push Log | TaskStatus | SyncStatus | 说明 |
|
||||
|---------|-------|---------|-----------|-----------|------|
|
||||
| POST/PUT | 202 | pending | IN_PROGRESS | IN_PROGRESS | 异步开始 |
|
||||
| - | - | processing | IN_PROGRESS | IN_PROGRESS | 异步处理中 |
|
||||
| - | - | success | SUCCESS | SUCCESS | 异步成功 |
|
||||
| - | - | failed | FAILED | FAILED | 异步失败 |
|
||||
| - | - | timeout | FAILED | FAILED | 轮询超时 |
|
||||
|
||||
## 5. Context 机制
|
||||
|
||||
### 5.1 Context 的作用
|
||||
|
||||
Context 是 SyncNode 的字典字段,用于存储业务数据之外的上下文信息,例如:
|
||||
- `project_id`: 当前对象所属的项目ID
|
||||
- `contract_id`: 合同变更所属的合同ID
|
||||
- `parent_id`: 父对象ID
|
||||
- 其他 API 调用需要但 schema 中没有的信息
|
||||
|
||||
### 5.2 Context 使用流程
|
||||
|
||||
**场景:加载合同数据**
|
||||
|
||||
1. **Handler.load()** 从 self._collection 获取所有 project
|
||||
2. 遍历每个 project,调用 `GET contract/list?project_id=xxx`
|
||||
3. 为每个返回的 contract 数据添加临时标记 `_context = {"project_id": xxx}`
|
||||
4. **Handler.create_node()** 创建 SyncNode 时,提取 `_context` 并设置到 `node.context`
|
||||
5. **Handler.update()** 更新时,从 `node.context["project_id"]` 获取 project_id 并添加到请求数据
|
||||
|
||||
### 5.3 Context 传递规则
|
||||
|
||||
- 父 → 子:从依赖的父节点继承 context
|
||||
- 横向传递:同级节点不共享 context
|
||||
- 更新时使用:从 context 提取必要信息添加到请求 payload
|
||||
|
||||
## 6. 智能 API 路由
|
||||
|
||||
### 6.1 路由策略
|
||||
|
||||
根据节点数据的字段,自动选择正确的 API endpoint:
|
||||
|
||||
**方案1:字段匹配**
|
||||
- 检查 node.data 中的字段集合
|
||||
- 匹配预定义的 endpoint → required_fields 映射表
|
||||
- 选择第一个匹配的 endpoint
|
||||
|
||||
**方案2:Schema 类型**
|
||||
- 根据 node.schema 的类型
|
||||
- 查找 schema_type → endpoint 映射表
|
||||
|
||||
**默认规则**:
|
||||
- CREATE: `{node_type}`
|
||||
- UPDATE: 子类重写 `_select_update_endpoint()`
|
||||
- DELETE: `{node_type}`
|
||||
|
||||
### 6.2 Project 示例
|
||||
|
||||
Project 有多个更新接口:
|
||||
- `project/key_constraints` - 需要字段:key_constraints, key_constraints_remark
|
||||
- `project/complete_investment` - 需要字段:complete_investment
|
||||
- `project/dynamic_investment` - 需要字段:dynamic_investment
|
||||
- `project/plan_construction` - 需要字段:plan_construction_capacity, plan_construction_date
|
||||
- ... 等
|
||||
|
||||
Handler 定义映射表,根据实际数据字段自动选择。
|
||||
|
||||
## 7. 依赖数据获取
|
||||
|
||||
### 7.1 获取模式
|
||||
|
||||
Handler 通过 `collection.filter(node_type="xxx")` 获取依赖数据:
|
||||
|
||||
**加载顺序**(按依赖关系):
|
||||
1. Project(无依赖)
|
||||
2. Contract(依赖 Project)
|
||||
3. Contract Change(依赖 Project + Contract)
|
||||
|
||||
### 7.2 数据流
|
||||
|
||||
```
|
||||
Pipeline
|
||||
→ 按拓扑序加载
|
||||
→ ProjectHandler.load()
|
||||
→ GET project/list
|
||||
→ 创建 ProjectSyncNode
|
||||
→ ContractHandler.load()
|
||||
→ collection.filter(node_type="project")
|
||||
→ 遍历 project,GET contract/list?project_id=xxx
|
||||
→ 创建 ContractSyncNode,设置 context["project_id"]
|
||||
→ ContractChangeHandler.load()
|
||||
→ collection.filter(node_type="contract")
|
||||
→ 遍历 contract,从 context 获取 project_id
|
||||
→ GET contract_change/list?project_id=xxx&contract_id=yyy
|
||||
→ 创建节点,设置 context
|
||||
```
|
||||
|
||||
## 8. 实现计划
|
||||
|
||||
### Phase 1: HTTP 通信层
|
||||
- 实现 **ApiClient** 类
|
||||
- HTTP Client 封装(httpx.AsyncClient)
|
||||
- 签名生成(_generate_signature)
|
||||
- 统一请求方法(get/post/put/delete)
|
||||
- **批量 Push Log 查询**(get_push_logs,单个也传列表)
|
||||
- ~~wait_for_push_log~~(不实现,由 DataSource 调用 poll)
|
||||
|
||||
### Phase 2: DataSource 协调层
|
||||
- 实现 **ApiDataSource** 类
|
||||
- 持有 ApiClient 实例
|
||||
- Handler 注册和管理
|
||||
- 实现 DataSource 接口(load_all, execute_batch)
|
||||
- 注入 api_client 到 Handler
|
||||
- 实现轮询逻辑(_poll_async_tasks)
|
||||
|
||||
### Phase 3: Handler 基类
|
||||
- 实现 **BaseApiHandler** 类
|
||||
- load/create/update/delete/poll 方法
|
||||
- 使用 self.api_client 发送请求
|
||||
- **extract_created_id**(子类可重写)
|
||||
- **_execute_with_retry**(可选,子类可重写重试策略)
|
||||
- Context 管理
|
||||
|
||||
### Phase 4: 业务验证
|
||||
- 实现 **ProjectApiHandler**(无依赖,简单场景)
|
||||
- 实现 load:GET project/list
|
||||
- 实现 create:POST project
|
||||
- 实现 poll:查询 push_log
|
||||
- 实现 **ContractApiHandler**(依赖 Project,验证 context)
|
||||
- 实现 load:遍历 project,GET contract/list?project_id=xxx
|
||||
- 设置 context["project_id"]
|
||||
- 编写测试验证设计
|
||||
|
||||
### Phase 5: 完善功能
|
||||
- 批量 Push Log 查询优化(等待后端实现批量接口)
|
||||
- 超时控制(可配置)
|
||||
- 请求日志和调试
|
||||
|
||||
### Phase 6: 推广
|
||||
- 实现剩余业务 Handler
|
||||
- 性能优化(并发控制)
|
||||
- 文档和示例
|
||||
|
||||
## 9. 与现有系统集成
|
||||
|
||||
**调用关系**:
|
||||
```
|
||||
Pipeline
|
||||
↓
|
||||
ApiDataSource (持有 ApiClient)
|
||||
↓
|
||||
BaseApiHandler (使用 api_client)
|
||||
↓
|
||||
ApiClient (发送 HTTP 请求)
|
||||
```
|
||||
|
||||
**职责分离**:
|
||||
- **ApiClient**: 纯粹的 HTTP 通信,无业务逻辑
|
||||
- **ApiDataSource**: 协调 Handler,管理执行流程
|
||||
- **BaseApiHandler**: 业务逻辑,调用 ApiClient 完成网络操作
|
||||
- **具体 Handler**: 实现特定业务的 load/create/update/delete
|
||||
|
||||
**优点**:
|
||||
- ApiClient 和 Handler 解耦,避免循环依赖
|
||||
- Handler 通过 self.api_client 访问 HTTP 功能
|
||||
- DataSource 统一管理 ApiClient 生命周期
|
||||
- Handler 内部灵活控制重试策略
|
||||
|
||||
## 10. 关键优势
|
||||
|
||||
1. **简化架构**:去掉 Repository 层,代码量减少 50%
|
||||
2. **职责清晰**:
|
||||
- ApiClient:HTTP 通信
|
||||
- DataSource:协调和轮询
|
||||
- Handler:业务逻辑和重试
|
||||
3. **重试灵活**:Handler 内部实现,不同业务可自定义策略
|
||||
4. **状态清晰**:Load 阶段创建 PENDING 节点,执行阶段明确状态转换
|
||||
5. **批量优化**:Push Log 批量查询,减少 HTTP 请求
|
||||
6. **易于扩展**:新业务只需实现 Handler,复用 Client 和 DataSource
|
||||
7. **易于测试**:ApiClient 可 mock,Handler 独立测试
|
||||
|
||||
## 11. 设计改进总结
|
||||
|
||||
| 改进点 | 初始设计 | 最终设计 | 理由 |
|
||||
|-------|---------|---------|------|
|
||||
| Push Log 查询 | get_push_log(id) | get_push_logs([ids]) | 批量查询 |
|
||||
| 轮询逻辑 | wait_for_push_log 在 Client | 移除,DataSource 调用 poll | 职责清晰 |
|
||||
| created_id 提取 | 在 Client | 在 Handler 基类 | 业务差异 |
|
||||
| 重试逻辑 | 在 DataSource | **在 Handler** | 业务灵活性 |
|
||||
| Load 状态 | GET 成功 → SUCCESS | GET 成功 → PENDING | Load ≠ 同步完成 |
|
||||
| 执行状态 | 不明确 | 执行前设置 IN_PROGRESS | 状态清晰 |
|
||||
7. **易于测试**:ApiClient 可以 mock,Handler 单独测试业务逻辑
|
||||
|
||||
## 12. 使用示例
|
||||
|
||||
### 12.1 基本使用流程
|
||||
|
||||
```python
|
||||
# 1. 创建 DataSource
|
||||
datasource = ApiDataSource(
|
||||
base_url="https://api.example.com",
|
||||
uid="user123",
|
||||
secret="secret_key"
|
||||
)
|
||||
|
||||
# 2. 初始化(必须调用)
|
||||
await datasource.initialize()
|
||||
|
||||
# 3. 注册 Handler
|
||||
datasource.register_handler(ProjectApiHandler())
|
||||
datasource.register_handler(ContractApiHandler())
|
||||
|
||||
# 4. 创建 Collection
|
||||
collection = DataCollection()
|
||||
|
||||
# 5. 加载数据(自动处理异常,不会抛出)
|
||||
await datasource.load_all(collection, order=["project", "contract"])
|
||||
|
||||
# 6. 执行同步(自动处理异常,不会抛出)
|
||||
await datasource.sync_all(collection, order=["project", "contract"])
|
||||
|
||||
# 7. 关闭连接(必须调用)
|
||||
await datasource.close()
|
||||
```
|
||||
|
||||
### 12.2 异常处理说明
|
||||
|
||||
**设计原则**:
|
||||
- ✅ **load_all 和 sync_all 不会抛出异常**
|
||||
- ✅ **Handler 的 create/update/delete 捕获所有 HTTP 异常**
|
||||
- ✅ **所有错误转换为状态(SyncStatus.FAILED)和错误信息(node.error)**
|
||||
|
||||
**为什么不需要 try-finally?**
|
||||
|
||||
```python
|
||||
# ❌ 不需要这样做
|
||||
try:
|
||||
await datasource.load_all(collection, order=["project"])
|
||||
finally:
|
||||
await datasource.close()
|
||||
|
||||
# ✅ 直接调用即可
|
||||
await datasource.initialize()
|
||||
await datasource.load_all(collection, order=["project"]) # 内部已处理异常
|
||||
await datasource.sync_all(collection, order=["project"]) # 内部已处理异常
|
||||
await datasource.close()
|
||||
```
|
||||
|
||||
**异常处理机制**:
|
||||
|
||||
1. **load_all 异常处理**:
|
||||
```python
|
||||
# 加载某个类型失败,记录错误但继续处理其他类型
|
||||
for node_type in order:
|
||||
try:
|
||||
raw_items = await handler.load(collection)
|
||||
# ... 创建节点
|
||||
except Exception as e:
|
||||
print(f"Error loading {node_type}: {e}")
|
||||
# 继续处理下一个类型
|
||||
```
|
||||
|
||||
2. **sync_all 异常处理**:
|
||||
```python
|
||||
# Handler 返回的异常已被捕获转为 TaskResult
|
||||
for node in nodes:
|
||||
try:
|
||||
result = await handler.create(node) # Handler 内部捕获异常
|
||||
# 根据 result.status 更新节点状态
|
||||
except Exception as e:
|
||||
# DataSource 的最后保护
|
||||
node.status = SyncStatus.FAILED
|
||||
node.error = str(e)
|
||||
```
|
||||
|
||||
3. **Handler 异常处理**:
|
||||
```python
|
||||
async def create(self, node: SyncNode) -> TaskResult:
|
||||
try:
|
||||
# 生成参数
|
||||
push_id = self._generate_push_id()
|
||||
data = {..., "push_id": push_id, "biz_id": self._generate_biz_id(node)}
|
||||
|
||||
# 调用 API
|
||||
response = await self.api_client.post("/api/v2/project", data)
|
||||
|
||||
# 返回成功
|
||||
return TaskResult(status=TaskStatus.SUCCESS, data_id=response['data']['id'])
|
||||
|
||||
except httpx.HTTPStatusError as e:
|
||||
# HTTP 错误(4xx, 5xx)
|
||||
return TaskResult(status=TaskStatus.FAILED, error=f"HTTP {e.response.status_code}: {e}")
|
||||
except httpx.NetworkError as e:
|
||||
# 网络错误
|
||||
return TaskResult(status=TaskStatus.FAILED, error=f"Network error: {e}")
|
||||
except Exception as e:
|
||||
# 其他异常
|
||||
return TaskResult(status=TaskStatus.FAILED, error=f"Unexpected error: {e}")
|
||||
```
|
||||
|
||||
### 12.3 Pipeline 集成示例
|
||||
|
||||
```python
|
||||
async def sync_pipeline(datasource: ApiDataSource):
|
||||
"""同步流程编排"""
|
||||
|
||||
# 初始化
|
||||
await datasource.initialize()
|
||||
|
||||
# 创建 Collection
|
||||
collection = DataCollection()
|
||||
|
||||
# 执行同步流程(所有错误都被内部处理)
|
||||
await datasource.load_all(collection, order=["project", "contract", "material"])
|
||||
await datasource.sync_all(collection, order=["project", "contract", "material"])
|
||||
|
||||
# 检查结果
|
||||
failed_nodes = collection.filter(status=SyncStatus.FAILED)
|
||||
if failed_nodes:
|
||||
print(f"Failed nodes: {len(failed_nodes)}")
|
||||
for node in failed_nodes:
|
||||
print(f" - {node.node_type}:{node.node_id}: {node.error}")
|
||||
|
||||
# 清理
|
||||
await datasource.close()
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- ✅ 不需要 try-except 包裹 load_all/sync_all
|
||||
- ✅ 通过检查节点状态判断成功/失败
|
||||
- ✅ 错误信息保存在 node.error 中
|
||||
- ✅ Pipeline 专注于流程编排,不处理异常细节
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
# DataSource + Handler 架构设计 V2
|
||||
|
||||
## 核心原则
|
||||
|
||||
### **职责分离**
|
||||
|
||||
```text
|
||||
Strategy (业务逻辑层)
|
||||
↓ 设置 action, 完成 ID 映射
|
||||
DataSource (状态管理 + 编排层)
|
||||
↓ 调用 Handler, 管理状态流转
|
||||
Handler (纯执行层)
|
||||
↓ 调用后端 API, 返回结果
|
||||
Backend API
|
||||
```
|
||||
|
||||
### **关键设计决策**
|
||||
|
||||
1. **ID 映射在 Strategy 完成** ✅
|
||||
- `Strategy.create()` / `Strategy.update()` 时,已将依赖字段的本地 ID 替换为远程 ID
|
||||
- DataSource 拿到的 `node.data` 已经是映射好的数据
|
||||
- Handler 直接使用 `node.data`,不需要关心 ID 映射
|
||||
|
||||
2. **状态管理在 DataSource** ✅
|
||||
- 所有 `node.status` 的转换由 DataSource 负责
|
||||
- Handler 只返回 `TaskResult`(成功/失败/进行中)
|
||||
- DataSource 根据 `TaskResult` 更新 `node.status`
|
||||
|
||||
3. **Handler 只负责执行** ✅
|
||||
- 调用后端 API(load/create/update/delete)
|
||||
- 返回执行结果(TaskResult)
|
||||
- 提供异步任务轮询(poll_tasks)
|
||||
- 提供数据质量检查(validate)
|
||||
|
||||
---
|
||||
|
||||
## Handler 接口设计
|
||||
|
||||
### **核心方法**
|
||||
|
||||
```python
|
||||
class NodeHandler(Protocol[T]):
|
||||
# ===== 数据加载 =====
|
||||
async def load(self, collection: Optional[DataCollection] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
从后端加载全部数据
|
||||
|
||||
Args:
|
||||
collection: Collection 上下文(可选,用于依赖查询)
|
||||
|
||||
Returns:
|
||||
原始数据列表
|
||||
|
||||
示例:
|
||||
# Contract Handler(依赖 Project)
|
||||
async def load(self, collection=None):
|
||||
# 从 collection 获取 project 列表
|
||||
if collection:
|
||||
projects = collection.filter(node_type="project")
|
||||
project_ids = [p.data_id for p in projects if p.data_id]
|
||||
else:
|
||||
project_ids = []
|
||||
|
||||
# 只加载这些项目的合同
|
||||
response = await self.api.get_contracts(project_ids=project_ids)
|
||||
return response["data"]
|
||||
"""
|
||||
|
||||
# ===== 数据操作 =====
|
||||
async def create(self, node: SyncNode[T]) -> TaskResult:
|
||||
"""
|
||||
创建节点
|
||||
|
||||
注意:node.data 已经过 ID 映射,直接使用即可
|
||||
|
||||
Returns:
|
||||
TaskResult:
|
||||
- SUCCESS: 同步完成,返回 data_id
|
||||
- IN_PROGRESS: 异步任务,返回 task_id
|
||||
- FAILED: 失败,返回 error
|
||||
|
||||
示例:
|
||||
# 同步接口
|
||||
async def create(self, node):
|
||||
try:
|
||||
response = await self.api.create_contract(node.data)
|
||||
return TaskResult.success(data_id=response["id"])
|
||||
except Exception as e:
|
||||
return TaskResult.failed(error=str(e))
|
||||
|
||||
# 异步接口
|
||||
async def create(self, node):
|
||||
try:
|
||||
response = await self.api.submit_contract_async(node.data)
|
||||
return TaskResult.in_progress(task_id=response["task_id"])
|
||||
except Exception as e:
|
||||
return TaskResult.failed(error=str(e))
|
||||
"""
|
||||
|
||||
async def update(self, node: SyncNode[T]) -> TaskResult:
|
||||
"""更新节点(同 create)"""
|
||||
|
||||
async def delete(self, node: SyncNode[T]) -> TaskResult:
|
||||
"""删除节点(同 create)"""
|
||||
|
||||
# ===== 异步任务轮询 =====
|
||||
async def poll_tasks(self, task_ids: List[str]) -> Dict[str, TaskResult]:
|
||||
"""
|
||||
批量轮询异步任务状态
|
||||
|
||||
Args:
|
||||
task_ids: 任务 ID 列表
|
||||
|
||||
Returns:
|
||||
{task_id: TaskResult} 字典
|
||||
|
||||
示例:
|
||||
async def poll_tasks(self, task_ids):
|
||||
response = await self.api.batch_query_tasks(task_ids)
|
||||
results = {}
|
||||
for task in response["tasks"]:
|
||||
if task["status"] == "completed":
|
||||
results[task["id"]] = TaskResult.success(
|
||||
data_id=task["result"]["contract_id"]
|
||||
)
|
||||
elif task["status"] == "failed":
|
||||
results[task["id"]] = TaskResult.failed(
|
||||
error=task["error"]
|
||||
)
|
||||
else: # running
|
||||
results[task["id"]] = TaskResult.in_progress(
|
||||
task_id=task["id"]
|
||||
)
|
||||
return results
|
||||
"""
|
||||
|
||||
# ===== 数据质量检查 =====
|
||||
async def validate(self, data: Dict[str, Any]) -> ValidationResult:
|
||||
"""
|
||||
数据质量检查
|
||||
|
||||
Args:
|
||||
data: 原始数据
|
||||
|
||||
Returns:
|
||||
ValidationResult(是否有效 + 错误列表)
|
||||
|
||||
示例:
|
||||
async def validate(self, data):
|
||||
errors = []
|
||||
if not data.get("code"):
|
||||
errors.append("合同编码为空")
|
||||
if not data.get("project_id"):
|
||||
errors.append("项目 ID 为空")
|
||||
|
||||
return ValidationResult(
|
||||
is_valid=len(errors) == 0,
|
||||
errors=errors
|
||||
)
|
||||
"""
|
||||
|
||||
# ===== 辅助方法 =====
|
||||
def extract_id(self, data: Dict[str, Any]) -> str:
|
||||
"""从原始数据提取 ID"""
|
||||
|
||||
def create_node(self, data: Dict[str, Any]) -> SyncNode[T]:
|
||||
"""从原始数据创建 SyncNode"""
|
||||
```
|
||||
|
||||
### **TaskResult 设计**
|
||||
|
||||
```python
|
||||
class TaskStatus(Enum):
|
||||
SUCCESS = "success" # 同步完成
|
||||
FAILED = "failed" # 失败
|
||||
IN_PROGRESS = "in_progress" # 异步任务进行中
|
||||
|
||||
|
||||
class TaskResult:
|
||||
def __init__(
|
||||
self,
|
||||
status: TaskStatus,
|
||||
data_id: Optional[str] = None, # 成功时返回的 ID
|
||||
task_id: Optional[str] = None, # 异步任务 ID
|
||||
error: Optional[str] = None, # 失败原因
|
||||
metadata: Optional[Dict] = None # 其他元数据
|
||||
):
|
||||
...
|
||||
|
||||
@classmethod
|
||||
def success(cls, data_id: Optional[str] = None) -> TaskResult:
|
||||
"""创建成功结果"""
|
||||
|
||||
@classmethod
|
||||
def failed(cls, error: str) -> TaskResult:
|
||||
"""创建失败结果"""
|
||||
|
||||
@classmethod
|
||||
def in_progress(cls, task_id: str) -> TaskResult:
|
||||
"""创建进行中结果"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DataSource 接口设计
|
||||
|
||||
### **接口方法**
|
||||
|
||||
```python
|
||||
class BaseDataSource(ABC):
|
||||
# ===== Handler 管理 =====
|
||||
def register_handler(self, handler: NodeHandler) -> None:
|
||||
"""注册 Handler"""
|
||||
|
||||
def get_handler(self, node_type: str) -> NodeHandler:
|
||||
"""获取 Handler"""
|
||||
|
||||
# ===== 数据加载 =====
|
||||
async def load_all(
|
||||
self,
|
||||
order: List[str]
|
||||
) -> None:
|
||||
"""
|
||||
按依赖顺序加载所有数据
|
||||
|
||||
注意:调用前需先 set_collection()
|
||||
|
||||
Args:
|
||||
order: 节点类型顺序(如 ["project", "contract", "construction"])
|
||||
|
||||
工作流程:
|
||||
1. 按顺序遍历每个节点类型
|
||||
2. 调用 Handler.load()
|
||||
3. Handler 可以从 self._collection 查询依赖节点
|
||||
4. 使用 Handler.create_node() 创建 SyncNode
|
||||
5. 添加到 Collection
|
||||
"""
|
||||
|
||||
# ===== 同步执行 =====
|
||||
async def sync_all(
|
||||
self,
|
||||
order: List[str]
|
||||
) -> None:
|
||||
"""
|
||||
按依赖顺序执行同步
|
||||
|
||||
流程:
|
||||
1. 按顺序遍历每个节点类型
|
||||
2. 筛选需要同步的节点(status=PENDING, action!=NONE)
|
||||
3. 调用 Handler 的 create/update/delete
|
||||
4. 根据 TaskResult 更新节点状态
|
||||
5. 处理异步任务轮询
|
||||
"""
|
||||
|
||||
# ===== 统计信息 =====
|
||||
def get_sync_summary(self, collection: DataCollection) -> Dict[str, Any]:
|
||||
"""获取同步统计信息"""
|
||||
```
|
||||
|
||||
### **状态流转逻辑**
|
||||
|
||||
```python
|
||||
async def _sync_nodes(self, handler, nodes):
|
||||
async_tasks = {} # {node_id: task_id}
|
||||
|
||||
for node in nodes:
|
||||
# 1. 跳过不需要处理的节点
|
||||
if node.action == SyncAction.NONE:
|
||||
continue
|
||||
|
||||
# 2. 跳过异常状态节点
|
||||
if node.binding_status in [DEPENDENCY_ERROR, ABNORMAL, WARNING]:
|
||||
node.status = SyncStatus.SKIPPED
|
||||
continue
|
||||
|
||||
# 3. 只处理 PENDING 节点
|
||||
if node.status != SyncStatus.PENDING:
|
||||
continue
|
||||
|
||||
# 4. 执行操作
|
||||
try:
|
||||
node.status = SyncStatus.IN_PROGRESS
|
||||
|
||||
if node.action == SyncAction.CREATE:
|
||||
result = await handler.create(node)
|
||||
elif node.action == SyncAction.UPDATE:
|
||||
result = await handler.update(node)
|
||||
elif node.action == SyncAction.DELETE:
|
||||
result = await handler.delete(node)
|
||||
|
||||
# 5. 处理结果
|
||||
if result.status == TaskStatus.SUCCESS:
|
||||
node.status = SyncStatus.SUCCESS
|
||||
if result.data_id:
|
||||
node.data_id = result.data_id # 回填 ID
|
||||
|
||||
elif result.status == TaskStatus.FAILED:
|
||||
node.status = SyncStatus.FAILED
|
||||
node.error = result.error
|
||||
|
||||
elif result.status == TaskStatus.IN_PROGRESS:
|
||||
async_tasks[node.node_id] = result.task_id
|
||||
|
||||
except Exception as e:
|
||||
node.status = SyncStatus.FAILED
|
||||
node.error = str(e)
|
||||
|
||||
# 6. 轮询异步任务
|
||||
if async_tasks:
|
||||
await self._poll_async_tasks(handler, nodes, async_tasks)
|
||||
```
|
||||
|
||||
### **异步任务轮询**
|
||||
|
||||
```python
|
||||
async def _poll_async_tasks(self, handler, nodes, async_tasks):
|
||||
node_map = {n.node_id: n for n in nodes}
|
||||
|
||||
max_retries = poll_max_retries # 默认轮询 1 次,可配置
|
||||
retry_count = 0
|
||||
|
||||
while async_tasks and retry_count < max_retries:
|
||||
# 批量查询
|
||||
task_ids = list(async_tasks.values())
|
||||
results = await handler.poll_tasks(task_ids)
|
||||
|
||||
# 处理结果
|
||||
for node_id, task_id in list(async_tasks.items()):
|
||||
if task_id not in results:
|
||||
continue
|
||||
|
||||
result = results[task_id]
|
||||
node = node_map[node_id]
|
||||
|
||||
if result.status == TaskStatus.SUCCESS:
|
||||
node.status = SyncStatus.SUCCESS
|
||||
if result.data_id:
|
||||
node.data_id = result.data_id
|
||||
async_tasks.pop(node_id)
|
||||
|
||||
elif result.status == TaskStatus.FAILED:
|
||||
node.status = SyncStatus.FAILED
|
||||
node.error = result.error
|
||||
async_tasks.pop(node_id)
|
||||
|
||||
# 还有未完成任务,等待后重试
|
||||
if async_tasks:
|
||||
await asyncio.sleep(2)
|
||||
retry_count += 1
|
||||
|
||||
# 超时未完成的任务标记为失败
|
||||
for node_id in async_tasks:
|
||||
node = node_map[node_id]
|
||||
node.status = SyncStatus.FAILED
|
||||
node.error = "Async task timeout"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整流程示例
|
||||
|
||||
### **Pipeline 编排**
|
||||
|
||||
```python
|
||||
# Stage 0: 初始化
|
||||
collection = DataCollection()
|
||||
datasource = ApiDataSource(api_client)
|
||||
|
||||
# 注册 Handler
|
||||
datasource.register_handler(ProjectNodeHandler(api_client))
|
||||
datasource.register_handler(ContractNodeHandler(api_client))
|
||||
|
||||
# Stage 1: 加载数据
|
||||
datasource.set_collection(collection)
|
||||
await datasource.load_all(
|
||||
order=["project", "contract"] # 依赖顺序
|
||||
)
|
||||
|
||||
# Stage 2: Strategy 执行
|
||||
for strategy in [project_strategy, contract_strategy]:
|
||||
await strategy.bind() # 设置 binding_status
|
||||
await strategy.create() # 设置 action=CREATE + ID 映射
|
||||
await strategy.update() # 设置 action=UPDATE + ID 映射
|
||||
await strategy.pre_sync_check()
|
||||
|
||||
# Stage 3: 物理同步
|
||||
await datasource.sync_all(
|
||||
order=["project", "contract"]
|
||||
)
|
||||
|
||||
# Stage 4: 检查结果
|
||||
summary = datasource.get_sync_summary(collection)
|
||||
print(f"Success: {summary['success']}, Failed: {summary['failed']}")
|
||||
```
|
||||
|
||||
### **Contract Handler 实现**
|
||||
|
||||
```python
|
||||
class ContractNodeHandler(BaseNodeHandler[ContractResponse]):
|
||||
def __init__(self, api_client):
|
||||
super().__init__(
|
||||
node_type="contract",
|
||||
node_class=ContractSyncNode,
|
||||
schema=ContractResponse
|
||||
)
|
||||
self.api = api_client
|
||||
|
||||
async def load(self):
|
||||
"""加载合同(依赖 Project)"""
|
||||
if self._collection:
|
||||
projects = self._collection.filter(node_type="project")
|
||||
project_ids = [p.data_id for p in projects if p.data_id]
|
||||
else:
|
||||
project_ids = []
|
||||
|
||||
response = await self.api.get_contracts(project_ids=project_ids)
|
||||
return response["data"]
|
||||
|
||||
async def create(self, node):
|
||||
"""创建合同(异步接口)"""
|
||||
try:
|
||||
# node.data 已经过 ID 映射,直接使用
|
||||
response = await self.api.create_contract_async(node.data)
|
||||
|
||||
if response.get("async"):
|
||||
return TaskResult.in_progress(task_id=response["task_id"])
|
||||
else:
|
||||
return TaskResult.success(data_id=response["id"])
|
||||
|
||||
except Exception as e:
|
||||
return TaskResult.failed(error=str(e))
|
||||
|
||||
async def update(self, node):
|
||||
"""更新合同(同步接口)"""
|
||||
try:
|
||||
response = await self.api.update_contract(node.data_id, node.data)
|
||||
return TaskResult.success()
|
||||
except Exception as e:
|
||||
return TaskResult.failed(error=str(e))
|
||||
|
||||
async def poll_tasks(self, task_ids):
|
||||
"""轮询异步任务"""
|
||||
response = await self.api.batch_query_tasks(task_ids)
|
||||
results = {}
|
||||
for task in response["tasks"]:
|
||||
if task["status"] == "completed":
|
||||
results[task["id"]] = TaskResult.success(
|
||||
data_id=task["result"]["contract_id"]
|
||||
)
|
||||
elif task["status"] == "failed":
|
||||
results[task["id"]] = TaskResult.failed(error=task["error"])
|
||||
else:
|
||||
results[task["id"]] = TaskResult.in_progress(task_id=task["id"])
|
||||
return results
|
||||
|
||||
async def validate(self, data):
|
||||
"""数据质量检查"""
|
||||
errors = []
|
||||
if not data.get("code"):
|
||||
errors.append("合同编码为空")
|
||||
if not data.get("project_id"):
|
||||
errors.append("项目 ID 为空")
|
||||
|
||||
return ValidationResult(is_valid=len(errors) == 0, errors=errors)
|
||||
|
||||
def extract_id(self, data):
|
||||
return data["id"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心优势
|
||||
|
||||
### **1. 职责清晰**
|
||||
|
||||
- **Strategy**: 业务逻辑 + ID 映射
|
||||
- **DataSource**: 状态管理 + 编排
|
||||
- **Handler**: 纯执行(后端 API 调用)
|
||||
|
||||
### **2. ID 映射前置**
|
||||
|
||||
- ID 映射在 Strategy 完成,DataSource 和 Handler 无需关心
|
||||
- `node.data` 已经是映射好的数据,直接使用
|
||||
|
||||
### **3. 状态统一管理**
|
||||
|
||||
- 所有状态流转由 DataSource 负责
|
||||
- Handler 只返回结果,不改变节点状态
|
||||
|
||||
### **4. 异步任务支持**
|
||||
|
||||
- Handler 返回 `TaskResult.in_progress(task_id)`
|
||||
- DataSource 自动轮询直到完成
|
||||
|
||||
### **5. 数据质量保障**
|
||||
|
||||
|
||||
- Handler 提供 `validate()` 方法
|
||||
- DataSource 在加载时检查数据质量
|
||||
|
||||
### **6. 易于测试**
|
||||
|
||||
- Handler 可以 Mock(返回假 TaskResult)
|
||||
- DataSource 可以单独测试状态流转逻辑
|
||||
- Strategy 可以单独测试 ID 映射逻辑
|
||||
|
||||
---
|
||||
|
||||
## 与现有架构的兼容性
|
||||
|
||||
完全符合 [架构分层设计.md](./架构分层设计.md) 的四层模型:
|
||||
|
||||
1. **Common 层**: SyncNode, DataCollection, BindingManager
|
||||
2. **Sync System 层**: SyncStrategy(负责 bind/create/update + ID 映射)
|
||||
3. **DataSource 层**: BaseDataSource + NodeHandler(状态管理 + 执行)
|
||||
4. **Pipeline 层**: 流程编排 + 依赖顺序
|
||||
|
||||
符合 [状态定义规范.md](./状态定义规范.md) 的状态流转逻辑。
|
||||
@@ -0,0 +1,150 @@
|
||||
# node_id / data_id / 持久化与生命周期说明
|
||||
|
||||
本文档说明同步系统中 `node_id` 与 `data_id` 的职责、生成时机、持久化行为,以及同步流程中“创建/绑定/回填”的完整生命周期。目标是避免“重复生成 node_id 导致绑定失效”的问题,并明确各层职责。
|
||||
|
||||
---
|
||||
|
||||
## 1. 基础概念
|
||||
|
||||
- **data_id**
|
||||
- 业务主键(来自数据源/远端系统的资源 ID)。
|
||||
- 在加载阶段通常已存在;在 CREATE 场景下初始为空,成功后由 DataSource 回填。
|
||||
|
||||
- **node_id**
|
||||
- 同步系统内部的**稳定标识符**(绑定与持久化的主键)。
|
||||
- 用于绑定表、持久化节点、依赖追踪等。
|
||||
- 设计目标:**跨同步周期稳定**,不能随每次加载而变化。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心结论(必须遵守)
|
||||
|
||||
1. **node_id 一旦建立,不应再被数据源“重新生成”。**
|
||||
- 绑定关系与持久化状态依赖 `node_id`;若每次加载都重新生成,会导致历史绑定失效。
|
||||
2. **加载阶段应先恢复 Collection(持久化节点),再加载 DataSource 数据。**
|
||||
- 数据源加载出的 `data_id` 应优先匹配已持久化的节点;匹配到则复用旧 `node_id`。
|
||||
3. **CREATE 场景允许临时/新建 node_id**,但必须通过后续回填与绑定更新保持一致性。
|
||||
|
||||
---
|
||||
|
||||
## 3. 正确的初始化/加载顺序
|
||||
|
||||
### 推荐流程(Pipeline 前半段)
|
||||
|
||||
1. **持久化恢复阶段**
|
||||
- 必须同时恢复 `node_id` 与 `data_id`(以及状态字段),否则无法建立稳定的映射关系。
|
||||
- Collection 可能为空,这属于合法情况。
|
||||
|
||||
2. **数据源加载阶段(DataSource.load_all)**
|
||||
- 对每一条业务数据(含 `data_id`):
|
||||
- **若 Collection 内已存在该 `data_id` 对应节点**:将数据写回原节点(保留旧 `node_id`)。
|
||||
- **若不存在**:由 **Collection** 生成新的 `node_id`,并创建新节点。
|
||||
|
||||
> 这样可避免“第二次同步时重建节点,导致旧绑定丢失”。
|
||||
|
||||
---
|
||||
|
||||
## 4. node_id 的来源与稳定性
|
||||
|
||||
### A. 持久化恢复阶段
|
||||
|
||||
- **恢复时必须包含 `node_id` 与 `data_id`**:
|
||||
- 两者成对恢复,才能保证后续 load 阶段按 `data_id` 命中旧节点。
|
||||
|
||||
### B. 数据源加载阶段
|
||||
|
||||
- **优先复用持久化 node_id**:
|
||||
- 通过 `data_id` 在 Collection 中查找并更新已有节点。
|
||||
- **仅在未知 data_id 时新建 node_id**:
|
||||
- 新节点 `node_id` 必须由 **Collection** 生成,而非 DataSource/Handler 直接拼接。
|
||||
|
||||
### C. 创建阶段(Strategy.create)
|
||||
|
||||
- 新建“目标端”节点时,**必须有 node_id**:
|
||||
- 用于写入 Collection、记录状态、建立临时绑定。
|
||||
- 成功后回填 `data_id`,**node_id 不变**。
|
||||
|
||||
---
|
||||
|
||||
## 5. data_id 的来源与回填
|
||||
|
||||
本节按阶段拆分说明 `data_id` 的来源与写回。
|
||||
|
||||
### A. 持久化恢复阶段
|
||||
|
||||
- `data_id` 从持久化数据中恢复(与 `node_id` 一起恢复)。
|
||||
|
||||
### B. 数据源加载阶段
|
||||
|
||||
- `data_id` 来自数据源原始数据(`id` / `_id`)。
|
||||
- 若命中已有节点,则仅更新数据与状态,不改变 `node_id`。
|
||||
|
||||
### C. 同步创建与轮询阶段
|
||||
|
||||
- CREATE 提交后,DataSource 从响应/轮询结果提取 `biz_id`。
|
||||
- 成功时回填到 `node.data_id`,并回写 `node.data` 的主键字段(仅 `id` / `_id`)。
|
||||
|
||||
### D. 持久化阶段
|
||||
|
||||
- 将更新后的 `node_id` / `data_id` / 状态字段持久化,供下次同步恢复使用。
|
||||
|
||||
---
|
||||
|
||||
## 6. 绑定与 node_id 的关系
|
||||
|
||||
- 绑定表的 key 为 `node_id`,绑定记录是 `local_node_id <-> remote_node_id`。
|
||||
- 因此:**node_id 必须稳定**,才能保证绑定长期有效。
|
||||
- 绑定可在创建前建立“临时关系”,但成功后不应更换 node_id。
|
||||
|
||||
---
|
||||
|
||||
## 7. 同步周期中的典型场景
|
||||
|
||||
### 场景 1:第一次同步(无持久化)
|
||||
- Collection 为空 → DataSource load 新节点 → 生成新 `node_id`。
|
||||
- Strategy bind / create / update 执行。
|
||||
- 持久化后,下次同步可复用 `node_id`。
|
||||
|
||||
### 场景 2:第二次同步(已有持久化)
|
||||
- 先恢复 Collection(已有 `node_id`)。
|
||||
- DataSource load 时按 `data_id` 匹配旧节点并更新数据。
|
||||
- 绑定关系仍有效。
|
||||
|
||||
### 场景 3:创建新数据(CREATE)
|
||||
- Strategy 产生目标端节点(`node_id` 新生成)。
|
||||
- DataSource 执行创建,回填 `data_id`。
|
||||
- 绑定记录更新为真实 `data_id`(但 node_id 保持不变)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 关键规则总结
|
||||
|
||||
- **node_id 是“同步系统内的稳定 ID”,必须持久化与复用。**
|
||||
- **data_id 是“业务系统的资源 ID”,由数据源加载或创建后回填。**
|
||||
- **加载顺序:先恢复持久化节点 → 再 load 数据源。**
|
||||
- **遇到已存在 data_id:复用 node_id;否则创建新节点。**
|
||||
- **CREATE 成功后只更新 data_id,不更换 node_id。**
|
||||
|
||||
---
|
||||
|
||||
## 9. 相关文件参考
|
||||
|
||||
- 绑定与状态定义:见 [docs/状态定义规范.md](状态定义规范.md)
|
||||
- 编排流程:见 [docs/编排流程规范.md](编排流程规范.md)
|
||||
- BaseDataSource:见 [sync_system_new/datasource/datasource.py](../sync_system_new/datasource/datasource.py)
|
||||
- BaseApiHandler:见 [sync_system_new/datasource/api/handler.py](../sync_system_new/datasource/api/handler.py)
|
||||
|
||||
---
|
||||
|
||||
## 10. 明确规则与建议实现
|
||||
|
||||
- **DataSource.load_all 必须先按 `data_id` 命中已有节点并更新**,未命中时再由 Collection 创建新节点并分配新的 `node_id`。
|
||||
- **推荐实现方式**:先 `find_by_data_id`,命中走通用更新;未命中再创建新节点(无需强制 `upsert_by_data_id` 接口)。
|
||||
- **创建流程中的临时绑定/成功更新规则**:
|
||||
- CREATE 节点创建时:`binding_status = NORMAL`,`action = CREATE`,`status = PENDING`,`data_id = ""`。
|
||||
- 执行提交前置为 `status = IN_PROGRESS`(异步)或保持 `PENDING`(同步接口待执行)。
|
||||
- 成功回填后:`status = SUCCESS`,`data_id` 回填并写回主键字段,绑定记录保持 `node_id` 不变。
|
||||
- 过程中出现错误:`status = FAILED`,`error` 记录原因。
|
||||
- **异常节点持久化/恢复策略**:
|
||||
- 持久化时保留异常节点与错误信息。
|
||||
- 恢复时是否保留/清理异常节点应通过策略配置控制(默认建议保留以便人工修复)。
|
||||
@@ -0,0 +1,232 @@
|
||||
# Skip Sync 行为修复说明
|
||||
|
||||
## 问题分析
|
||||
|
||||
### 1. **绑定失败的根本原因**
|
||||
|
||||
**问题现象**:
|
||||
- Company: 1/39 绑定
|
||||
- Supplier: 0/65202 绑定(完全没绑定!)
|
||||
- 导致依赖supplier的业务(contract, material等)也绑不上
|
||||
- 远程异常创建了74条数据
|
||||
|
||||
**根本原因有两个**:
|
||||
|
||||
#### 原因1:skip_sync 跳过了整个策略(包括 bind)
|
||||
|
||||
```python
|
||||
# 旧逻辑:skip_sync=True 时跳过整个策略
|
||||
if getattr(strategy, 'skip_sync', False):
|
||||
continue # ← 跳过了 bind/create/update 全部!
|
||||
```
|
||||
|
||||
修复后:
|
||||
```python
|
||||
# 新逻辑:bind() 始终执行,skip_sync 只跳过 create/update/sync
|
||||
await strategy.bind() # ← 始终执行
|
||||
|
||||
if getattr(strategy, 'skip_sync', False):
|
||||
continue # ← 只跳过 create/update
|
||||
```
|
||||
|
||||
#### 原因2:自动绑定ID解析失败没有设置DEPENDENCY_ERROR
|
||||
|
||||
```python
|
||||
# 旧逻辑:ID解析失败只是跳过
|
||||
if not resolved_key_dict:
|
||||
continue # ← 节点保持 MISSING,会被误创建!
|
||||
```
|
||||
|
||||
修复后:
|
||||
```python
|
||||
# 新逻辑:ID解析失败设置DEPENDENCY_ERROR
|
||||
if not resolved_key_dict:
|
||||
node.set_binding_status(BindingStatus.DEPENDENCY_ERROR, "业务键中的ID字段解析失败")
|
||||
continue # ← 节点为 DEPENDENCY_ERROR,不会被创建
|
||||
```
|
||||
|
||||
### 2. **状态与创建的关系**
|
||||
|
||||
| 状态 | 来源 | 允许创建 | 说明 |
|
||||
|------|------|:---:|------|
|
||||
| **UNCHECKED** | 加载后初始状态 | ❌ | bind() 未执行 |
|
||||
| **NORMAL** | 有绑定记录 | ❌ | 已绑定 |
|
||||
| **MISSING** | 无绑定记录 | ✅ | 真正的孤儿 |
|
||||
| **WARNING** | 业务键字段缺失 | ❌ | 数据不完整 |
|
||||
| **DEPENDENCY_ERROR** | 依赖不满足 | ❌ | 父节点未就绪 |
|
||||
| **ABNORMAL** | 数据损坏 | ❌ | 需人工处理 |
|
||||
|
||||
**关键原则**:
|
||||
- `create()` 只查找 `binding_status == MISSING` 的节点
|
||||
- 如果 bind() 没执行,状态是 UNCHECKED → 不会创建
|
||||
- 如果 bind() 执行了但依赖不满足 → DEPENDENCY_ERROR → 不会创建
|
||||
|
||||
### 3. **Supplier 默认行为变更**
|
||||
|
||||
**旧行为**:
|
||||
- `default_skip_sync = True` → 默认跳过同步
|
||||
- 需要在调用层设置 `force_sync_types=["supplier"]`
|
||||
|
||||
**新行为**:
|
||||
- `default_skip_sync` 已移除(使用基类默认值 False)
|
||||
- Supplier 默认会执行完整同步(bind/create/update)
|
||||
- 如需跳过,在调用层配置 `skip_sync_types=["supplier"]`
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 修改 1:分离 bind() 和 skip_sync
|
||||
|
||||
**新逻辑**:
|
||||
```python
|
||||
# sync_system_new/pipeline/full_sync_pipeline.py
|
||||
|
||||
# Step 1a: 绑定(始终执行)
|
||||
await strategy.bind()
|
||||
|
||||
# Step 1b: 检查是否跳过 create/update/sync
|
||||
if getattr(strategy, 'skip_sync', False):
|
||||
print(f"⏭️ Skipping create/update/sync for {node_type}")
|
||||
# 继续执行 pre_sync_check,但跳过 create/update/sync
|
||||
if hasattr(strategy, "pre_sync_check"):
|
||||
ok = await strategy.pre_sync_check()
|
||||
...
|
||||
continue
|
||||
|
||||
# Step 1c: 正常的 create/update
|
||||
await strategy.create()
|
||||
await strategy.update()
|
||||
```
|
||||
|
||||
**效果**:
|
||||
- ✅ **bind() 始终执行**(建立绑定关系)
|
||||
- ✅ **skip_sync=True 只跳过 create/update/sync**(避免昂贵操作)
|
||||
- ✅ **pre_sync_check 仍然执行**(依赖检查)
|
||||
|
||||
### 修改 2:添加 force_sync_types 配置
|
||||
|
||||
**配置示例**:
|
||||
```python
|
||||
# tests/run_full_sync_pipeline_simple.py
|
||||
CONFIG = {
|
||||
# 第一次运行:从 API 加载 + 绑定 + 创建
|
||||
"force_sync_types": ["supplier"],
|
||||
"wipe_persistence_on_start": True,
|
||||
|
||||
# 第二次运行:从持久化加载 + 跳过同步
|
||||
# "handler_configs": {"supplier": {"load_mode": "persisted_only"}},
|
||||
# "skip_sync_types": ["supplier"],
|
||||
# "wipe_persistence_on_start": False,
|
||||
}
|
||||
```
|
||||
|
||||
**优先级**:
|
||||
1. **force_sync_types**(最高)→ `skip_sync = False`
|
||||
2. **skip_sync_types** → `skip_sync = True`
|
||||
3. **策略默认值** (default_skip_sync)
|
||||
|
||||
### 修改 3:添加测试日志保存
|
||||
|
||||
**功能**:
|
||||
```python
|
||||
# tests/run_full_sync_pipeline_simple.py
|
||||
log_path = log_dir / f"simple_pipeline_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
||||
sys.stdout = Tee(original_stdout, log_file)
|
||||
```
|
||||
|
||||
**输出位置**:
|
||||
```
|
||||
test_results/simple_pipeline_20260204_162345.log
|
||||
```
|
||||
|
||||
## 使用指南
|
||||
|
||||
### Phase 1: 首次运行(API + 绑定 + 创建)
|
||||
|
||||
```python
|
||||
CONFIG = {
|
||||
# 不需要特殊配置,所有类型默认都会同步
|
||||
"wipe_persistence_on_start": True, # 清空数据库
|
||||
}
|
||||
```
|
||||
|
||||
**预期结果**:
|
||||
- Supplier: 从API加载60000条 → 自动绑定(credit_code) → 拉取到本地 → 持久化
|
||||
- Company: 自动绑定 → 拉取
|
||||
- Contract等: 通过supplier_id/project_id绑定 → 创建
|
||||
|
||||
### Phase 2: 后续运行(持久化 + 跳过同步)
|
||||
|
||||
```python
|
||||
CONFIG = {
|
||||
"handler_configs": {
|
||||
"supplier": {"load_mode": "persisted_only"} # 从DB加载
|
||||
},
|
||||
"skip_sync_types": ["supplier"], # 跳过create/update(但仍执行bind)
|
||||
"wipe_persistence_on_start": False, # 保留数据
|
||||
}
|
||||
```
|
||||
|
||||
**预期结果**:
|
||||
- Supplier: 从DB加载(带绑定关系) → 执行bind(验证状态) → 跳过create/update
|
||||
- 其他业务: 正常同步
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 1. 删除远程异常创建的数据
|
||||
|
||||
```bash
|
||||
# TODO: 通过API删除远程的74条测试数据
|
||||
```
|
||||
|
||||
### 2. 清空数据库重新测试
|
||||
|
||||
```bash
|
||||
rm _runtime/test_simple.db
|
||||
python scripts/legacy/run_full_sync_pipeline_simple.py
|
||||
```
|
||||
|
||||
### 3. 验证绑定统计
|
||||
|
||||
**期望输出**:
|
||||
```
|
||||
Local Summary:
|
||||
company: 39/39 bindings ← 全部绑定
|
||||
supplier: 60000+/65202 bindings ← 大量绑定
|
||||
contract: XX/34 bindings ← 通过supplier_id绑定
|
||||
|
||||
Remote Summary:
|
||||
company: 39/39 bindings
|
||||
supplier: 60000+/60000 bindings
|
||||
contract: XX/155 bindings (XX creates)
|
||||
```
|
||||
|
||||
### 4. 检查日志文件
|
||||
|
||||
```bash
|
||||
cat test_results/simple_pipeline_*.log | grep "Skipping"
|
||||
```
|
||||
|
||||
**期望**:
|
||||
- 第一次运行:**不应该有** "Skipping" 输出
|
||||
- 第二次运行:`⏭️ Skipping create/update/sync for supplier`
|
||||
|
||||
## 风险评估
|
||||
|
||||
### 影响范围
|
||||
- ✅ 修复了 supplier/company 绑定失败问题
|
||||
- ✅ 防止未绑定节点异常创建
|
||||
- ✅ 保持了两阶段工作流(首次API+绑定,后续持久化)
|
||||
- ⚠️ 可能影响其他使用 skip_sync 的场景(需要测试)
|
||||
|
||||
### 向后兼容性
|
||||
- ✅ 原始脚本 (run_full_sync_pipeline_api.py) 不受影响
|
||||
- ✅ 没有 skip_sync 属性的策略行为不变
|
||||
- ✅ 新增的 force_sync_types 是可选参数
|
||||
|
||||
## 待办事项
|
||||
|
||||
- [ ] 测试所有业务类型的绑定统计
|
||||
- [ ] 验证依赖关系链(project → contract → material → detail)
|
||||
- [ ] 检查 construction_task 为什么没绑定(0/32)
|
||||
- [ ] 测试第二次运行的持久化加载逻辑
|
||||
- [ ] 更新文档说明 skip_sync 的新语义
|
||||
@@ -0,0 +1,72 @@
|
||||
# 业务流程与依赖关系
|
||||
|
||||
本文档描述推送系统涉及的业务实体及其依赖关系,用于指导同步顺序编排。
|
||||
|
||||
---
|
||||
|
||||
## 业务实体分类
|
||||
|
||||
### 1. 基础数据(仅拉取)
|
||||
|
||||
- **用户(User)**:拉取集团管理员、区域公司管理员及特定用户信息,用于跨平台穿透。其他用户需手动创建。
|
||||
- **公司(Company)**:仅拉取,不推送。
|
||||
- **供应商(Supplier)**:仅拉取,不推送。
|
||||
|
||||
### 2. 项目及扩展数据
|
||||
|
||||
**项目(Project)**
|
||||
- **绑定方式**:必须手动绑定(根节点)
|
||||
- **同步操作**:允许更新和拉取
|
||||
- **依赖项**:公司、附件(Attachment)
|
||||
|
||||
**项目扩展业务**
|
||||
- 通过 `(project_id, key)` 或 `(project_id, code)` 自动绑定
|
||||
- 允许创建、更新、拉取
|
||||
- 依赖项目
|
||||
|
||||
### 3. 合同及子业务
|
||||
|
||||
**合同(Contract)**
|
||||
- **绑定方式**:通过业务键自动绑定(需与 ECP 平台数据交叉检验)
|
||||
- **同步操作**:允许拉取、创建、更新、删除
|
||||
- **依赖项**:项目
|
||||
|
||||
**合同子业务**(施工任务、物资、结算、力能)
|
||||
- **绑定方式**:通过 `(contract_id, code)` 自动绑定
|
||||
- **同步操作**:允许拉取、创建、更新、删除
|
||||
- **特殊规则**:首次绑定后,项目侧不会同步创建远程已有的数据
|
||||
- **依赖项**:合同
|
||||
|
||||
**合同子业务明细**(施工明细、物资明细、结算明细、力能明细)
|
||||
- **绑定方式**:通过 `(parent_id, date)` 自动绑定
|
||||
- **同步操作**:允许拉取、创建、更新、删除
|
||||
- **特殊规则**:首次绑定后,项目侧不会同步创建远程已有的数据
|
||||
- **依赖项**:对应的合同子业务
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系图
|
||||
|
||||
```
|
||||
Company (基础)
|
||||
↓
|
||||
Project (手动绑定)
|
||||
↓
|
||||
Contract (自动绑定)
|
||||
↓
|
||||
Contract Sub-Business (施工/物资/结算/力能)
|
||||
↓
|
||||
Details (明细)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 同步顺序原则
|
||||
|
||||
1. **依赖优先**:父节点必须先于子节点同步
|
||||
2. **绑定传递**:子节点的业务键构建依赖父节点的 ID 映射
|
||||
3. **故障隔离**:父节点同步失败时,子节点标记为 `DEPENDENCY_ERROR` 并跳过
|
||||
|
||||
详细状态定义参见:[sync_system_new/sync_system/STATE_DEFINITIONS.md](../sync_system_new/sync_system/STATE_DEFINITIONS.md)
|
||||
|
||||
详细架构设计参见:[sync_system_new/architecture_layers.md](../sync_system_new/architecture_layers.md)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,152 @@
|
||||
# 节点状态持久化与人工介入规范
|
||||
|
||||
> **代码实现**:`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 绑定需手工指定
|
||||
@@ -0,0 +1,112 @@
|
||||
# 架构分层设计:四层模型
|
||||
|
||||
本架构旨在将同步系统的**数据容器**、**同步逻辑**、**执行层**与**编排流程**完全解耦,提供极简且类型安全的开发体验。
|
||||
|
||||
---
|
||||
|
||||
## 1. 架构概览
|
||||
|
||||
系统分为四层,依赖关系严格从上到下:
|
||||
|
||||
1. **通用层 (Common)**: 定义纯数据结构(Data Container)和基础工具。
|
||||
2. **同步系统层 (Sync System)**: 定义同步行为接口(SyncStrategy)与业务规则。
|
||||
3. **数据源层 (DataSource)**: 实现数据的加载与持久化,属于盲目执行层。
|
||||
4. **编排层 (Pipeline)**: 应用层逻辑,硬编码同步顺序,协调 ID 映射与错误传播。
|
||||
|
||||
---
|
||||
|
||||
## 2. 通用层 (Layer 0: Common)
|
||||
|
||||
本层不包含业务逻辑,仅定义数据契约。
|
||||
|
||||
- **SyncNode[T]**: 核心状态机容器。
|
||||
- `origin_data`: 原始数据(对应 `data_id`)。
|
||||
- `data`: 计算后的最新目标数据。
|
||||
- `data_id`: 业务主键ID。加载时必定有值,CREATE 节点初始为空字符串 `""`(布尔值为 False,可通过校验)。
|
||||
- `action`: 同步意图 (NONE, CREATE, UPDATE, DELETE)。
|
||||
- `status`: 执行状态 (PENDING, IN_PROGRESS, SUCCESS, FAILED, SKIPPED, PRECONDITION_FAILED)。
|
||||
- 详见 [状态定义规范 § 5.A](./状态定义规范.md#5-执行同步状态转换-physical-sync-state-transitions)
|
||||
- `binding_status`: 绑定状态 (UNCHECKED, NORMAL, MISSING, DEPENDENCY_ERROR, ABNORMAL, WARNING)。
|
||||
- 详见 [状态定义规范 § 1](./状态定义规范.md#1-核心状态判定表-core-binding-matrix)
|
||||
- `error`: 异常详情。
|
||||
- **DataCollection**: 节点的集合,提供类型安全的查询和过滤方法。
|
||||
- **BindingManager**:
|
||||
- **ID 映射**: 维护 `local_node_id` <-> `remote_node_id` 的双向映射。
|
||||
- **绑定键**: 使用 `node_id` 作为绑定记录的键,**一致不变**。CREATE 成功后只更新节点的 `data_id`,绑定记录不变。
|
||||
- **持久化**: 将映射关系持久化到存储(如 JSON/数据库)。
|
||||
- **Enums**: `SyncAction`, `SyncStatus`, `BindingStatus` 等。
|
||||
|
||||
---
|
||||
|
||||
## 3. 同步系统层 (Layer 1: Sync System)
|
||||
|
||||
核心业务逻辑层。通过实现 `SyncStrategy` 接口来定义特定实体的同步逻辑。
|
||||
|
||||
- **SyncStrategy[T] 接口**:
|
||||
- `bind()`:
|
||||
- 职责:判定 `binding_status` (NORMAL/MISSING/ABNORMAL/WARNING/DEPENDENCY_ERROR)
|
||||
- 详见 [状态定义规范 § 1-3](./状态定义规范.md)
|
||||
- `create()`:
|
||||
- 职责:处理孤儿节点(MISSING),在对方侧创建新节点并设置 `action=CREATE`
|
||||
- **ID 替换**: 创建新节点时,复制数据并将依赖字段的本地 ID 替换为远程 ID
|
||||
- **内联检查**: 通过 `IDResolver.resolve_and_validate()` 验证依赖 ID 解析
|
||||
- 详见 [状态定义规范 § 4.C](./状态定义规范.md)
|
||||
- `update()`:
|
||||
- 职责:处理差异节点(NORMAL 且有 diff),设置 `action=UPDATE`
|
||||
- **ID 替换**: 准备更新数据时,替换数据中的依赖 ID
|
||||
- **内联检查**: 验证目标节点 data_id 完整性,失败则设置 `PRECONDITION_FAILED`
|
||||
- **FAILED 保护**: 自动跳过 `status=FAILED` 的节点
|
||||
- 详见 [状态定义规范 § 4.A](./状态定义规范.md)
|
||||
---
|
||||
|
||||
## 4. 数据源层 (Layer 2: DataSource)
|
||||
|
||||
作为各个后端的适配器,负责数据加载与持久化。
|
||||
|
||||
- **职责**:
|
||||
- `load_nodes(node_type)`: 从后端加载并返回 `SyncNode` 集合,自动添加到 `DataCollection`。
|
||||
- `save_nodes(nodes)`:
|
||||
- 遍历节点,仅处理 `status == PENDING` 且 `action != NONE` 的节点。
|
||||
- 根据 `action` 执行相应的 CREATE/UPDATE/DELETE 操作。
|
||||
- 执行成功后,更新节点的 `status` 和相关信息(如新生成的 remote_id)。
|
||||
|
||||
---
|
||||
|
||||
## 5. 编排层 (Layer 3: Pipeline)
|
||||
|
||||
最高层级,负责流程编排。
|
||||
|
||||
- **职责**:
|
||||
- **显式顺序**: 直接在代码中按顺序调用同步逻辑(例如:先同步项目,再同步合同)。
|
||||
- **故障传播**: 若前置实体某节点同步失败(`FAILED`),编排器在处理后续依赖节点时可将其标记为 `DEPENDENCY_ERROR`。
|
||||
- **生命周期管理**: 初始化 `BindingManager` 和 `DataCollection`,同步结束后调用持久化方法。
|
||||
|
||||
---
|
||||
|
||||
## 6. ID 映射与对齐:核心流程
|
||||
|
||||
为了解决本地数据引用本地 ID 与远程数据引用远程 ID 的冲突,系统采用以下映射逻辑:
|
||||
|
||||
### 绑定阶段 (Binding)
|
||||
- **时机**: `Strategy.bind`
|
||||
- **目标**: 在 `BindingManager` 中确定 local_id ↔ remote_id 映射
|
||||
- **手段**: 业务键匹配(如 Code 匹配)、历史记录恢复等
|
||||
|
||||
### 执行阶段 (Execution)
|
||||
- **时机**: `Strategy.create()` / `Strategy.update()` 阶段
|
||||
- **目标**: 在创建新节点或准备更新时,将依赖字段的本地 ID 替换为远程 ID
|
||||
- **手段**:
|
||||
- `create()`: 创建对方节点时,复制数据并替换所有依赖 ID
|
||||
- `update()`: 准备更新数据时,替换数据中的依赖 ID
|
||||
- **验证**: `_pre_commit_check` 在 create/update 最后检查,确保所有依赖节点都已有远程 ID
|
||||
|
||||
**注意**: ID 替换在 Strategy 的 create/update 阶段完成,DataSource 只负责盲目执行已准备好的数据。
|
||||
|
||||
---
|
||||
|
||||
## 7. 关键设计原则
|
||||
|
||||
- **删除类型依赖配置**: 依赖关系通过 `Pipeline` 中的代码调用顺序自然表达。
|
||||
- **无状态 DataSource**: 数据源不了解绑定逻辑,只看到 `action`、`status` 和 `binding_status`。
|
||||
- **BindingManager 为核心**: 负责 ID 空间映射和持久化。
|
||||
- **配置驱动**: 通过 `StrategyConfig` 和预设配置(ConfigPresets)灵活控制同步行为。
|
||||
@@ -0,0 +1,342 @@
|
||||
# 同步系统状态定义 (Final Spec)
|
||||
|
||||
本文档定义了同步系统的状态机逻辑。逻辑流转遵循以下顺序:
|
||||
1. **核心状态判定** (Core State)
|
||||
2. **依赖检查与ID解析** (Dependency)
|
||||
3. **自动绑定/发现** (Discovery)
|
||||
4. **策略执行** (Action)
|
||||
|
||||
---
|
||||
|
||||
## 0. 绑定状态枚举 (BindingStatus)
|
||||
|
||||
| 状态值 | 含义 | 来源 | 允许创建 |
|
||||
| :--- | :--- | :--- | :---: |
|
||||
| **UNCHECKED** | 未执行绑定检查 | 加载后的初始状态 | ❌ |
|
||||
| **NORMAL** | 绑定正常 | 核心状态判定 | ❌ |
|
||||
| **MISSING** | 无绑定记录(孤儿) | 核心状态判定 | ✅ |
|
||||
| **WARNING** | 业务键不完整 | 自动绑定阶段 | ❌ |
|
||||
| **ABNORMAL** | 数据损坏 | 核心状态判定 | ❌ |
|
||||
| **DEPENDENCY_ERROR** | 依赖未就绪 | 依赖检查或自动绑定 | ❌ |
|
||||
|
||||
**关键原则**:
|
||||
- **UNCHECKED 阻止创建**:加载后节点状态为 UNCHECKED,只有执行了 `bind()` 后才会转为其他状态
|
||||
- **只有 MISSING 状态才能触发 CREATE**:`create()` 方法只查找 `binding_status == MISSING` 的节点
|
||||
- **如果 bind() 被跳过**(如 skip_sync=True),节点保持 UNCHECKED,不会被创建
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心状态判定表 (Core Binding Matrix)
|
||||
|
||||
**说明**:
|
||||
* **Binding Record Pair**: 数据库中的绑定记录 (必须成对出现,已排除单边绑定)。
|
||||
* **Data Valid**: 指数据是否可读、Schema校验是否通过。
|
||||
* **Warning vs Abnormal**:
|
||||
* `ABNORMAL`: 自身数据损坏。
|
||||
* `WARNING`: 自身健康,但因为对方损坏导致**连接不可用**。
|
||||
|
||||
| Bind Pair (Record) | Local Data (Valid?) | Remote Data (Valid?) | -> | **Local Status** | **Remote Status** | **说明** |
|
||||
| :---: | :---: | :---: | :--- | :--- | :--- | :--- |
|
||||
| **YES** | **YES** | **YES** | -> | **NORMAL** | **NORMAL** | **稳态**。链路完整,数据健康,可进行 Diff/Update。 |
|
||||
| **YES** | **YES** | **NO** | -> | **WARNING** | **ABNORMAL** | **远程损坏**。本地虽好,但链路已断 (Link Degraded)。禁止同步。 |
|
||||
| **YES** | **NO** | **YES** | -> | **ABNORMAL** | **WARNING** | **本地损坏**。远程虽好,但链路已断。禁止同步。 |
|
||||
| **YES** | **NO** | **NO** | -> | **ABNORMAL** | **ABNORMAL** | **双向损坏**。 |
|
||||
| **NO** | **YES** | N/A | -> | **MISSING** | N/A | **本地孤儿**。进入“依赖与发现”流程。 |
|
||||
| **NO** | N/A | **YES** | -> | N/A | **MISSING** | **远程孤儿**。进入“依赖与发现”流程。 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 依赖与业务键解析 (Dependency & ID Resolution)
|
||||
|
||||
**适用范围**: 所有涉及外键依赖 (Foreign Key) 的节点。
|
||||
**前置条件**: 核心状态 = `MISSING` (准备进行自动绑定或新建)。
|
||||
|
||||
在进行 `Auto-Bind` 或 `Create` 之前,必须先尝试构建**远程业务键**。
|
||||
例如:本地 `Contract(proj_id="P1", code="C01")` 想找远程对象,必须先查询 `P1` 对应的远程 ID `RP_99`,转换成 `(RP_99, "C01")` 才能去远程搜索。
|
||||
|
||||
| 依赖项 (Dependency) | 依赖项绑定状态 | -> | **能否构建业务键** | **节点状态** | **说明** |
|
||||
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||
| **无依赖** | N/A | -> | ✅ Yes | **MISSING** | 根节点 (如 Project),直接进入下一阶段。 |
|
||||
| **有依赖** | **NORMAL** 且 `data_id` 非空(`status` 任意) | -> | ✅ Yes | **MISSING** | 依赖项已获得远程ID,可进入下一阶段。 |
|
||||
| **有依赖** | **NORMAL** 且 `data_id` 为空("",含 `status = FAILED`) | -> | ❌ No | **DEPENDENCY_ERROR** | 依赖项未回填ID,阻断绑定与同步。 |
|
||||
| **有依赖** | **MISSING / ABNORMAL** | -> | ❌ No | **DEPENDENCY_ERROR** | **依赖阻断**。父节点未同步或损坏,无法计算远程键,无法绑定,无法创建。 |
|
||||
| **有依赖** | **WARNING** | -> | ❌ No | **DEPENDENCY_ERROR** | 父节点状态不确定(如连接降级),禁止基于此建立新关系。 |
|
||||
|
||||
> **处理策略**: 处于 `DEPENDENCY_ERROR` 状态的节点,其动作强制为 **SKIP** (或 PENDING)。必须先修复父节点。
|
||||
>
|
||||
> **错误信息格式**: `DEPENDENCY_ERROR` 节点的 `error` 字段会包含详细的依赖失败信息:
|
||||
> - `dep_err: not_found: field_name(node_type)=value` - 依赖节点未找到
|
||||
> - `dep_err: field_name: status=FAILED` - 依赖节点状态为 FAILED
|
||||
> - 多个依赖失败时用分号分隔:`dep_err: not_found: project_id(project)=P1; contract_id: status=FAILED`
|
||||
|
||||
---
|
||||
|
||||
## 3. 自动绑定状态处理 (Auto-Binding State Handling)
|
||||
|
||||
此逻辑遵循“去噪后 1:1”原则:只有在排除掉所有健康同步对后,若剩余节点形成绝对的 1:1 关系,才允许自动绑定。
|
||||
|
||||
**配置说明与触发机制**:
|
||||
- **初始化场景**: 通常在系统第一次初始化或首次接入新实体时,配置 `auto_bind = True` 来对齐历史数据。
|
||||
- **后续同步**: 系统在日常同步中不建议开启全局自动绑定。自动绑定仅作为“发现”手段,一旦建立过关系的节点,其后续绑定操作通常仅在“自动创建”逻辑中伴随产生(详见第 4 节孤儿处理)。
|
||||
- **执行前提**: 只有当节点状态为 `MISSING` 且依赖解析成功(`DEPENDENCY_ERROR = False`)时,才会尝试以下探测逻辑。
|
||||
- **业务键完整性**: 若业务键字段缺失(如 `serial_number = null`),直接标记 `binding_status = WARNING`,并在 `node.error` 记录原因(`Auto-bind skipped: missing fields ...`),不参与自动绑定与孤儿创建判定。- **ID字段解析失败**: 若业务键包含ID字段(如 `supplier_id`)且依赖节点未绑定,ID解析失败,标记 `binding_status = DEPENDENCY_ERROR`,阻止孤儿创建。
|
||||
**核心算法**:
|
||||
1. **全集搜索**: 寻找本地/远端所有共享同一个业务键(Business Key)的记录。
|
||||
2. **排除稳态对**: 识别并移除**成对且互绑**的 `NORMAL` 记录。
|
||||
- 只有当本地 A 与远端 B 互为绑定对象,且状态皆为 `NORMAL` 时,才将这一对记录从待判定集合中剔除。
|
||||
- 若出现单边 `NORMAL` 记录(即其配对项不在本次搜索范围内或状态不属于另一端的 `NORMAL`),则不予剔除,视为环境污染项。
|
||||
3. **判定剩余**: 检查排除上述稳态对后的剩余集合(包含 `MISSING`, `ABNORMAL`, `WARNING` 及单边已占用的 `NORMAL` 记录)。
|
||||
|
||||
> **术语**:
|
||||
> - `L_remain` = 本地记录中,排除“成对稳态”后剩余的数量。
|
||||
> - `R_remain` = 远端记录中,排除“成对稳态”后剩余的数量。
|
||||
|
||||
| `L_remain` | `R_remain` | -> | **动作** | **说明** |
|
||||
| :---: | :---: | :--- | :--- | :--- |
|
||||
| **1** | **1** | -> | **BIND (Link)** | **1对1 自动对齐**:仅当两端剩余记录皆为 `MISSING` 且绝对 1:1 时,执行自动绑定。 |
|
||||
| **1** | **1** | -> | **SKIP** | **带障不处理**:若剩余记录中包含 `ABNORMAL` 等异常状态,说明存在历史冲突,不执行自动动作。 |
|
||||
| **N** | **0** | -> | **CREATE** | **可判定缺失**:业务键完整且唯一,本地存在而远端不存在(N:0)时,才允许进入孤儿创建策略。 |
|
||||
| 其他 | 其他 | -> | **SKIP** | **非 1:1 不处理**:存在多对多、多对一或无候选情况,系统保持原状。 |
|
||||
|
||||
**设计优势**:
|
||||
1. **对故障极端敏感**:环境中存在任何 `ABNORMAL` 记录都会阻断自动绑定,强制人工介入处理异常。
|
||||
2. **状态纯净**:失败时不产生任何中间态(如已废弃的 `AMBIGUOUS`),保持语义清晰。
|
||||
3. **静默自愈**: 当外界清理了环境且满足 1:1 条件后,系统下次同步会自动完成绑定。
|
||||
|
||||
### 3.1 空数据处理策略
|
||||
|
||||
**原则**:**空数据不推送**。
|
||||
|
||||
**空数据定义**:
|
||||
- 初始化时创建但未编辑的业务记录(如空合同、空施工任务)
|
||||
- 业务键字段为空或关键字段缺失的记录
|
||||
|
||||
**处理方式**:
|
||||
- 在自动绑定阶段,若业务键字段缺失,标记 `binding_status = WARNING`,记录原因
|
||||
- 不参与自动绑定与孤儿创建判定
|
||||
- 不推送到远程系统
|
||||
|
||||
**设计理由**:
|
||||
1. **远程系统简化**:远程只需处理完整数据,无需兼容空数据校验逻辑
|
||||
2. **避免垃圾数据**:空数据通常会被本地删除,若已推送则清理复杂
|
||||
3. **数据质量保障**:远程系统只能看到本地已正确编辑保存的业务
|
||||
|
||||
**配置方式**(Strategy 层):
|
||||
```python
|
||||
# 在 Strategy 配置中指定必填字段
|
||||
default_config = StrategyConfig(
|
||||
auto_bind_fields=["contract_id", "task_type"], # 业务键字段
|
||||
# 如果这些字段为空,节点将被标记为 WARNING 而非 MISSING
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 动作策略表 (Action Policy Matrix)
|
||||
|
||||
此表定义了在特定状态下,`execute` 阶段允许产生的 **意图行动 (`SyncAction`)**。
|
||||
|
||||
#### A. 稳态更新 (NORMAL)
|
||||
适用于已有绑定关系且链路健康的节点。
|
||||
|
||||
| 状态 | 动作条件 | 执行动作 (`SyncAction`) |
|
||||
| :--- | :--- | :--- |
|
||||
| **NORMAL** | `compute_diff` 判定存在差异 | **UPDATE** |
|
||||
| **NORMAL** | `compute_diff` 判定无差异 | **NONE** |
|
||||
|
||||
#### B. 异常处理 (阻断态)
|
||||
处于这些状态的节点,引擎会自动中止后续的读写操作。
|
||||
|
||||
| 状态 | 说明 | 动作 (`SyncAction`) |
|
||||
| :--- | :--- | :--- |
|
||||
| **DEPENDENCY_ERROR** | 父节点缺失/异常同步导致阻断 | **NONE** |
|
||||
| **ABNORMAL / WARNING** | 数据损坏或链路降级,需人工修复 | **NONE** |
|
||||
|
||||
#### C. 孤儿处理 (Create Only)
|
||||
适用于最终探测状态为 **MISSING** 且无法/不满足自动绑定的节点。
|
||||
|
||||
**本地孤儿 (Local Orphan)**: 本地有,远程无。
|
||||
| 策略配置 (`local_orphan_action`) | 对应的同步模式 | 动作 (`SyncAction`) |
|
||||
| :--- | :--- | :--- |
|
||||
| **"create_remote"** | Push (本地推远端) | **CREATE** |
|
||||
| **"delete_local"** | Pull (远端拉本地) | **报错 (未实现)** |
|
||||
| **"none"** | Any | **NONE** |
|
||||
|
||||
**远程孤儿 (Remote Orphan)**: 远程有,本地无。
|
||||
| 策略配置 (`remote_orphan_action`) | 对应的同步模式 | 动作 (`SyncAction`) |
|
||||
| :--- | :--- | :--- |
|
||||
| **"create_local"** | Pull (远端拉本地) | **CREATE** |
|
||||
| **"delete_remote"** | Push (本地推远端) | **报错 (未实现)** |
|
||||
| **"none"** | Any | **NONE** |
|
||||
|
||||
---
|
||||
|
||||
## 5. 执行同步状态转换 (Physical Sync State Transitions)
|
||||
|
||||
在策略执行完成、设置好 `action` 后,DataSource 层负责执行实际的同步操作并更新 `status` 字段。
|
||||
|
||||
### A. 执行状态枚举 (SyncStatus)
|
||||
|
||||
| 状态值 | 含义 | 适用场景 |
|
||||
| :--- | :--- | :--- |
|
||||
| **PENDING** | 待执行 | 初始状态、无动作的节点 |
|
||||
| **IN_PROGRESS** | 执行中 | 异步接口已提交,等待结果回调 |
|
||||
| **SUCCESS** | 执行成功 | 同步操作完成且成功 |
|
||||
| **FAILED** | 执行失败 | 同步操作失败,`error` 字段记录原因 |
|
||||
| **SKIPPED** | 已跳过 | 节点被主动跳过(如依赖未满足) |
|
||||
| **PRECONDITION_FAILED** | 前置条件失败 | create/update 内部验证发现问题 |
|
||||
|
||||
### B. 执行转换矩阵
|
||||
|
||||
#### 同步接口(一步式)
|
||||
|
||||
适用于同步 API(调用后立即返回成功/失败)。
|
||||
|
||||
| 输入: action | API 调用结果 | → | 输出: status | 副作用 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **CREATE** | 成功返回 (200/201) | → | **SUCCESS** | `data_id` 设置为远程返回的ID |
|
||||
| **CREATE** | 失败返回 (4xx/5xx) | → | **FAILED** | `error` 记录错误信息 |
|
||||
| **UPDATE** | 成功返回 (200) | → | **SUCCESS** | - |
|
||||
| **UPDATE** | 失败返回 (4xx/5xx) | → | **FAILED** | `error` 记录错误信息 |
|
||||
| **DELETE** | 成功返回 (200/204) | → | **SUCCESS** | 节点可能被移除 |
|
||||
| **DELETE** | 失败返回 (4xx/5xx) | → | **FAILED** | `error` 记录错误信息 |
|
||||
| **NONE** | - | → | **PENDING** | 不执行,保持原状 |
|
||||
|
||||
#### 异步接口(两步式)
|
||||
|
||||
适用于异步 API(提交后需要轮询或回调获取结果)。
|
||||
|
||||
| 输入: action | API 提交结果 | → | 输出: status | 后续操作 |
|
||||
| :--- | :--- | :--- | :--- | :--- |
|
||||
| **CREATE** | 提交成功 (202 Accepted) | → | **IN_PROGRESS** | 记录 task_id,等待轮询 |
|
||||
| **CREATE** | 提交失败 (4xx/5xx) | → | **FAILED** | `error` 记录错误 |
|
||||
| **IN_PROGRESS** | 轮询返回成功 | → | **SUCCESS** | `data_id` 设置为远程ID |
|
||||
| **IN_PROGRESS** | 轮询返回失败 | → | **FAILED** | `error` 记录错误 |
|
||||
| **IN_PROGRESS** | 超时未完成 | → | **FAILED** | `error` 记录 "Timeout" |
|
||||
|
||||
### B+. Handler 降级(action 取消)
|
||||
|
||||
> **代码实现**:各 `domain/*/api_handler.py` 中的 `update()` 方法。
|
||||
|
||||
当 Handler 在构建 API 请求数据时,发现实际无需更新的字段(如 schema 校验后所有字段与远程一致),
|
||||
Handler 会主动将节点的 `action` 从 `UPDATE` 降级为 `NONE`:
|
||||
|
||||
```
|
||||
action=UPDATE → Handler 校验 → 无实际变更字段 → action=NONE(不提交 API)
|
||||
```
|
||||
|
||||
此机制确保 DataSource 不会向远程发送无意义的空更新请求。
|
||||
|
||||
### C. 关键副作用
|
||||
|
||||
| 动作 | 成功时的副作用 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **CREATE** | 临时绑定已建立 | 绑定关系可在提交前创建,用于推送与追踪;是否成功不影响该绑定存在 |
|
||||
| **CREATE** | 设置 `data_id` | 成功后从API响应中提取远程分配的ID,更新到节点 |
|
||||
| **CREATE** | 回写 `data` 主键 | DataSource 将远程返回的ID写回 `SyncNode.data`(如 `id`/`*_id` 字段) |
|
||||
| **CREATE** | 更新绑定记录 | 若使用临时ID,成功后替换为真实的远程ID |
|
||||
| **UPDATE** | 更新 `origin_data` | 将当前 `data` 保存为新的 `origin_data` |
|
||||
| **DELETE** | 移除绑定记录 | 删除成功后,清除对应的绑定关系 |
|
||||
|
||||
### D. 错误处理
|
||||
|
||||
| 错误类型 | 记录方式 | 重试策略 |
|
||||
| :--- | :--- | :--- |
|
||||
| **网络错误** | `error = "Network timeout"` | 可重试 |
|
||||
| **权限错误** | `error = "403 Forbidden"` | 不可重试,需人工介入 |
|
||||
| **数据校验错误** | `error = "Validation failed: ..."` | 不可重试,需修复数据 |
|
||||
| **依赖缺失** | `status = PRECONDITION_FAILED` | 不执行,等待依赖修复 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 同步检查规范 (Sync Check Specification)
|
||||
|
||||
在执行同步前后,需要进行状态检查以确保操作的合法性和完整性。
|
||||
|
||||
### A. 内联前置检查(create/update 阶段)
|
||||
|
||||
> **实现说明**:原 `_pre_commit_check` 方法已移除(保留空壳兼容接口)。
|
||||
> 所有有效检查已内联到 `strategy_ops/create_ops.py::add_create_action()` 和 `strategy_ops/update_ops.py::add_update_action()` 中。
|
||||
|
||||
**create() 阶段内联检查**:
|
||||
|
||||
| 检查类别 | 检查条件 | 不通过时 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **数据完整性** | `src_node.get_data()` 不为空 | 跳过该节点,记录日志 | MISSING 但数据为空 |
|
||||
| **依赖 ID 解析** | `id_resolver.resolve_and_validate()` 成功 | `binding_status → DEPENDENCY_ERROR` | 依赖字段无法解析到目标系统 ID |
|
||||
|
||||
**update() 阶段内联检查**:
|
||||
|
||||
| 检查类别 | 检查条件 | 不通过时 | 说明 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **FAILED 保护** | `src_node.status != FAILED` | 跳过该节点 | 不覆盖上次失败的状态和错误 |
|
||||
| **数据完整性** | `src_node.get_data()` 不为空 | 跳过该节点 | NORMAL 但数据为空 |
|
||||
| **目标节点存在** | 通过绑定找到目标节点 | 跳过该节点 | 绑定记录指向的节点不在集合中 |
|
||||
| **data_id 完整性** | `target_node.data_id` 非空 | `status → PRECONDITION_FAILED` | UPDATE 必须有业务主键 |
|
||||
| **依赖 ID 解析** | `id_resolver.resolve_and_validate()` 成功 | `status → PRECONDITION_FAILED` | 依赖字段无法解析到目标系统 ID |
|
||||
| **差异检测** | `_needs_update()` 返回 True | 跳过该节点 | 无差异则不更新 |
|
||||
|
||||
> **注意**:以下检查项在当前实现中 **未覆盖**【未实现】:
|
||||
> - 状态合法性:`binding_status` 必须是已知状态
|
||||
> - 动作合法性:`CREATE/UPDATE` 只能用于 `NORMAL` 节点
|
||||
> - 绑定一致性:`NORMAL` 节点必须有绑定记录
|
||||
|
||||
### B. 同步后检查 (Post-Sync Check)
|
||||
|
||||
**目标**: 验证同步结果的完整性,发现残留问题。
|
||||
|
||||
> **实现说明**:当前 `_stage4_post_sync_check` 只打印摘要表格,不做异常检测。以下检查项均为【未实现】。
|
||||
|
||||
#### 检查项清单【未实现】
|
||||
|
||||
| 检查类别 | 检查条件 | 异常情况 | 严重程度 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| **CREATE 完成性** | CREATE 节点应该有 `data_id` | `action = CREATE, status = SUCCESS` 但 `data_id = ""` | 🔴 ERROR |
|
||||
| **CREATE 绑定** | CREATE 节点应该有绑定记录 | CREATE 成功但无绑定 | 🔴 ERROR |
|
||||
| **UPDATE 完成性** | UPDATE 节点应该成功 | `action = UPDATE` 但 `status = FAILED` | ⚠️ WARNING |
|
||||
| **绑定保持** | UPDATE 节点的绑定应该保持 | UPDATE 后绑定记录丢失 | 🔴 ERROR |
|
||||
| **残留异常** | 不应有 ABNORMAL 节点 | 同步后仍有 `binding_status = ABNORMAL` | ⚠️ WARNING |
|
||||
| **残留依赖错误** | 不应有 DEPENDENCY_ERROR | 同步后仍有依赖阻断 | ⚠️ WARNING |
|
||||
|
||||
#### 统计指标
|
||||
|
||||
| 指标 | 计算方式 | 用途 |
|
||||
| :--- | :--- | :--- |
|
||||
| **成功率** | `SUCCESS / (CREATE + UPDATE)` | 评估同步质量 |
|
||||
| **异常率** | `(ABNORMAL + WARNING) / total` | 发现数据质量问题 |
|
||||
| **阻断率** | `DEPENDENCY_ERROR / total` | 发现依赖关系问题 |
|
||||
|
||||
#### 决策规则
|
||||
|
||||
| 检查结果 | ERROR 数量 | 决策 |
|
||||
| :--- | :--- | :--- |
|
||||
| ✅ 通过 | 0 | **继续持久化**,正常结束 |
|
||||
| ⚠️ 有警告 | 0, WARNING ≥1 | **继续持久化**,打印警告报告 |
|
||||
| ❌ 有错误 | ≥1 | **继续持久化**(已同步无法回滚),打印错误报告 |
|
||||
|
||||
> **注意**: Post-Sync Check 即使有 ERROR 也不会终止流程,因为同步已经执行,无法回滚。检查目的是发现问题并记录,供后续修复。
|
||||
|
||||
---
|
||||
|
||||
## 附录: 状态字段说明
|
||||
|
||||
### SyncNode 核心字段
|
||||
|
||||
| 字段 | 类型 | 设置阶段 | 含义 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `binding_status` | BindingStatus | Bind 阶段 | 绑定健康状态 |
|
||||
| `action` | SyncAction | Create/Update 阶段 | 待执行动作 |
|
||||
| `status` | SyncStatus | Physical Sync 阶段 | 执行结果状态 |
|
||||
| `data_id` | str | 数据加载 / CREATE 后 | 业务主键ID(CREATE 前为 "") |
|
||||
| `depend_ids` | List[str] | 数据加载 | 依赖节点的 node_id 列表 |
|
||||
| `error` | str \| None | Physical Sync 阶段 | 错误信息(失败时) |
|
||||
|
||||
### 状态流转时序
|
||||
|
||||
```
|
||||
[数据加载] [Bind] [Create/Update] [Physical Sync]
|
||||
binding_status: UNCHECKED → NORMAL/MISSING/... → (保持) → (保持)
|
||||
action: NONE → NONE → CREATE/UPDATE → (保持)
|
||||
status: PENDING → PENDING → PENDING → SUCCESS/FAILED
|
||||
data_id: 从源加载 → (保持) → 可能为 "" → CREATE 后设置
|
||||
```
|
||||
@@ -0,0 +1,498 @@
|
||||
# 状态机形式化规范
|
||||
|
||||
本文档用 **Guard-Event-Action (GEA)** 形式严格定义节点状态机。
|
||||
所有转换以代码实现为准,模糊之处在文档中显式标注。
|
||||
|
||||
> **代码基线**
|
||||
> - 枚举: `sync_system_new/common/types.py`
|
||||
> - 决策引擎: `sync_system_new/sync_system/decision.py`
|
||||
> - 重置策略: `sync_system_new/common/state_reset.py`
|
||||
> - 同步策略: `sync_system_new/sync_system/strategy.py`
|
||||
> - 物理同步: `sync_system_new/datasource/datasource.py`
|
||||
|
||||
> **配置基线(状态机真源)**
|
||||
> - `sync_system_new/state_machine/node_state_machine.yaml`
|
||||
|
||||
### 2026-02 对齐说明(重构前必读)
|
||||
|
||||
以下语义以 `node_state_machine.yaml` 为准,用于修正文档中易混淆点:
|
||||
|
||||
1. **E12a 为什么从 S01 出发?**
|
||||
- `S01` 只约束**源节点**是 NORMAL/NONE/PENDING 且有 `data_id`。
|
||||
- `E12a` 的 guard 是 `target_has_data_id=false`,检查对象是**目标节点**。
|
||||
- 所以“源节点在 S01”与“E12a 触发前置失败”并不冲突。
|
||||
|
||||
2. **S01 会不会触发创建?**
|
||||
- 当前配置中,create 的入口状态是 `S02(MISSING)`,不是 `S01`。
|
||||
- `S01` 进入的是 update_prepare(E12/E13/E14)。
|
||||
|
||||
3. **N:0 + create_enabled=true 的当前行为**
|
||||
- 自动绑定分支 `E08f`:`S02 -> S01`,并 `emit spawn_target_node_state: S06`。
|
||||
- create_prepare 分支 `E09b` 也可在检查成功后 `S02 -> S01 + spawn S06`。
|
||||
- 即:源节点不会停在“等待创建结果”的专用状态;创建执行结果体现在目标创建节点链路 `S06 -> S10/S11/S12/S13`。
|
||||
|
||||
4. **旧章节中的事件编号偏差**
|
||||
- 本文后续仍有部分“E11a/E11b/…”旧编号描述,已与当前 YAML 不一致。
|
||||
- 进行代码重构时,优先依据 YAML 的 `E08/E09/E10/E12/E13/E14/E15..E20`。
|
||||
|
||||
---
|
||||
|
||||
## §1 状态空间定义
|
||||
|
||||
### §1.1 状态维度
|
||||
|
||||
节点携带 **三个独立的状态字段** 和 **两个辅助字段**:
|
||||
|
||||
| 字段 | 枚举 | 值域 | 语义 |
|
||||
|------|------|------|------|
|
||||
| **B** – `binding_status` | `BindingStatus` | `UNCHECKED` · `NORMAL` · `MISSING` · `ABNORMAL` · `WARNING` · `DEPENDENCY_ERROR` | 绑定健康度 |
|
||||
| **A** – `action` | `SyncAction` | `NONE` · `CREATE` · `UPDATE` · `DELETE` | 待执行动作 |
|
||||
| **S** – `status` | `SyncStatus` | `PENDING` · `IN_PROGRESS` · `SUCCESS` · `FAILED` · `SKIPPED` · `PRECONDITION_FAILED` | 执行结果 |
|
||||
|
||||
辅助字段(影响 Guard 但不是状态维度):
|
||||
|
||||
| 字段 | 类型 | 语义 |
|
||||
|------|------|------|
|
||||
| `data_id` | `str` | 业务主键(空串 `""` = 尚未从远程获取) |
|
||||
| `error` | `str \| None` | 最近一次错误信息 |
|
||||
|
||||
理论组合 = 6 × 4 × 6 = **144**,实际可达 ≈ **23** 种。
|
||||
|
||||
### §1.2 可达状态枚举
|
||||
|
||||
以下列出所有在正常流程中可以到达的 `(binding_status, action, status)` 三元组。
|
||||
|
||||
| # | binding_status | action | status | 语义 | 产生阶段 |
|
||||
|---|---|---|---|------|---------|
|
||||
| R1 | UNCHECKED | NONE | PENDING | 首次加载 | 数据加载 |
|
||||
| R2 | UNCHECKED | NONE | SUCCESS | 从持久化恢复(上轮成功)后重置 | 阶段1/2重置 |
|
||||
| R3 | UNCHECKED | NONE | FAILED | 从持久化恢复(上轮失败)后重置 | 阶段1/2重置 |
|
||||
| R4 | UNCHECKED | NONE | SKIPPED | 从持久化恢复(上轮跳过)后重置 | 阶段1/2重置 |
|
||||
| R5 | UNCHECKED | NONE | PRECONDITION_FAILED | 从持久化恢复(上轮前置失败)后重置 | 阶段1/2重置 |
|
||||
| R6 | NORMAL | NONE | PENDING | 绑定正常,无需操作(首次) | bind.p1 |
|
||||
| R7 | NORMAL | NONE | SUCCESS | 绑定正常,无需操作(历史成功) | bind.p1 |
|
||||
| R8 | NORMAL | NONE | FAILED | 绑定正常但上次失败,被 FAILED 保护 | bind.p1 |
|
||||
| R9 | MISSING | NONE | PENDING | 孤儿,等待自动绑定或创建(首次) | bind.p1 |
|
||||
| R10 | MISSING | NONE | SUCCESS | 孤儿(历史成功但绑定丢失?) | bind.p1 |
|
||||
| R11 | ABNORMAL | NONE | * | 数据损坏,需人工 | bind.p1 |
|
||||
| R12 | WARNING | NONE | * | 多候选/对方损坏,需人工 | bind.p1/p3 |
|
||||
| R13 | DEPENDENCY_ERROR | NONE | PENDING | 依赖不满足 | bind.p2/p3/create |
|
||||
| R14 | NORMAL | CREATE | PENDING | 准备创建(目标节点) | create |
|
||||
| R15 | NORMAL | CREATE | IN_PROGRESS | 异步创建进行中 | sync |
|
||||
| R16 | NORMAL | CREATE | SUCCESS | 创建成功 | sync |
|
||||
| R17 | NORMAL | CREATE | FAILED | 创建失败 | sync |
|
||||
| R18 | NORMAL | UPDATE | PENDING | 准备更新(目标节点) | update |
|
||||
| R19 | NORMAL | UPDATE | IN_PROGRESS | 异步更新进行中 | sync |
|
||||
| R20 | NORMAL | UPDATE | SUCCESS | 更新成功 | sync |
|
||||
| R21 | NORMAL | UPDATE | FAILED | 更新失败 | sync |
|
||||
| R22 | NORMAL | UPDATE | PRECONDITION_FAILED | 更新前置条件失败 | update |
|
||||
| R23 | NORMAL | CREATE | SKIPPED | 创建被 Handler 跳过 | sync |
|
||||
|
||||
> **不可达组合(示例)**:
|
||||
> - `(MISSING, UPDATE, *)` — MISSING 节点不可能被安排 UPDATE
|
||||
> - `(ABNORMAL, CREATE, *)` — ABNORMAL 节点不可能被安排 CREATE
|
||||
> - `(*, DELETE, *)` — DELETE 尚未实现
|
||||
> - `(UNCHECKED, CREATE, *)` — UNCHECKED 是瞬态,在 bind 之前不会有 CREATE
|
||||
> - `(DEPENDENCY_ERROR, UPDATE, *)` — DEPENDENCY_ERROR 节点不可能进入 update 流程
|
||||
|
||||
### §1.3 辅助字段约束
|
||||
|
||||
| 对应状态 | binding_status | action | status | data_id 约束 | error 约束 |
|
||||
|---------|---|---|---|-------------|-----------|
|
||||
| R14 | NORMAL | CREATE | PENDING | `""` (待远程返回) | None |
|
||||
| R16 | NORMAL | CREATE | SUCCESS | 非空 (远程回填) | None |
|
||||
| R17 | NORMAL | CREATE | FAILED | `""` (未回填) | 非空 |
|
||||
| R22 | NORMAL | UPDATE | PRECONDITION_FAILED | * | 非空 |
|
||||
| R13 | DEPENDENCY_ERROR | NONE | PENDING | * | 非空 |
|
||||
|
||||
---
|
||||
|
||||
## §2 不变量 (Invariants)
|
||||
|
||||
以下约束在**任何事件执行后**都必须成立。违反即为 Bug。
|
||||
|
||||
```
|
||||
INV-1 B = UNCHECKED ⟹ A = NONE
|
||||
(UNCHECKED 是瞬态,在 bind 之前不允许有动作)
|
||||
|
||||
INV-2 B ∈ {ABNORMAL, WARNING, DEPENDENCY_ERROR} ⟹ A = NONE
|
||||
(阻断态节点不允许执行任何同步动作)
|
||||
|
||||
INV-3 B = MISSING ⟹ A ∈ {NONE, CREATE}
|
||||
(MISSING 节点只能等待创建或保持不动)
|
||||
|
||||
INV-4 A = CREATE ⟹ B = NORMAL
|
||||
(CREATE 动作只出现在目标节点上,其 binding_status 已被设为 NORMAL)
|
||||
|
||||
INV-5 A = UPDATE ⟹ B = NORMAL
|
||||
(UPDATE 动作只对 NORMAL 节点执行)
|
||||
|
||||
INV-6 S = IN_PROGRESS ⟹ A ∈ {CREATE, UPDATE, DELETE}
|
||||
(只有提交过 API 的节点才可能处于 IN_PROGRESS)
|
||||
|
||||
INV-7 S = PRECONDITION_FAILED ⟹ A = UPDATE
|
||||
(PRECONDITION_FAILED 只在 update 阶段产生)
|
||||
|
||||
INV-8 A = CREATE ∧ S = SUCCESS ⟹ data_id ≠ ""
|
||||
(创建成功必须回填 data_id)
|
||||
|
||||
INV-9 S = FAILED ⟹ error ≠ None
|
||||
(失败必须有错误信息)
|
||||
|
||||
INV-10 B = DEPENDENCY_ERROR ⟹ error ≠ None
|
||||
(依赖阻断必须有错误信息)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §3 事件与转换
|
||||
|
||||
### §3.1 事件总览图
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 节点生命周期事件流 │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
|
||||
持久化恢复 策略层 (Strategy) 执行层 (DataSource)
|
||||
═══════════ ══════════════════════════════════ ══════════════════════════
|
||||
┌──────────────────────────────┐
|
||||
┌──────┐ │ bind() │ ┌──────────────────────┐
|
||||
│E01 │──────────▶│ E04 ─┐ │ │ sync_all() │
|
||||
│RESET1│ │ E05 ├─ phase1: core_state │ │ │
|
||||
└──┬───┘ │ E06 ─┘ │ │ E12 SYNC_OK │
|
||||
│ │ E07 ── phase2: dep_check │ ┌───▶│ E13 SYNC_FAIL │
|
||||
┌──▼───┐ │ E08 ─┐ │ │ │ E14 SYNC_ASYNC │
|
||||
│E02 │ │ E09 ├─ phase3: auto_bind │ │ │ E15 SYNC_TIMEOUT │
|
||||
│ZOMBIE│ │ E10 ─┘ │ │ │ E16 HANDLER_SKIP │
|
||||
└──┬───┘ └──────────────────────────────┘ │ │ E17 HANDLER_DOWN │
|
||||
│ ┌──────────────────────────────┐ │ └──────────────────────┘
|
||||
┌──▼───┐ │ create() / update() │ │
|
||||
│E03 │──────────▶│ E11a CREATE_OK │────┘
|
||||
│RESET2│ │ E11b CREATE_DEP_FAIL │
|
||||
└──────┘ │ E11c UPDATE_DIFF │
|
||||
│ E11d UPDATE_PRECOND_FAIL │
|
||||
│ E11e UPDATE_SKIP_FAILED │
|
||||
└──────────────────────────────┘
|
||||
```
|
||||
|
||||
|
||||
### §3.2 状态转换详解
|
||||
|
||||
本节按 Pipeline 执行阶段组织。每个阶段包含:
|
||||
1. **状态转换图**:状态(R 编号)为节点,事件(E 编号)为有向边
|
||||
2. **转换表**:每个事件的精确规格——起始状态、判断条件、结束状态、副作用
|
||||
|
||||
---
|
||||
|
||||
#### 🔄 阶段 0:重置
|
||||
|
||||
**状态转换图:**
|
||||
|
||||
```
|
||||
╔═══════════════════════════════════════╗
|
||||
║ 任意持久化终态 (R6 ~ R23) ║
|
||||
╚═══════════════════╤═══════════════════╝
|
||||
│ E01
|
||||
▼
|
||||
┌────────────────────────────────────────┐ E02
|
||||
│ 瞬态: UNCHECKED │────────────▶ [节点删除 + 解除绑定]
|
||||
│ (action 保留, status 保留, error=None) │
|
||||
└────────────────────┬──────────────────┘
|
||||
│ E03 (存活节点)
|
||||
▼
|
||||
╔══════════════════════════════════════════════════════════════╗
|
||||
║ [R1] UNCHECKED, NONE, PENDING ← 首次加载 ║
|
||||
║ [R2] UNCHECKED, NONE, SUCCESS ← 上轮成功 ║
|
||||
║ [R3] UNCHECKED, NONE, FAILED ← 上轮失败 ║
|
||||
║ [R4] UNCHECKED, NONE, SKIPPED ← 上轮跳过 ║
|
||||
║ [R5] UNCHECKED, NONE, PRECONDITION_FAILED ← 上轮前置 ║
|
||||
╚══════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
**转换表:**
|
||||
|
||||
| 事件 | 起始状态 | 判断条件 (Guard) | 结束状态 | 副作用 |
|
||||
|------|---------|-----------------|---------|--------|
|
||||
| **E01**: PHASE1_RESET | R6~R23 (任意持久化终态) | 节点从持久化加载 | 瞬态 (UNCHECKED, action 保留, status 保留) | error → None; data/origin_data → None |
|
||||
| **E02**: ZOMBIE_KILL | 瞬态 | action = CREATE ∧ (status = FAILED ∨ data_id = "") | **节点删除** | 解除绑定关系; 对 local/remote 两侧都扫描 |
|
||||
| **E03**: ACTION_RESET | 瞬态 (存活节点) | E02 完成后所有存活节点 | R1 / R2 / R3 / R4 / R5 (按原 status 分) | action → NONE |
|
||||
|
||||
> **代码**: `NodeStateReset.get_phase1_reset_defaults()` · `cleanup_create_failed_zombies()` · `reset_action_for_nodes()`
|
||||
|
||||
---
|
||||
|
||||
#### 🔗 阶段 1:Bind
|
||||
|
||||
Bind 由三个子阶段串行执行。后一阶段只处理前一阶段的遗留 MISSING 节点。
|
||||
|
||||
**状态转换图:**
|
||||
|
||||
```
|
||||
[R1~R5] UNCHECKED, NONE, *
|
||||
│
|
||||
│ ══════════ Phase 1: 核心状态判定 ══════════
|
||||
│
|
||||
├── E04 (有绑定, 双方数据 VALID) ───────────▶ [R6/R7/R8] NORMAL ──────▶ 出口 ✅
|
||||
├── E04 (有绑定, 自身数据缺失/损坏) ────────▶ [R11] ABNORMAL ─────────▶ 出口 🔒
|
||||
├── E04 (有绑定, 对方数据缺失/损坏) ────────▶ [R12] WARNING ──────────▶ 出口 🔒
|
||||
│
|
||||
└── E05 (无绑定记录) ───────────────────────▶ [R9] MISSING
|
||||
│
|
||||
══════════ Phase 2: 依赖检查 ══════════ │
|
||||
│
|
||||
├── E06 (data = None) ──────────────────────▶ [R11] ABNORMAL ──────▶ 出口 🔒
|
||||
├── E07 (依赖节点不满足) ───────────────────▶ [R13] DEP_ERROR ─────▶ 出口 🔒
|
||||
│ (无依赖配置 或 依赖全部满足 → 继续 ▼)
|
||||
│
|
||||
══════════ Phase 3: 自动绑定 ══════════
|
||||
│
|
||||
├── E08 (auto_bind 关闭) ──────────────────▶ [R12] WARNING ────────▶ 出口 🔒
|
||||
├── E09 (去噪后 1:1 匹配成功) ─────────────▶ [R6] NORMAL ─────────▶ 出口 ✅
|
||||
├── E10a (去噪后非 1:1, 多候选) ───────────▶ [R12] WARNING ────────▶ 出口 🔒
|
||||
├── E10b (业务键字段缺失) ─────────────────▶ [R12] WARNING ────────▶ 出口 🔒
|
||||
├── E10c (业务键中 ID 字段解析失败) ───────▶ [R13] DEP_ERROR ─────▶ 出口 🔒
|
||||
│
|
||||
└── (1:0 无远程匹配)
|
||||
├── create_enabled = True → [R9] MISSING ──────────────▶ 进入 create
|
||||
└── create_enabled = False → [R12] WARNING ─────────────▶ 出口 🔒
|
||||
```
|
||||
|
||||
**转换表:**
|
||||
|
||||
| 事件 | 起始状态 | 判断条件 (Guard) | 结束状态 | 副作用 |
|
||||
|------|---------|-----------------|---------|--------|
|
||||
| **E04**: BIND_PAIRED | R1~R5 | 有绑定记录 ∧ 双方数据 VALID | R6 (PENDING) / R7 (SUCCESS) / R8 (FAILED) — status 保持 | 远程节点同步设置 binding_status |
|
||||
| **E04**: BIND_PAIRED (自身异常) | R1~R5 | 有绑定记录 ∧ 本方数据缺失或损坏 | R11 (ABNORMAL) | 对方节点设为 WARNING |
|
||||
| **E04**: BIND_PAIRED (对方异常) | R1~R5 | 有绑定记录 ∧ 对方数据缺失或损坏 | R12 (WARNING) | 对方节点设为 ABNORMAL |
|
||||
| **E05**: BIND_ORPHAN | R1~R5 | 无绑定记录 | R9 (MISSING, NONE, status 保持) | — |
|
||||
| **E06**: DATA_NULL | R9 | binding_status = MISSING ∧ data = None | R11 (ABNORMAL) | error = "数据损坏: 节点数据为空或不存在" |
|
||||
| **E07**: DEP_CHECK_FAIL | R9 | depend_fields 配置非空 ∧ ∃依赖节点: 不存在 ∨ status ≠ NORMAL ∨ data_id 为空 | R13 (DEPENDENCY_ERROR) | error = "依赖项不满足: ..."; depend_ids 填充 |
|
||||
| **E08**: AUTOBIND_DISABLED | R9 | auto_bind = False 或 auto_bind_fields 为空 | R12 (WARNING) | — |
|
||||
| **E09**: AUTOBIND_OK | R9 | 去噪后 local:remote = 1:1 ∧ 双方都是 MISSING | R6 (NORMAL) | 建立绑定记录; 双方都设为 NORMAL |
|
||||
| **E10a**: AUTOBIND_AMBIGUOUS | R9 | 去噪后非 1:1 (多候选) | R12 (WARNING) | error += "存在多个候选节点" |
|
||||
| **E10b**: AUTOBIND_KEY_MISSING | R9 | auto_bind_fields 中有字段不存在于 data | R12 (WARNING) | error += "缺少业务键字段: ..." |
|
||||
| **E10c**: AUTOBIND_ID_FAIL | R9 | 业务键包含 ID 字段 ∧ ID 解析失败 | R13 (DEPENDENCY_ERROR) | error = "业务键中的ID字段解析失败: ..." |
|
||||
| **E10d**: ORPHAN_CREATE_DISABLED | R9 | 去噪后 1:0 ∧ create_enabled = False | R12 (WARNING) | error += "孤儿且不允许自动创建" |
|
||||
|
||||
> **E04 核心状态矩阵** (完整查表):
|
||||
>
|
||||
> | | 远程: VALID | 远程: ABSENT | 远程: INVALID |
|
||||
> |---|---|---|---|
|
||||
> | **本地: VALID** | 本:NORMAL 远:NORMAL | 本:WARNING 远:ABNORMAL | 本:WARNING 远:ABNORMAL |
|
||||
> | **本地: ABSENT** | 本:ABNORMAL 远:WARNING | 本:ABNORMAL 远:ABNORMAL | — |
|
||||
> | **本地: INVALID** | 本:ABNORMAL 远:WARNING | — | 本:ABNORMAL 远:ABNORMAL |
|
||||
>
|
||||
> 不在矩阵中的组合 → 默认 (ABNORMAL, ABNORMAL)
|
||||
|
||||
> **代码**: `SyncDecisionEngine.determine_core_status()` · `strategy_ops/bind_ops.py::phase_core_state()` · `strategy_ops/bind_ops.py::check_dependencies_for_nodes()` · `strategy_ops/bind_ops.py::phase_auto_binding()`
|
||||
|
||||
---
|
||||
|
||||
#### ⚡ 阶段 2:Create / Update
|
||||
|
||||
**状态转换图:**
|
||||
|
||||
```
|
||||
═══════════════ Create ═══════════════
|
||||
|
||||
[R9] MISSING, NONE, PENDING (源节点, data ≠ None)
|
||||
│
|
||||
├── E11a (ID 解析成功) ────▶ 源节点: [R9] → [R6] NORMAL
|
||||
│ 新建目标节点: → [R14] NORMAL, CREATE, PENDING
|
||||
│ + 建立绑定记录, 目标 data_id = ""
|
||||
│
|
||||
└── E11b (ID 解析失败) ────▶ 源节点: [R9] → [R13] DEPENDENCY_ERROR
|
||||
|
||||
|
||||
═══════════════ Update ═══════════════
|
||||
|
||||
[R6/R7] NORMAL, NONE, PENDING/SUCCESS (源节点, status ≠ FAILED)
|
||||
│
|
||||
├── E11c (差异检测通过) ────▶ 目标节点 → [R18] NORMAL, UPDATE, PENDING
|
||||
├── E11d.1 (目标 data_id="") ▶ 目标节点 → [R22] status = PRECONDITION_FAILED
|
||||
├── E11d.2 (ID 解析失败) ───▶ 目标节点 → [R22] action = UPDATE, status = PRECOND_FAIL
|
||||
└── (无差异) ──────────────▶ 保持不变
|
||||
|
||||
|
||||
═══════════════ FAILED 保护 ═══════════════
|
||||
|
||||
[R8] NORMAL, NONE, FAILED (源节点)
|
||||
│
|
||||
└── E11e ──────────────────▶ 跳过,保持 [R8] 不变
|
||||
```
|
||||
|
||||
**转换表:**
|
||||
|
||||
| 事件 | 起始状态 (源节点) | 判断条件 (Guard) | 结束状态 | 副作用 |
|
||||
|------|---------|-----------------|---------|--------|
|
||||
| **E11a**: CREATE_OK | R9 (MISSING) | data ≠ None ∧ IDResolver.resolve_and_validate() = OK | 源 → R6 (NORMAL); **新建**目标 → R14 | 建立绑定; 新节点 data = 已替换 ID 的副本, data_id = "" |
|
||||
| **E11b**: CREATE_DEP_FAIL | R9 (MISSING) | data ≠ None ∧ IDResolver.resolve_and_validate() = FAIL | R13 (DEPENDENCY_ERROR) | error = "创建时依赖 ID 未解析: ..." |
|
||||
| **E11c**: UPDATE_DIFF | R6/R7 (NORMAL, status≠FAILED) | 目标存在 ∧ 目标 data_id ≠ "" ∧ ID 解析 OK ∧ _needs_update() = True | 目标 → R18 (UPDATE, PENDING) | 目标: data 替换, error = None |
|
||||
| **E11d**: UPDATE_PRECOND_FAIL | R6/R7 (NORMAL, status≠FAILED) | 目标 data_id = "" (d1) 或 ID 解析失败 (d2) | 目标 → R22 (UPDATE, PRECONDITION_FAILED) | error = "目标缺少 data_id" 或 "依赖 ID 未解析" |
|
||||
| **E11e**: UPDATE_SKIP_FAILED | R8 (NORMAL, FAILED) | status = FAILED | R8 不变 | 保护上次的 error 和 FAILED 状态,不做任何修改 |
|
||||
|
||||
> **代码**: `strategy_ops/create_ops.py::add_create_action()` · `strategy_ops/update_ops.py::add_update_action()`
|
||||
|
||||
---
|
||||
|
||||
#### 📡 阶段 3:Sync(物理同步)
|
||||
|
||||
**状态转换图:**
|
||||
|
||||
```
|
||||
[R14] NORMAL, CREATE, PENDING [R18] NORMAL, UPDATE, PENDING
|
||||
│ │
|
||||
└───────────────┬───────────────────┘
|
||||
│
|
||||
├── E12 (API 成功) ──────▶ [R16] CREATE,SUCCESS / [R20] UPDATE,SUCCESS
|
||||
├── E13 (API 失败) ──────▶ [R17] CREATE,FAILED / [R21] UPDATE,FAILED
|
||||
├── E16 (Handler 跳过) ──▶ [R23] CREATE, SKIPPED
|
||||
├── E17 (Handler 降级) ──▶ [R6] NORMAL, NONE, PENDING (仅 UPDATE)
|
||||
│
|
||||
└── E14 (异步提交) ──────▶ [R15] CREATE,IN_PROGRESS / [R19] UPDATE,INP
|
||||
│
|
||||
├── (轮询成功) ──▶ [R16] / [R20] SUCCESS
|
||||
└── E15 (超时) ──▶ [R17] / [R21] FAILED
|
||||
```
|
||||
|
||||
**转换表:**
|
||||
|
||||
| 事件 | 起始状态 | 判断条件 (Guard) | 结束状态 | 副作用 |
|
||||
|------|---------|-----------------|---------|--------|
|
||||
| **E12**: SYNC_OK | R14 或 R18 | Handler API 返回 SUCCESS | R16 (CREATE) 或 R20 (UPDATE) | error → None; data_id 回填 (CREATE); origin_data 更新 (UPDATE) |
|
||||
| **E13**: SYNC_FAIL | R14 或 R18 | Handler API 返回 FAILED | R17 (CREATE) 或 R21 (UPDATE) | error = Handler 返回的错误信息 |
|
||||
| **E14**: SYNC_ASYNC | R14 或 R18 | Handler API 返回 IN_PROGRESS | R15 (CREATE) 或 R19 (UPDATE) | 记录 task_id, 进入轮询队列 |
|
||||
| **E15**: SYNC_TIMEOUT | R15 或 R19 | 轮询次数 > max_retries | R17 (CREATE) 或 R21 (UPDATE) | error = "Async task timeout after N seconds" |
|
||||
| **E16**: HANDLER_SKIP | R14 或 R18 | Handler API 返回 SKIPPED | R23 (CREATE,SKIPPED) | error → None |
|
||||
| **E17**: HANDLER_DOWNGRADE | R18 (仅 UPDATE) | Handler 运行时发现无实际变更字段 | R6 (NORMAL, NONE, PENDING) | action: UPDATE → NONE; 不提交 API; 统计列从 update 移入 none |
|
||||
|
||||
> **代码**: `BaseDataSource._handle_task_result()` · `_poll_async_tasks()` · `domain/*/api_handler.py`
|
||||
|
||||
---
|
||||
|
||||
## §4 终态分类与死胡同
|
||||
|
||||
### §4.1 终态分类
|
||||
|
||||
| 分类 | 状态 | (binding_status, action, status) | 语义 | 下一轮行为 |
|
||||
|------|------|------|------|---------|
|
||||
| ✅ 成功 | R16 | (NORMAL, CREATE, SUCCESS) | 创建成功 | →R2→bind→R7→update 检查 |
|
||||
| ✅ 成功 | R20 | (NORMAL, UPDATE, SUCCESS) | 更新成功 | →R2→bind→R7→update 检查 |
|
||||
| ❌ 失败 | R17 | (NORMAL, CREATE, FAILED) | 创建失败 | E02 zombie 判定→删除节点→对方回到 R9 重建 |
|
||||
| ❌ 失败 | R21 | (NORMAL, UPDATE, FAILED) | 更新失败 | →R3→bind→R8→FAILED 保护(E11e)→保持 |
|
||||
| 🔒 阻断 | R11 | (ABNORMAL, NONE, \*) | 数据损坏 | 每轮重新 bind 检查; 需人工修复数据 |
|
||||
| 🔒 阻断 | R12 | (WARNING, NONE, \*) | 多候选/对方异常 | 每轮重新 bind 检查; 需人工绑定或修复 |
|
||||
| 🔒 阻断 | R13 | (DEPENDENCY_ERROR, NONE, PENDING) | 依赖不满足 | 每轮重新检查依赖; 依赖修复后自动恢复 |
|
||||
| ⏭ 跳过 | R23 | (NORMAL, CREATE, SKIPPED) | Handler 跳过创建 | →R4→bind→R6→update 检查(无绑定则再 create) |
|
||||
| 🔄 异步 | R15 | (NORMAL, CREATE, IN\_PROGRESS) | 创建异步中 | 轮询直到 SUCCESS 或超时→FAILED |
|
||||
| 🔄 异步 | R19 | (NORMAL, UPDATE, IN\_PROGRESS) | 更新异步中 | 轮询直到 SUCCESS 或超时→FAILED |
|
||||
| 🔄 前置失败 | R22 | (NORMAL, UPDATE, PRECONDITION\_FAILED) | 前置条件不满足 | →R5→bind→R6/R7→update 重试 |
|
||||
|
||||
### §4.2 恢复路径
|
||||
|
||||
```
|
||||
R17 (CREATE, FAILED):
|
||||
持久化 → 下轮 E01 → 瞬态(UNCHECKED, CREATE, FAILED)
|
||||
→ E02: action=CREATE ∧ (status=FAILED ∨ data_id="") → 满足 zombie 条件
|
||||
→ 删除节点 + 解除绑定
|
||||
→ 对方节点回到 R9 (MISSING) → 重新进入 create 流程
|
||||
|
||||
R21 (UPDATE, FAILED):
|
||||
持久化 → 下轮 E01 → 瞬态(UNCHECKED, UPDATE, FAILED)
|
||||
→ E02: action=UPDATE ≠ CREATE → 非 zombie,存活
|
||||
→ E03: action → NONE → R3 (UNCHECKED, NONE, FAILED)
|
||||
→ bind → R8 (NORMAL, NONE, FAILED)
|
||||
→ E11e: FAILED 保护 → 跳过 update → 保持 R8
|
||||
⚠ 需人工清除 FAILED 才能恢复
|
||||
|
||||
R22 (UPDATE, PRECONDITION_FAILED):
|
||||
持久化 → 下轮 E01 → 瞬态(UNCHECKED, UPDATE, PRECONDITION_FAILED)
|
||||
→ E02: action=UPDATE ≠ CREATE → 非 zombie,存活
|
||||
→ E03: action → NONE → R5 (UNCHECKED, NONE, PRECONDITION_FAILED)
|
||||
→ bind → R6/R7 (NORMAL) → update 流程重试
|
||||
|
||||
R11 (ABNORMAL):
|
||||
每轮 E01 重置 → bind 重新检查
|
||||
→ 若数据被人工修复 → E04 → R6/R7/R8 (NORMAL)
|
||||
→ 若数据仍损坏 → 再次 R11
|
||||
|
||||
R12 (WARNING):
|
||||
每轮 E01 重置 → bind 重新检查
|
||||
→ 若人工建立了绑定 → E04 → R6/R7 (NORMAL)
|
||||
→ 若对方数据被修复 → E04 → R6/R7 (NORMAL)
|
||||
|
||||
R13 (DEPENDENCY_ERROR):
|
||||
每轮 E01 重置 → bind 重新检查依赖
|
||||
→ 依赖节点达到 NORMAL 且 data_id 非空后
|
||||
→ E07 通过 → 继续 auto_bind 或 create 流程
|
||||
```
|
||||
|
||||
### §4.3 FAILED 保护机制详解
|
||||
|
||||
FAILED 保护确保失败节点不会被后续流程静默覆盖,保留 error 信息供人工调查。
|
||||
|
||||
```
|
||||
进入条件:
|
||||
节点经过 bind 后为 R8 (NORMAL, NONE, FAILED)
|
||||
即: 绑定关系正常,但上一轮执行失败
|
||||
|
||||
保护行为:
|
||||
`strategy_ops/update_ops.py::add_update_action()` 中:
|
||||
if source.status == SyncStatus.FAILED:
|
||||
skip (→ 事件 E11e)
|
||||
结果: R8 原样保持 → 持久化 → 下一轮依然 R8
|
||||
|
||||
解除条件 (需人工介入):
|
||||
1. 清除 error 字段 (→ None)
|
||||
2. 将 status 改为 PENDING 或 SUCCESS
|
||||
3. 下轮 bind 后 → R6 或 R7 → 正常进入 update 流程
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## §5 完备性分析
|
||||
|
||||
### §5.1 状态 × 事件矩阵
|
||||
|
||||
行 = 当前状态,列 = 可能触发的事件。`→Rx` = 转换到目标状态,`—` = 不可能触发。
|
||||
|
||||
| 状态 | E01 | E02 | E03 | E04/E05 | E06~E10 | E11a/b | E11c/d/e | E12~E17 |
|
||||
|------|-----|-----|-----|---------|---------|--------|----------|---------|
|
||||
| R1~R5 | — | — | — | →R6~R13 | — | — | — | — |
|
||||
| R6/R7 | →瞬态 | — | — | — | — | — | →R18/R22 | — |
|
||||
| R8 | →瞬态 | — | — | — | — | — | →R8(skip) | — |
|
||||
| R9 | →瞬态 | — | — | — | →R6/R11~R13 | →R6+R14/R13 | — | — |
|
||||
| R11~R13 | →瞬态 | — | — | — | — | — | — | — |
|
||||
| R14 | →瞬态 | — | — | — | — | — | — | →R15~R17/R23 |
|
||||
| R15 | →瞬态 | — | — | — | — | — | — | →R16/R17 |
|
||||
| R18 | →瞬态 | — | — | — | — | — | — | →R6/R19~R21/R23 |
|
||||
| R19 | →瞬态 | — | — | — | — | — | — | →R20/R21 |
|
||||
| R16/R17 | →瞬态 | — | — | — | — | — | — | — |
|
||||
| R20~R23 | →瞬态 | — | — | — | — | — | — | — |
|
||||
|
||||
> **读表方式**: 找到当前状态行和事件列组的交叉点,即可得到目标状态。`—` 表示该状态不可能触发该事件。
|
||||
|
||||
### §5.2 已知 GAP
|
||||
|
||||
| GAP | 描述 | 风险 | 建议 |
|
||||
|-----|------|------|------|
|
||||
| GAP-1 | `set_binding_status()` / `set_action()` / `set_status()` 是纯 setter,不验证转换合法性 | 中 | 加入前置断言:检查 (当前状态, 新状态) 是否匹配某条已知转换 |
|
||||
| GAP-2 | INV-1 ~ INV-10 未在代码中 assert | 中 | 每次 setter 调用后或阶段结束时断言所有不变量 |
|
||||
| GAP-3 | R22 (PRECONDITION\_FAILED) 节点下一轮行为路径需验证 | 低 | 已补充恢复路径: R22→E01→E03→R5→bind→R6/R7→update 重试 |
|
||||
| GAP-4 | Handler 降级 (E17) 后 status 残留为 PENDING 但 action 已清零 | 低 | 可考虑降级时同时重置 status 为 SUCCESS(当前保持 PENDING) |
|
||||
| GAP-5 | 物理同步后缺少整体一致性检查 (post-sync invariant check) | 低 | 在 `sync_all()` 结束后遍历所有节点验证 INV-1 ~ INV-10 |
|
||||
|
||||
---
|
||||
|
||||
## §6 引用
|
||||
|
||||
| 文档 | 路径 |
|
||||
|------|------|
|
||||
| 枚举定义 | `sync_system_new/common/types.py` |
|
||||
| 决策引擎 | `sync_system_new/sync_system/decision.py` |
|
||||
| 重置策略 | `sync_system_new/common/state_reset.py` |
|
||||
| 同步策略 | `sync_system_new/sync_system/strategy.py` |
|
||||
| 物理同步 | `sync_system_new/datasource/datasource.py` |
|
||||
| ID 解析 | `sync_system_new/sync_system/resolve_ids.py` |
|
||||
| 节点生命周期(叙事) | [节点生命周期状态转移](./节点生命周期状态转移.md) |
|
||||
@@ -0,0 +1,534 @@
|
||||
# 同步编排流程规范 (Pipeline Specification)
|
||||
|
||||
本文档定义了完整的同步编排流程,包括各阶段的职责、节点状态转换和检查点。
|
||||
|
||||
> **详细的状态定义和转换规则,请参考**: [STATE_DEFINITIONS.md](../sync_system/STATE_DEFINITIONS.md)
|
||||
|
||||
---
|
||||
|
||||
## 流程概览
|
||||
|
||||
同步系统的完整执行流程分为 5 个阶段:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Stage 0: 初始化 (Initialization) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ - 创建 Collection (Local/Remote) │
|
||||
│ - 创建 BindingManager │
|
||||
│ - 注册 SyncStrategy (各业务类型) │
|
||||
│ - 创建 DataSource (Local/Remote) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Stage 1: 数据加载 (Data Loading) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 节点状态: binding_status=UNCHECKED, action=NONE, status=PENDING │
|
||||
│ - Persistence.load_nodes() → 从持久化恢复 │
|
||||
│ - DataSource.load_all() → 从数据源加载 │
|
||||
│ - BindingManager.load_bindings() → 加载绑定关系 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Stage 2 & 3: 策略执行与同步 (按 node_type 顺序) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ For each node_type in sync_order: │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ 2.1 bind() → 绑定判定 │ │
|
||||
│ │ 2.2 create() → 设置 CREATE action │ │
|
||||
│ │ └─ 内联 ID 解析 + 前置检查 │ │
|
||||
│ │ 2.3 Commit Creates → 立即同步 CREATE 节点 │ │
|
||||
│ │ └─ DataSource.sync_all(poll_async=True) │ │
|
||||
│ │ 2.4 update() → 设置 UPDATE action │ │
|
||||
│ │ └─ 内联 ID 解析 + 前置检查 │ │
|
||||
│ │ 2.5 Commit Updates → 立即同步 UPDATE 节点 │ │
|
||||
│ │ └─ DataSource.sync_all(poll_async=True) │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 说明: │
|
||||
│ - 每个 node_type 完整同步后才处理下一个(保证依赖顺序) │
|
||||
│ - commit 操作内部包含异步轮询,无需独立轮询阶段 │
|
||||
│ - sync_all() 自动过滤 action=CREATE/UPDATE 的节点 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Stage 4: 同步结果报告 (Summary Report) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ - 打印同步结果摘要 │
|
||||
│ * 节点统计(按 action/status 分类) │
|
||||
│ * 绑定关系统计 │
|
||||
│ * 异常节点检查(data_id 缺失、绑定缺失等) │
|
||||
│ - 收集统计信息(成功/失败/残留问题) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Stage 5: 状态持久化 (State Persistence) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ - Collection.persist() → 保存节点状态 │
|
||||
│ - BindingManager.persist() → 保存绑定关系 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
【流程完成】
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 各阶段节点状态
|
||||
|
||||
### Stage 0: 初始化
|
||||
|
||||
**输出**: 所有组件实例化完成。
|
||||
|
||||
---
|
||||
|
||||
### Stage 1: 数据加载
|
||||
|
||||
**节点状态**:
|
||||
```python
|
||||
binding_status = BindingStatus.UNCHECKED # 未判定
|
||||
action = SyncAction.NONE # 无动作
|
||||
status = SyncStatus.PENDING # 待处理
|
||||
data_id = <从数据源加载> # 已有业务ID
|
||||
origin_data = <从数据源加载> # 原始数据快照
|
||||
data = <从数据源加载> # 当前数据
|
||||
depend_ids = <从数据源加载> # 依赖节点列表
|
||||
```
|
||||
|
||||
**关键点**:
|
||||
- `data_id` 在加载时**必定有值**(来自数据源)
|
||||
- 只有待创建的新节点 `data_id` 才可能为 ""(空字符串)
|
||||
- 从持久化恢复时应重置所有 `action` 为 `NONE`
|
||||
- `binding_status` 初始值是 `UNCHECKED`,不是 `None`
|
||||
|
||||
---
|
||||
|
||||
### Stage 2.1: Bind (状态判定)
|
||||
|
||||
**状态转换**: `binding_status: UNCHECKED → NORMAL/MISSING/ABNORMAL/WARNING/DEPENDENCY_ERROR`
|
||||
|
||||
**可能的结果状态**:
|
||||
|
||||
| 结果状态 | 含义 | 占比 |
|
||||
|---------|------|-----|
|
||||
| **NORMAL** | 有绑定,数据健康 | 大部分 |
|
||||
| **MISSING** | 无绑定,孤儿节点 | 少量 |
|
||||
| **ABNORMAL** | 有绑定,自身数据损坏 | 极少 |
|
||||
| **WARNING** | 有绑定,对方数据损坏 | 极少 |
|
||||
| **DEPENDENCY_ERROR** | 依赖未满足 | 少量 |
|
||||
|
||||
**详细规则**: 参考 [STATE_DEFINITIONS.md § 1-3](../sync_system/STATE_DEFINITIONS.md)
|
||||
|
||||
**补充约束**:
|
||||
- 若依赖节点 `binding_status = NORMAL` 但 `data_id` 为空(含 `status = FAILED`),视为依赖阻断。
|
||||
- 只要依赖节点已回填 `data_id`,即使其 `status = FAILED`,后续类型仍允许进入 Create/Update。
|
||||
- 自动绑定只在**业务键完整且唯一**时才尝试:
|
||||
- 若业务键字段缺失(如 `serial_number = null`),设置 `binding_status = WARNING`,并在 `node.error` 记录原因:`Auto-bind skipped: missing fields ...`。
|
||||
- 若同一业务键出现多对多/多对一,设置 `binding_status = WARNING`,并在 `node.error` 记录原因:`Auto-bind skipped: ambiguous candidates`。
|
||||
- 以上情况仅记录 **Warning**,不进行自动绑定。
|
||||
|
||||
---
|
||||
|
||||
### Stage 2.2: Create (孤儿处理)
|
||||
|
||||
**状态转换**: `action: NONE → CREATE` (对 MISSING 节点)
|
||||
|
||||
**补充约束**:
|
||||
- `binding_status = WARNING` 的节点不会进入 CREATE(`create()` 只查找 MISSING 节点)
|
||||
- 创建时会通过 `IDResolver.resolve_and_validate()` 二次验证依赖 ID,失败则设置 `DEPENDENCY_ERROR`
|
||||
|
||||
**副作用**:
|
||||
- 在对方 Collection 创建新节点
|
||||
- 新节点的 `binding_status = NORMAL, action = CREATE, data_id = ""`
|
||||
- 建立绑定记录
|
||||
- 原 MISSING 节点也变为 `NORMAL`
|
||||
|
||||
**示例**:
|
||||
```
|
||||
Before:
|
||||
local_node: binding_status=MISSING, action=NONE
|
||||
|
||||
After Create:
|
||||
local_node: binding_status=NORMAL, action=NONE
|
||||
remote_node (新建): binding_status=NORMAL, action=CREATE, data_id=""
|
||||
绑定记录: local_id ↔ remote_temp_id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Stage 2.3: Update (差异同步)
|
||||
|
||||
**状态转换**: `action: NONE → UPDATE` (对 NORMAL 且有差异的节点)
|
||||
|
||||
**前提条件**:
|
||||
- `binding_status = NORMAL`
|
||||
- `status != FAILED`(保护上次失败的状态和错误信息)
|
||||
- `compute_diff()` 判定有差异
|
||||
- 配置的 `update_direction` 决定哪一方节点设置 UPDATE
|
||||
|
||||
**内联检查**:
|
||||
- 目标节点 `data_id` 为空 → `status = PRECONDITION_FAILED`
|
||||
- `IDResolver.resolve_and_validate()` 失败 → `status = PRECONDITION_FAILED`
|
||||
- Handler 校验后无实际变更字段 → `action` 降级为 `NONE`(不提交)
|
||||
|
||||
---
|
||||
|
||||
### Stage 2 & 3: 策略执行与同步
|
||||
|
||||
**执行顺序**: 按 `sync_order` 顺序逐个处理 node_type
|
||||
|
||||
**单个 node_type 的完整流程**:
|
||||
|
||||
```
|
||||
For node_type in sync_order:
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 2.1 Bind - 绑定判定 │
|
||||
│ - 设置 binding_status: NORMAL/MISSING/... │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 2.2 Create - 创建策略 │
|
||||
│ - 处理 MISSING 节点,设置 action=CREATE │
|
||||
│ - 内联 ID 解析验证 (resolve_and_validate) │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 2.3 Commit Creates - 立即同步 CREATE │
|
||||
│ - DataSource.sync_all(poll_async=True) │
|
||||
│ - 自动过滤 action=CREATE 的节点 │
|
||||
│ - 内部包含异步轮询(IN_PROGRESS→SUCCESS)│
|
||||
│ - ⭐ 执行 data_id 回填和绑定更新 │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 2.4 Update - 更新策略 │
|
||||
│ - 处理差异节点,设置 action=UPDATE │
|
||||
│ - 内联 ID 解析验证 + data_id 检查 │
|
||||
│ - FAILED 节点自动跳过 │
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 2.5 Commit Updates - 立即同步 UPDATE │
|
||||
│ - DataSource.sync_all(poll_async=True) │
|
||||
│ - 自动过滤 action=UPDATE 的节点 │
|
||||
│ - 内部包含异步轮询(IN_PROGRESS→SUCCESS)│
|
||||
└─────────────────────────────────────────────┘
|
||||
↓
|
||||
下一个 node_type
|
||||
```
|
||||
|
||||
**关键特性**:
|
||||
1. **按类型顺序**: 每个 node_type 完全同步后才处理下一个(保证依赖顺序)
|
||||
2. **立即提交**: create/update 后立即同步,不等待所有策略执行完
|
||||
3. **内置轮询**: `sync_all(poll_async=True)` 内部自动轮询异步任务
|
||||
4. **data_id 回填**: CREATE 成功后立即回填 data_id,下游依赖可用
|
||||
|
||||
**同步状态转换**:
|
||||
|
||||
**同步接口(一步式)**:
|
||||
```
|
||||
action=CREATE, API 成功 → status=SUCCESS, data_id=<远程ID>
|
||||
action=CREATE, API 失败 → status=FAILED, error=<错误信息>
|
||||
action=UPDATE, API 成功 → status=SUCCESS
|
||||
action=UPDATE, API 失败 → status=FAILED, error=<错误信息>
|
||||
action=NONE → status=PENDING (不执行)
|
||||
```
|
||||
|
||||
**异步接口(两步式,内置轮询)**:
|
||||
```
|
||||
action=CREATE, 提交成功 → status=IN_PROGRESS, task_id=<任务ID>
|
||||
status=IN_PROGRESS, 轮询中... → status=IN_PROGRESS (保持)
|
||||
status=IN_PROGRESS, 轮询成功 → status=SUCCESS, data_id=<远程ID>
|
||||
status=IN_PROGRESS, 轮询失败 → status=FAILED, error=<错误信息>
|
||||
status=IN_PROGRESS, 超时 → status=FAILED, error="Timeout"
|
||||
```
|
||||
|
||||
**关键副作用**(由 DataSource 在 sync_all 中执行):
|
||||
- CREATE 成功: `data_id` 从 "" 更新为远程分配的 ID,并回写 `data` 主键字段
|
||||
- CREATE 成功: 绑定记录从临时ID更新为真实ID
|
||||
- UPDATE 成功: `origin_data` 更新为当前 `data`
|
||||
- DELETE 成功: 移除绑定记录
|
||||
|
||||
**skip_sync 处理**:
|
||||
- 如果 `strategy.skip_sync = True`,跳过 create/update/commit 步骤
|
||||
- bind 步骤始终执行(用于状态判定)
|
||||
|
||||
**详细规则**: 参考 [STATE_DEFINITIONS.md § 5](../sync_system/STATE_DEFINITIONS.md)
|
||||
|
||||
---
|
||||
|
||||
### Stage 4: 同步结果报告
|
||||
|
||||
**目标**: 打印同步结果摘要,帮助用户了解同步情况
|
||||
|
||||
**报告内容**:
|
||||
1. **节点统计**:按 node_type 统计各种 action/status 的节点数量
|
||||
2. **绑定关系**:每个 node_type 的绑定记录数
|
||||
3. **异常检查**:扫描并报告异常情况:
|
||||
- CREATE 成功但 data_id 为空(DataSource 回填失败)
|
||||
- CREATE 成功但绑定记录缺失
|
||||
- UPDATE/CREATE 失败的节点及错误原因
|
||||
|
||||
**输出示例**:
|
||||
```
|
||||
================================================================================
|
||||
📊 Local Collection Summary
|
||||
================================================================================
|
||||
Node Type Bindings Create Update Delete None Total
|
||||
--------------------------------------------------------------------------------
|
||||
project 5/5 0(0/0/0) 2(2/0/0) 0(0/0/0) 3 5(5/0/0)
|
||||
contract 10/12 2(2/0/0) 5(5/0/0) 0(0/0/0) 5 12(12/0/0)
|
||||
```
|
||||
|
||||
**注意**: 此阶段不修改任何状态,仅作为信息展示。
|
||||
|
||||
---
|
||||
|
||||
### Stage 5: 状态持久化
|
||||
|
||||
**持久化内容**:
|
||||
- `binding_status`, `action`, `status` 的最终值
|
||||
- `data_id` (可能在 CREATE 后更新)
|
||||
- `error` (如果失败)
|
||||
- 绑定关系的完整映射
|
||||
|
||||
---
|
||||
|
||||
## 错误处理策略
|
||||
|
||||
| 阶段 | 失败类型 | 处理策略 |
|
||||
|------|---------|---------|
|
||||
| Stage 0 | 初始化失败 | **终止流程**,抛出异常 |
|
||||
| Stage 1 | 数据加载失败 | **终止流程**,抛出异常 |
|
||||
| Stage 2 & 3 | 代码异常 | **终止流程**,抛出异常 |
|
||||
| Stage 2: create/update 内联检查 | 依赖ID未解析、data_id缺失 | **继续流程**,节点标记为 DEPENDENCY_ERROR 或 PRECONDITION_FAILED |
|
||||
| Stage 2 & 3 | API 调用失败 | **继续流程**,标记节点 FAILED |
|
||||
| Stage 4 | - | 总是继续,仅作为报告 |
|
||||
| Stage 5 | 持久化失败 | **记录错误**,不影响内存状态 |
|
||||
|
||||
---
|
||||
|
||||
## 节点状态完整时间线
|
||||
|
||||
```
|
||||
┌──────────────┬─────────────┬────────┬────────┬─────────┐
|
||||
│ 阶段 │ binding_ │ action │ status │ data_id │
|
||||
│ │ status │ │ │ │
|
||||
├──────────────┼─────────────┼────────┼────────┼─────────┤
|
||||
│ 数据加载 │ UNCHECKED │ NONE │PENDING │ 已加载 │
|
||||
│ Bind │ NORMAL/... │ NONE │PENDING │ 已加载 │
|
||||
│ Create │ NORMAL │ CREATE │PENDING │ ""* │
|
||||
│ Commit Create│ NORMAL │ CREATE │SUCCESS │ 远程ID** │
|
||||
│ Update │ NORMAL │ UPDATE │PENDING │ 已加载 │
|
||||
│ Commit Update│ NORMAL │ UPDATE │SUCCESS │ 已加载 │
|
||||
└──────────────┴─────────────┴────────┴────────┴─────────┘
|
||||
|
||||
* 新创建的节点 data_id 初始为 ""
|
||||
** CREATE 成功后 data_id 立即更新为远程分配的ID,下游依赖可用
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 配置驱动的流程控制
|
||||
|
||||
### 跳过某些阶段
|
||||
|
||||
```python
|
||||
pipeline_config = {
|
||||
"skip_bind": False, # 是否跳过 bind
|
||||
"skip_create": False, # 是否跳过 create
|
||||
"skip_update": False, # 是否跳过 update
|
||||
"skip_post_check": False, # 是否跳过同步后检查
|
||||
"fail_on_warning": False, # Pre-Check 时是否将 WARNING 视为失败
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:`skip_pre_check` 已移除,原 `_pre_commit_check` 逻辑已内联到 create/update 方法中。
|
||||
```
|
||||
|
||||
### 策略预设应用
|
||||
|
||||
```python
|
||||
# 首次同步:启用自动绑定,双向创建
|
||||
for strategy in strategies:
|
||||
strategy.apply_preset("first_sync")
|
||||
|
||||
# 日常同步:仅推送更新
|
||||
for strategy in strategies:
|
||||
strategy.apply_preset("push_only")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 幂等性保证
|
||||
|
||||
**要求**: 同一个 Collection 的多次同步应该是幂等的。
|
||||
|
||||
**设计保证**:
|
||||
1. **Bind 阶段**: 基于绑定记录判定,已绑定节点不会重复绑定
|
||||
2. **Create 阶段**: 已有绑定的节点 (NORMAL) 不会重复创建
|
||||
3. **Update 阶段**: 基于 `compute_diff()` 判定,无差异则不执行
|
||||
4. **Sync 阶段**: `action=NONE` 的节点跳过执行
|
||||
|
||||
**前提条件**:
|
||||
- 绑定记录正确持久化
|
||||
- 节点的 `binding_status` 正确保存和加载
|
||||
- `compute_diff()` 逻辑准确
|
||||
|
||||
---
|
||||
|
||||
## 监控和日志
|
||||
|
||||
### 关键指标
|
||||
|
||||
| 指标 | 含义 | 阶段 |
|
||||
|------|------|-----|
|
||||
| `nodes_loaded` | 加载的节点总数 | Stage 1 |
|
||||
| `nodes_normal` | NORMAL 状态节点数 | Stage 2.1 |
|
||||
| `nodes_missing` | MISSING 状态节点数 | Stage 2.1 |
|
||||
| `nodes_abnormal` | ABNORMAL+WARNING 节点数 | Stage 2.1 |
|
||||
| `nodes_dep_error` | DEPENDENCY_ERROR 节点数 | Stage 2.1 |
|
||||
| `actions_create` | CREATE 动作节点数 | Stage 2.2 |
|
||||
| `actions_update` | UPDATE 动作节点数 | Stage 2.3 |
|
||||
| `sync_success` | 同步成功节点数 | Stage 3 |
|
||||
| `sync_failed` | 同步失败节点数 | Stage 3 |
|
||||
| `success_rate` | 成功率 (success/total) | Stage 4 |
|
||||
|
||||
### 日志级别
|
||||
|
||||
```
|
||||
INFO: 流程阶段切换、统计信息
|
||||
DEBUG: 节点级别的详细操作
|
||||
WARN: 检查发现的警告、同步失败
|
||||
ERROR: 检查发现的错误、致命异常
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 参考文档
|
||||
|
||||
- [STATE_DEFINITIONS.md](../sync_system/STATE_DEFINITIONS.md) - 详细的状态定义和转换规则
|
||||
- [CHECK_SPEC.md](./CHECK_SPEC.md) - 检查点的实现细节和代码示例(可选)
|
||||
# 1. 持久化 Collection
|
||||
await local_collection.persist()
|
||||
await remote_collection.persist()
|
||||
|
||||
# 2. 持久化 Binding
|
||||
await binding_manager.persist()
|
||||
```
|
||||
|
||||
**持久化内容**:
|
||||
- 节点的 `binding_status`, `action`, `status`, `error`
|
||||
- 节点的 `data_id`(可能在 CREATE 后更新)
|
||||
- 绑定关系的完整映射
|
||||
|
||||
---
|
||||
|
||||
## 错误处理策略
|
||||
|
||||
### 各阶段的失败处理
|
||||
|
||||
| 阶段 | 失败原因示例 | 处理策略 |
|
||||
|------|------------|---------|
|
||||
| Stage 1: 数据加载 | 网络错误、文件缺失 | **终止流程**,抛出异常 |
|
||||
| Stage 2.1: Bind | 代码bug | **终止流程**,抛出异常 |
|
||||
| Stage 2.2-2.3: Create/Update | 代码bug | **终止流程**,抛出异常 |
|
||||
| Stage 2: create/update 内联检查 | 依赖ID未解析、data_id缺失 | **继续流程**,节点标记为 DEPENDENCY_ERROR 或 PRECONDITION_FAILED |
|
||||
| Stage 3: Physical Sync | API错误、权限不足 | **继续流程**,标记节点为 FAILED |
|
||||
| Stage 4: Post-Sync Check | 部分节点同步失败 | **继续流程**,打印警告 |
|
||||
| Stage 5: Persistence | 磁盘满、权限错误 | **记录错误**,但不影响内存状态 |
|
||||
|
||||
### 部分成功的处理
|
||||
|
||||
在 Stage 3 中,允许部分节点成功、部分失败:
|
||||
- 成功的节点: `status = SUCCESS`
|
||||
- 失败的节点: `status = FAILED`, `error = "错误信息"`
|
||||
|
||||
Post-Sync Check 会统计成功率并给出警告。
|
||||
|
||||
---
|
||||
|
||||
## 配置驱动的流程控制
|
||||
|
||||
### 跳过某些阶段
|
||||
|
||||
通过配置控制哪些阶段执行:
|
||||
|
||||
```python
|
||||
pipeline_config = {
|
||||
"skip_bind": False, # 是否跳过 bind
|
||||
"skip_create": False, # 是否跳过 create
|
||||
"skip_update": False, # 是否跳过 update
|
||||
"skip_post_check": False, # 是否跳过同步后检查
|
||||
"fail_on_warning": False, # 是否将 WARNING 也视为失败
|
||||
}
|
||||
```
|
||||
|
||||
### 策略预设应用
|
||||
|
||||
在 Stage 2 之前,可以应用策略预设:
|
||||
|
||||
```python
|
||||
# 首次同步:启用自动绑定,双向创建
|
||||
for strategy in strategies:
|
||||
strategy.apply_preset("first_sync")
|
||||
|
||||
# 日常同步:仅推送更新
|
||||
for strategy in strategies:
|
||||
strategy.apply_preset("push_only")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 监控和日志
|
||||
|
||||
### 关键指标
|
||||
|
||||
在各阶段收集以下指标:
|
||||
|
||||
| 指标 | 含义 |
|
||||
|------|------|
|
||||
| `nodes_loaded` | 加载的节点总数 |
|
||||
| `nodes_bound` | 成功绑定的节点数 |
|
||||
| `nodes_missing` | MISSING 状态的节点数 |
|
||||
| `nodes_abnormal` | ABNORMAL/WARNING 状态的节点数 |
|
||||
| `nodes_created` | CREATE 动作的节点数 |
|
||||
| `nodes_updated` | UPDATE 动作的节点数 |
|
||||
| `sync_success` | 同步成功的节点数 |
|
||||
| `sync_failed` | 同步失败的节点数 |
|
||||
| `errors_count` | 检查发现的 ERROR 数量 |
|
||||
| `warnings_count` | 检查发现的 WARNING 数量 |
|
||||
|
||||
### 日志级别
|
||||
|
||||
```
|
||||
INFO: 流程阶段切换、统计信息
|
||||
DEBUG: 节点级别的详细操作
|
||||
WARN: 检查发现的警告
|
||||
ERROR: 检查发现的错误、异常
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 幂等性保证
|
||||
|
||||
**要求**: 同一个 Collection 的多次同步应该是幂等的。
|
||||
|
||||
**设计保证**:
|
||||
1. **Bind 阶段**: 基于绑定记录判定状态,已绑定的节点不会重复绑定
|
||||
2. **Create 阶段**: 已有绑定的节点(NORMAL)不会重复创建
|
||||
3. **Update 阶段**: 基于 `compute_diff()` 判定,无差异则不执行
|
||||
4. **Sync 阶段**: DataSource 根据 `action` 执行,`action=NONE` 的节点跳过
|
||||
|
||||
**前提条件**:
|
||||
- Binding 记录正确持久化
|
||||
- 节点的 `binding_status` 正确保存和加载
|
||||
- `compute_diff()` 逻辑准确
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
详细的检查规范和状态转换规则,请参考:
|
||||
- [CHECK_SPEC.md](./CHECK_SPEC.md) - 检查点详细规范
|
||||
- [STATE_TRANSITIONS.md](./STATE_TRANSITIONS.md) - 状态转换表
|
||||
|
||||
@@ -0,0 +1,492 @@
|
||||
# 节点生命周期状态转移 (Node Lifecycle State Transitions)
|
||||
|
||||
本文档从一个**单节点视角**出发,完整描述一个 `SyncNode` 从诞生到持久化的全部状态变化。
|
||||
|
||||
> **代码映射**:
|
||||
> - 持久化重置:`sync_system_new/common/state_reset.py` → `NodeStateReset`
|
||||
> - 绑定判定:`sync_system_new/sync_system/decision.py` → `SyncDecisionEngine`
|
||||
> - 策略执行:`sync_system_new/sync_system/strategy.py` → `DefaultSyncStrategy`
|
||||
> - ID 解析:`sync_system_new/sync_system/resolve_ids.py` → `IDResolver`
|
||||
> - 物理同步:`sync_system_new/datasource/datasource.py` → `BaseDataSource`
|
||||
> - Handler 降级:`sync_system_new/domain/*/api_handler.py`
|
||||
|
||||
---
|
||||
|
||||
## 状态字段一览
|
||||
|
||||
| 字段 | 类型 | 含义 | 谁负责设置 |
|
||||
|------|------|------|-----------|
|
||||
| `binding_status` | BindingStatus | 绑定健康状态 | Strategy.bind() |
|
||||
| `action` | SyncAction | 待执行动作 | Strategy.create() / update() |
|
||||
| `status` | SyncStatus | 执行结果状态 | DataSource.sync_all() |
|
||||
| `data_id` | str | 业务主键ID | 数据加载 / DataSource 回填 |
|
||||
| `error` | str \| None | 错误信息 | 各阶段 |
|
||||
|
||||
---
|
||||
|
||||
## 完整生命周期
|
||||
|
||||
### 第一次运行(无持久化数据)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 节点尚不存在 │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
数据源加载 (Stage 1)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 节点诞生 │
|
||||
│ │
|
||||
│ binding_status = UNCHECKED ← Collection 创建节点时的初始值 │
|
||||
│ action = NONE │
|
||||
│ status = PENDING │
|
||||
│ data_id = "abc-123" ← 从数据源获取的业务主键 │
|
||||
│ data = {...} ← 从数据源获取的业务数据 │
|
||||
│ origin_data = {...} ← 同上,原始快照 │
|
||||
│ depend_ids = [] ← 空,待 bind 阶段填充 │
|
||||
│ error = None │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第二次及后续运行(有持久化数据)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 从持久化恢复(原始值) │
|
||||
│ │
|
||||
│ binding_status = NORMAL (上次值) │
|
||||
│ action = UPDATE (上次值) │
|
||||
│ status = SUCCESS (上次值) │
|
||||
│ data_id = "abc-123" │
|
||||
│ error = None (上次值) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
阶段1:加载时重置 (NodeStateReset)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 阶段1重置后 │
|
||||
│ │
|
||||
│ binding_status = UNCHECKED ← 重置(每次重新判定) │
|
||||
│ action = UPDATE ← 保留!(用于阶段2识别 CREATE 失败) │
|
||||
│ status = SUCCESS ← 保留!(不自动重置) │
|
||||
│ data_id = "abc-123" ← 不变 │
|
||||
│ data = None ← 重置(等待数据源覆盖) │
|
||||
│ origin_data = None ← 重置(等待数据源覆盖) │
|
||||
│ error = None ← 清空(避免上次错误干扰) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
阶段2:CREATE 僵尸清理
|
||||
(只影响 action=CREATE 且 status=FAILED 或 data_id="" 的节点)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 阶段2清理后(正常节点) │
|
||||
│ │
|
||||
│ action = NONE ← 全部重置为 NONE,准备进入策略阶段 │
|
||||
│ 其他字段不变 │
|
||||
│ │
|
||||
│ (若为 CREATE 僵尸节点:直接从 Collection 删除 + 解除绑定) │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
数据源加载覆盖 (Stage 1 后半)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ 数据源加载后 │
|
||||
│ │
|
||||
│ data = {...} ← 数据源最新数据覆盖 │
|
||||
│ origin_data = {...} ← 数据源最新数据覆盖 │
|
||||
│ 其他字段不变 │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Bind 阶段状态转移 (Stage 2.1)
|
||||
|
||||
Bind 分三个子阶段顺序执行,每个子阶段只修改 `binding_status`。
|
||||
|
||||
### Phase 1: 核心状态判定
|
||||
|
||||
> **代码**:`strategy_ops/bind_ops.py::phase_core_state()` → `SyncDecisionEngine.determine_core_status()`
|
||||
|
||||
```
|
||||
输入:binding_status = UNCHECKED
|
||||
|
||||
┌─ 有绑定记录?
|
||||
│
|
||||
┌─────┤
|
||||
│ YES │
|
||||
│ └─── 本地数据 + 远程数据 都健康?
|
||||
│ │
|
||||
│ ┌────┴────┐
|
||||
│ │ YES │ NO
|
||||
│ ▼ ▼
|
||||
│ NORMAL WARNING / ABNORMAL
|
||||
│
|
||||
│ NO
|
||||
└──────────────► MISSING
|
||||
```
|
||||
|
||||
| 输入条件 | binding_status 输出 |
|
||||
|---------|-------------------|
|
||||
| 有绑定,双方数据健康 | **NORMAL** |
|
||||
| 有绑定,自身数据损坏 | **ABNORMAL** |
|
||||
| 有绑定,对方数据损坏 | **WARNING** |
|
||||
| 无绑定记录 | **MISSING** |
|
||||
|
||||
### Phase 2: 依赖检查
|
||||
|
||||
> **代码**:`strategy_ops/bind_ops.py::phase_dependency_check()` → `strategy_ops/bind_ops.py::check_dependencies_for_nodes()`
|
||||
|
||||
**仅处理 `binding_status = MISSING` 的节点**,其他状态跳过。
|
||||
|
||||
```
|
||||
输入:binding_status = MISSING
|
||||
|
||||
┌─ 有配置 depend_fields?
|
||||
│
|
||||
┌─────┤
|
||||
│ NO └──► 保持 MISSING(无依赖,直接通过)
|
||||
│
|
||||
│ YES ──► 逐一检查依赖节点:
|
||||
│ │
|
||||
│ ┌────┴────────────────────────┐
|
||||
│ │ 全部依赖节点: │
|
||||
│ │ - 存在? ✅ │
|
||||
│ │ - binding_status=NORMAL? ✅ │
|
||||
│ │ - data_id 非空? ✅ │
|
||||
│ └────┬────────────────────────┘
|
||||
│ │
|
||||
│ ┌────┴────┐
|
||||
│ │ 全通过 │ 任一失败
|
||||
│ ▼ ▼
|
||||
│ MISSING DEPENDENCY_ERROR
|
||||
│ + error = "依赖项不满足: ..."
|
||||
│ + depend_ids 已填充
|
||||
```
|
||||
|
||||
**副作用**:无论检查是否通过,都会填充 `node.depend_ids`。
|
||||
|
||||
### Phase 3: 自动绑定
|
||||
|
||||
> **代码**:`strategy_ops/bind_ops.py::phase_auto_binding()`
|
||||
|
||||
**仅处理 `binding_status = MISSING` 且依赖已通过的节点**。
|
||||
|
||||
```
|
||||
输入:binding_status = MISSING
|
||||
|
||||
auto_bind = False?
|
||||
└──► MISSING → WARNING("跳过自动绑定,无法确认是否需要创建")
|
||||
|
||||
auto_bind = True:
|
||||
│
|
||||
├─ 业务键字段缺失?
|
||||
│ └──► MISSING → WARNING("缺少业务键字段: ...")
|
||||
│
|
||||
├─ 业务键中 ID 字段解析失败?
|
||||
│ └──► MISSING → DEPENDENCY_ERROR("业务键中的ID字段解析失败")
|
||||
│
|
||||
├─ 去噪后 1:1 匹配?
|
||||
│ └──► MISSING → NORMAL(执行自动绑定,建立绑定记录)
|
||||
│
|
||||
├─ N:0(无远程匹配)?
|
||||
│ ├──► create_enabled = True → MISSING → NORMAL,并 spawn 目标 CREATE 节点(S06)
|
||||
│ └──► create_enabled = False → MISSING → WARNING(孤儿且不允许自动创建,需人工处理)
|
||||
│
|
||||
└─ N:M / 多对多 / 多对一?
|
||||
└──► MISSING → WARNING("存在多个候选节点,无法自动绑定")
|
||||
```
|
||||
|
||||
### Bind 阶段结束后可能的状态
|
||||
|
||||
| binding_status | 后续可执行的操作 |
|
||||
|---------------|----------------|
|
||||
| **NORMAL** | UPDATE(如有差异)|
|
||||
| **MISSING** | CREATE |
|
||||
| **ABNORMAL** | 无(需人工) |
|
||||
| **WARNING** | 无(阻止自动操作) |
|
||||
| **DEPENDENCY_ERROR** | 无(等待依赖修复) |
|
||||
|
||||
---
|
||||
|
||||
## Create 阶段状态转移 (Stage 2.2)
|
||||
|
||||
> **代码**:`create()` → `strategy_ops/create_ops.py::add_create_action()`
|
||||
|
||||
> **对齐说明(与状态机配置一致)**:
|
||||
> - create_prepare 的入口仍是源节点 `binding_status = MISSING`。
|
||||
> - 但当前配置下,`N:0 + create_enabled=true` 可以在自动绑定阶段直接把源节点转为 `NORMAL`,并 `emit spawn_target_node_state=S06`。
|
||||
> - 因此“触发创建”的入口有两条:
|
||||
> 1) auto_bind 分支(E08f)直接 spawn;
|
||||
> 2) create_prepare 分支(E09b)在 create 检查成功后 spawn。
|
||||
|
||||
**仅处理 `binding_status = MISSING` 的节点**。
|
||||
|
||||
```
|
||||
输入:源节点 binding_status = MISSING, action = NONE
|
||||
|
||||
┌─ 源节点数据为空?
|
||||
│ └──► 跳过,记录日志
|
||||
│
|
||||
├─ IDResolver.resolve_and_validate() 失败?
|
||||
│ └──► binding_status → DEPENDENCY_ERROR
|
||||
│ error = "创建时依赖 ID 未解析: ..."
|
||||
│
|
||||
└─ 成功 ──► 在目标 Collection 创建新节点
|
||||
│
|
||||
├─ 源节点:
|
||||
│ binding_status → NORMAL("从本地创建到远程")
|
||||
│ action 不变(仍为 NONE)
|
||||
│
|
||||
└─ 新建目标节点:
|
||||
binding_status = NORMAL
|
||||
action = CREATE
|
||||
status = PENDING
|
||||
data_id = ""(待远程返回)
|
||||
data = 已替换 ID 的数据副本
|
||||
│
|
||||
└─ 同时建立绑定记录:local_id ↔ remote_id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commit Creates — 物理同步 (Stage 2.3)
|
||||
|
||||
> **代码**:`DataSource.sync_all()` → Handler.create()
|
||||
|
||||
```
|
||||
输入:action = CREATE, status = PENDING
|
||||
|
||||
┌─ Handler 执行 API 调用
|
||||
│
|
||||
├─ 同步 API 成功(200/201)
|
||||
│ └──► status → SUCCESS
|
||||
│ data_id = "remote-xyz"(远程分配的 ID 回填)
|
||||
│ data["id"] = "remote-xyz"(主键字段回写)
|
||||
│
|
||||
├─ 异步 API 提交成功(202)
|
||||
│ └──► status → IN_PROGRESS
|
||||
│ context.task_id = "task-xxx"
|
||||
│ │
|
||||
│ └─ 轮询结果:
|
||||
│ ├─ 成功 → status → SUCCESS, data_id 回填
|
||||
│ ├─ 失败 → status → FAILED, error = "..."
|
||||
│ └─ 超时 → status → FAILED, error = "Timeout"
|
||||
│
|
||||
└─ API 失败(4xx/5xx)
|
||||
└──► status → FAILED
|
||||
error = "远程操作失败: ..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Update 阶段状态转移 (Stage 2.4)
|
||||
|
||||
> **代码**:`update()` → `strategy_ops/update_ops.py::add_update_action()`
|
||||
|
||||
> **对齐说明**:图里的 `E12a` 从 `S01` 出发并不矛盾。`S01` 约束的是“源节点”有 `data_id`;
|
||||
> `E12a` 检查的是“目标节点”是否有 `data_id`(guard: `target_has_data_id=false`),两者不是同一个对象。
|
||||
|
||||
**过滤条件**:`binding_status = NORMAL` 且 `status != FAILED`
|
||||
|
||||
```
|
||||
输入:源节点 binding_status = NORMAL, action = NONE
|
||||
|
||||
┌─ status == FAILED?
|
||||
│ └──► 跳过(保护上次失败的状态和错误信息)
|
||||
│
|
||||
├─ 源节点数据为空?
|
||||
│ └──► 跳过
|
||||
│
|
||||
├─ 通过绑定找不到目标节点?
|
||||
│ └──► 跳过,记录日志
|
||||
│
|
||||
├─ 目标节点 data_id 为空?
|
||||
│ └──► 目标节点 status → PRECONDITION_FAILED
|
||||
│ ("目标节点缺少 data_id")
|
||||
│
|
||||
├─ IDResolver.resolve_and_validate() 失败?
|
||||
│ └──► 目标节点 action → UPDATE
|
||||
│ 目标节点 status → PRECONDITION_FAILED
|
||||
│ 目标节点 error = "依赖 ID 未解析: ..."
|
||||
│
|
||||
├─ _needs_update() 判定无差异?
|
||||
│ └──► 跳过(不设置 action)
|
||||
│
|
||||
└─ 有差异 ──► 更新目标节点:
|
||||
action → UPDATE
|
||||
status → PENDING
|
||||
data = 已替换 ID 的数据副本
|
||||
error = None
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commit Updates — 物理同步 (Stage 2.5)
|
||||
|
||||
> **代码**:`DataSource.sync_all()` → Handler.update()
|
||||
|
||||
```
|
||||
输入:action = UPDATE, status = PENDING
|
||||
|
||||
┌─ Handler 构建 API 请求
|
||||
│
|
||||
├─ Handler 发现无实际变更字段
|
||||
│ └──► action → NONE(Handler 降级,不提交 API)
|
||||
│
|
||||
├─ 同步 API 成功(200)
|
||||
│ └──► status → SUCCESS
|
||||
│
|
||||
├─ 异步 API 提交成功(202)
|
||||
│ └──► status → IN_PROGRESS → (轮询)→ SUCCESS / FAILED
|
||||
│
|
||||
└─ API 失败
|
||||
└──► status → FAILED
|
||||
error = "远程操作失败: ..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 持久化阶段 (Stage 5)
|
||||
|
||||
```
|
||||
节点当前状态原样写入存储。
|
||||
|
||||
持久化字段:
|
||||
✅ node_id, data_id, data, origin_data
|
||||
✅ binding_status, action, status, error, context
|
||||
❌ depend_ids(不持久化,下次 bind 时从 data 实时计算)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 全景时间线表
|
||||
|
||||
下表展示**一个节点在典型场景中的状态变化**:
|
||||
|
||||
### 场景 A:首次创建(本地 → 远程)
|
||||
|
||||
| 阶段 | binding_status | action | status | data_id | error |
|
||||
|------|---------------|--------|--------|---------|-------|
|
||||
| 数据加载 | UNCHECKED | NONE | PENDING | "abc-123" | None |
|
||||
| bind phase1 | **MISSING** | NONE | PENDING | "abc-123" | None |
|
||||
| bind phase2 | MISSING | NONE | PENDING | "abc-123" | None |
|
||||
| bind phase3 | MISSING | NONE | PENDING | "abc-123" | None |
|
||||
| create | **NORMAL** | NONE | PENDING | "abc-123" | None |
|
||||
| _(新建远程节点)_ | NORMAL | **CREATE** | PENDING | **""** | None |
|
||||
| commit create | NORMAL | CREATE | **SUCCESS** | **"remote-456"** | None |
|
||||
| 持久化 | NORMAL | CREATE | SUCCESS | "remote-456" | None |
|
||||
|
||||
### 场景 B:日常更新
|
||||
|
||||
| 阶段 | binding_status | action | status | data_id | error |
|
||||
|------|---------------|--------|--------|---------|-------|
|
||||
| 持久化恢复 | NORMAL→**UNCHECKED** | CREATE→保留 | SUCCESS→保留 | "abc-123" | 清空 |
|
||||
| 阶段2 action重置 | UNCHECKED | **NONE** | SUCCESS | "abc-123" | None |
|
||||
| 数据源覆盖 | UNCHECKED | NONE | SUCCESS | "abc-123" | None |
|
||||
| bind phase1 | **NORMAL** | NONE | SUCCESS | "abc-123" | None |
|
||||
| update 检测差异 | NORMAL | NONE | SUCCESS | "abc-123" | None |
|
||||
| _(目标节点)_ | NORMAL | **UPDATE** | **PENDING** | "remote-456" | None |
|
||||
| commit update | NORMAL | UPDATE | **SUCCESS** | "remote-456" | None |
|
||||
|
||||
### 场景 C:依赖阻断
|
||||
|
||||
| 阶段 | binding_status | action | status | data_id | error |
|
||||
|------|---------------|--------|--------|---------|-------|
|
||||
| 数据加载 | UNCHECKED | NONE | PENDING | "contract-789" | None |
|
||||
| bind phase1 | **MISSING** | NONE | PENDING | "contract-789" | None |
|
||||
| bind phase2 | **DEPENDENCY_ERROR** | NONE | PENDING | "contract-789" | "依赖项不满足: project_id: 状态=dep_err" |
|
||||
| _(后续阶段不处理)_ | | | | | |
|
||||
| 持久化 | DEPENDENCY_ERROR | NONE | PENDING | "contract-789" | "依赖项不满足..." |
|
||||
|
||||
### 场景 D:CREATE 失败 → 下次自动恢复
|
||||
|
||||
| 阶段 | binding_status | action | status | data_id | error |
|
||||
|------|---------------|--------|--------|---------|-------|
|
||||
| **第 1 次运行** | | | | | |
|
||||
| create | NORMAL | CREATE | PENDING | "" | None |
|
||||
| commit create 失败 | NORMAL | CREATE | **FAILED** | "" | "远程操作失败: 500" |
|
||||
| 持久化 | NORMAL | CREATE | FAILED | "" | "远程操作失败: 500" |
|
||||
| **第 2 次运行** | | | | | |
|
||||
| 阶段1恢复 | **UNCHECKED** | CREATE | FAILED | "" | **None** |
|
||||
| 阶段2僵尸清理 | _(节点被删除,绑定被解除)_ | | | | |
|
||||
| 数据源重新加载 | UNCHECKED | NONE | PENDING | "" → 新 node_id | None |
|
||||
| bind → create | 重新走首次创建流程 | | | | |
|
||||
|
||||
### 场景 E:UPDATE 失败 → 保留供人工处理
|
||||
|
||||
| 阶段 | binding_status | action | status | data_id | error |
|
||||
|------|---------------|--------|--------|---------|-------|
|
||||
| **第 1 次运行** | | | | | |
|
||||
| update | NORMAL | UPDATE | PENDING | "remote-456" | None |
|
||||
| commit update 失败 | NORMAL | UPDATE | **FAILED** | "remote-456" | "403 Forbidden" |
|
||||
| 持久化 | NORMAL | UPDATE | FAILED | "remote-456" | "403 Forbidden" |
|
||||
| **第 2 次运行** | | | | | |
|
||||
| 阶段1恢复 | UNCHECKED | UPDATE | FAILED | "remote-456" | None |
|
||||
| 阶段2 action重置 | UNCHECKED | **NONE** | **FAILED** | "remote-456" | None |
|
||||
| bind | NORMAL | NONE | FAILED | "remote-456" | None |
|
||||
| update 跳过 | _(status=FAILED,自动跳过)_ | | | | |
|
||||
| 持久化 | NORMAL | NONE | FAILED | "remote-456" | None |
|
||||
| _(需人工重置 status 后才能重新更新)_ | | | | | |
|
||||
|
||||
---
|
||||
|
||||
## 状态转换合法性约束
|
||||
|
||||
### binding_status 允许的转换
|
||||
|
||||
```
|
||||
UNCHECKED → NORMAL (核心判定: 有绑定且健康)
|
||||
UNCHECKED → MISSING (核心判定: 无绑定记录)
|
||||
UNCHECKED → ABNORMAL (核心判定: 自身数据损坏)
|
||||
UNCHECKED → WARNING (核心判定: 对方数据损坏)
|
||||
|
||||
MISSING → DEPENDENCY_ERROR (依赖检查: 父节点不满足)
|
||||
MISSING → WARNING (自动绑定: 业务键缺失 / 多候选 / auto_bind=False)
|
||||
MISSING → DEPENDENCY_ERROR (自动绑定: ID 字段解析失败)
|
||||
MISSING → NORMAL (自动绑定: 1:1 成功)
|
||||
MISSING → NORMAL (create: 源节点创建成功后)
|
||||
MISSING → DEPENDENCY_ERROR (create: ID 解析失败)
|
||||
```
|
||||
|
||||
> **注意**:当前 `SyncNode.set_binding_status()` 不做转换合法性校验【未实现】。
|
||||
|
||||
### action 允许的转换
|
||||
|
||||
```
|
||||
NONE → CREATE (create: 新建目标节点)
|
||||
NONE → UPDATE (update: 检测到差异)
|
||||
UPDATE → NONE (Handler 降级: 实际无变更字段)
|
||||
```
|
||||
|
||||
### status 允许的转换
|
||||
|
||||
```
|
||||
PENDING → IN_PROGRESS (DataSource: 异步提交成功)
|
||||
PENDING → SUCCESS (DataSource: 同步执行成功)
|
||||
PENDING → FAILED (DataSource: 执行失败)
|
||||
PENDING → SKIPPED (DataSource: 被跳过)
|
||||
(常见于 CREATE/UPDATE:Handler 返回 SKIPPED(无可执行操作/业务条件不满足),对应状态机里的 E19→S13 或 E20→S14)
|
||||
PENDING → PRECONDITION_FAILED (Strategy: 前置条件不满足)
|
||||
IN_PROGRESS → SUCCESS (DataSource: 异步轮询成功)
|
||||
IN_PROGRESS → FAILED (DataSource: 异步轮询失败/超时)
|
||||
```
|
||||
|
||||
> **注意**:当前 `SyncNode.set_status()` 不做转换合法性校验【未实现】。
|
||||
|
||||
---
|
||||
|
||||
## 参考文档
|
||||
|
||||
- [状态定义规范](./状态定义规范.md) — 状态枚举和判定规则
|
||||
- [持久化与人工介入规范](./持久化与人工介入规范.md) — 持久化重置策略
|
||||
- [编排流程规范](./编排流程规范.md) — Pipeline 阶段定义
|
||||
- [架构分层设计](./架构分层设计.md) — 各层职责
|
||||
- [node_id_data_id_lifecycle](./node_id_data_id_lifecycle.md) — ID 生命周期
|
||||
Reference in New Issue
Block a user