339 lines
12 KiB
Python
339 lines
12 KiB
Python
import copy
|
||
from typing import TypeVar, Generic, Type, Dict, List, Optional, Any
|
||
from pydantic import BaseModel
|
||
import logging
|
||
from pathlib import Path
|
||
|
||
from ..common.collection import DataCollection
|
||
from ..common.binding import BindingManager
|
||
from ..common.sync_node import SyncNode
|
||
from .config import StrategyConfig, OrphanAction, UpdateDirection, ConfigPresets
|
||
from .strategy_ops import (
|
||
get_phase1_reset_defaults,
|
||
run_bind,
|
||
run_create,
|
||
run_delete,
|
||
run_phase2_cleanup,
|
||
run_update,
|
||
)
|
||
from ..engine import (
|
||
StateMachineConfig,
|
||
StateMachineRuntime,
|
||
)
|
||
from ..validation import SchemaDiffValidator
|
||
from .strategy_ops.compare_ops import normalized_data_for_compare
|
||
|
||
T = TypeVar("T", bound=BaseModel)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class BaseSyncStrategy(Generic[T]):
|
||
"""
|
||
同步策略基类。
|
||
纯逻辑层,不负责 IO 执行。
|
||
|
||
子类可以通过覆盖 default_config 来设置默认配置:
|
||
```python
|
||
class ProjectStrategy(DefaultSyncStrategy):
|
||
default_config = StrategyConfig(
|
||
auto_bind_fields=["name", "code"],
|
||
local_orphan_action=OrphanAction.CREATE_REMOTE
|
||
)
|
||
schema = ProjectResponse
|
||
```
|
||
|
||
跳过同步控制:
|
||
skip_sync: bool - 如果为 True,则跳过该类型的 bind/create/update/sync 阶段
|
||
数据仍会被加载,但不会执行任何同步操作
|
||
适用于只需要加载数据供其他类型引用的场景(如 supplier)
|
||
skip_post_check: bool - 如果为 True,则跳过该类型最终 post-check 的 reload 与一致性校验
|
||
"""
|
||
# 类变量:默认配置(子类可覆盖)
|
||
default_config: StrategyConfig = StrategyConfig()
|
||
# 类变量:schema 类型(子类必须设置)
|
||
schema: Type[T] = None # type: ignore
|
||
# 类变量:是否默认跳过同步(子类可覆盖)
|
||
default_skip_sync: bool = False
|
||
# 类变量:是否默认跳过 post-check(子类可覆盖)
|
||
default_skip_post_check: bool = False
|
||
|
||
def __init__(
|
||
self,
|
||
node_type: str,
|
||
local_collection: DataCollection,
|
||
remote_collection: DataCollection,
|
||
binding_manager: BindingManager,
|
||
):
|
||
if self.schema is None:
|
||
raise ValueError(f"{self.__class__.__name__} must define schema as a class variable")
|
||
|
||
self.node_type = node_type
|
||
self.local_collection = local_collection
|
||
self.remote_collection = remote_collection
|
||
self.binding_manager = binding_manager
|
||
# 复制类级别的默认配置到实例
|
||
self.config = self.default_config.model_copy(deep=True)
|
||
# 是否跳过同步(可运行时修改)
|
||
self.skip_sync = self.default_skip_sync
|
||
# 是否跳过 post-check(可运行时修改)
|
||
self.skip_post_check = self.default_skip_post_check
|
||
# 统一由 Pipeline 注入 runtime;未注入时在执行阶段显式报错
|
||
self.sm_runtime: Optional[StateMachineRuntime] = None
|
||
self._schema_diff_validator: Optional[SchemaDiffValidator] = None
|
||
|
||
# 配置逻辑校验
|
||
self._validate_config()
|
||
|
||
def ensure_runtime(self) -> StateMachineRuntime:
|
||
runtime = self.sm_runtime
|
||
if runtime is None:
|
||
cfg_path = Path(__file__).resolve().parents[1] / "config" / "node_state_machine.yaml"
|
||
runtime = StateMachineRuntime(StateMachineConfig.load(cfg_path))
|
||
self.sm_runtime = runtime
|
||
self.local_collection.set_state_machine_runtime(runtime)
|
||
self.remote_collection.set_state_machine_runtime(runtime)
|
||
return runtime
|
||
|
||
def _validate_config(self) -> None:
|
||
"""
|
||
校验配置的逻辑一致性。
|
||
|
||
规则:
|
||
- PUSH 模式:local_orphan_action 应为 CREATE_REMOTE,remote_orphan_action 应为 NONE
|
||
- PULL 模式:local_orphan_action 应为 NONE,remote_orphan_action 应为 CREATE_LOCAL
|
||
- NONE 模式:两者都应为 NONE
|
||
"""
|
||
direction = self.config.update_direction
|
||
local_action = self.config.local_orphan_action
|
||
remote_action = self.config.remote_orphan_action
|
||
|
||
# PUSH 模式检查
|
||
if direction == UpdateDirection.PUSH:
|
||
if local_action == OrphanAction.CREATE_LOCAL:
|
||
logger.warning(
|
||
f"[{self.node_type}] Config inconsistency: "
|
||
f"update_direction=PUSH but local_orphan_action=CREATE_LOCAL (本地创建)。"
|
||
f"应使用 CREATE_REMOTE (推送到远程)"
|
||
)
|
||
if remote_action == OrphanAction.CREATE_LOCAL:
|
||
logger.warning(
|
||
f"[{self.node_type}] Config inconsistency: "
|
||
f"update_direction=PUSH but remote_orphan_action=CREATE_LOCAL (本地创建)。"
|
||
f"PUSH 模式应设为 NONE 或 DELETE_REMOTE"
|
||
)
|
||
|
||
# PULL 模式检查
|
||
elif direction == UpdateDirection.PULL:
|
||
if local_action == OrphanAction.CREATE_REMOTE:
|
||
logger.warning(
|
||
f"[{self.node_type}] Config inconsistency: "
|
||
f"update_direction=PULL but local_orphan_action=CREATE_REMOTE (推送到远程)。"
|
||
f"PULL 模式应设为 NONE 或 DELETE_LOCAL"
|
||
)
|
||
if remote_action in {OrphanAction.CREATE_REMOTE, OrphanAction.DELETE_LOCAL}:
|
||
logger.warning(
|
||
f"[{self.node_type}] Config inconsistency: "
|
||
f"update_direction=PULL but remote_orphan_action={remote_action}。"
|
||
f"PULL 模式建议使用 CREATE_LOCAL 或 NONE"
|
||
)
|
||
|
||
# NONE 模式检查
|
||
elif direction == UpdateDirection.NONE:
|
||
if local_action != OrphanAction.NONE:
|
||
logger.warning(
|
||
f"[{self.node_type}] Config inconsistency: "
|
||
f"update_direction=NONE but local_orphan_action={local_action}。"
|
||
f"应设为 NONE"
|
||
)
|
||
if remote_action != OrphanAction.NONE:
|
||
logger.warning(
|
||
f"[{self.node_type}] Config inconsistency: "
|
||
f"update_direction=NONE but remote_orphan_action={remote_action}。"
|
||
f"应设为 NONE"
|
||
)
|
||
|
||
def set_config(self, config: StrategyConfig):
|
||
"""完全替换配置对象"""
|
||
self.config = config
|
||
self._schema_diff_validator = None
|
||
self._validate_config()
|
||
|
||
def update_config(self, **kwargs):
|
||
"""
|
||
临时修改部分配置项。
|
||
|
||
Example:
|
||
strategy.update_config(
|
||
local_orphan_action=OrphanAction.NONE,
|
||
update_direction=UpdateDirection.PULL
|
||
)
|
||
"""
|
||
valid_fields = type(self.config).model_fields
|
||
for key, value in kwargs.items():
|
||
if key in valid_fields:
|
||
setattr(self.config, key, value)
|
||
else:
|
||
logger.warning(f"[{self.node_type}] Unknown config key: {key}")
|
||
self._schema_diff_validator = None
|
||
# 更新配置后也要校验
|
||
self._validate_config()
|
||
|
||
def apply_preset(self, preset_name: str):
|
||
"""
|
||
应用配置预设。
|
||
|
||
Args:
|
||
preset_name: 预设名称,可选:first_sync, daily_sync, repair_sync, pull_only, push_only
|
||
|
||
Example:
|
||
strategy.apply_preset("first_sync")
|
||
"""
|
||
preset_config = ConfigPresets.get_preset(preset_name)
|
||
self.config = preset_config
|
||
self._schema_diff_validator = None
|
||
self._validate_config()
|
||
logger.info(f"[{self.node_type}] Applied preset: {preset_name}")
|
||
|
||
def get_schema_diff_validator(self) -> SchemaDiffValidator:
|
||
if self._schema_diff_validator is None:
|
||
self._schema_diff_validator = SchemaDiffValidator(
|
||
schema_name=self.schema.__name__,
|
||
ignore_fields=self.config.compare_ignore_fields,
|
||
ignore_list_item_fields=self.config.post_check_ignore_list_item_fields,
|
||
)
|
||
return self._schema_diff_validator
|
||
|
||
def reset_schema_diff_validator(self) -> None:
|
||
self.get_schema_diff_validator().reset()
|
||
|
||
def emit_schema_diff_report(self) -> None:
|
||
validator = self.get_schema_diff_validator()
|
||
if not validator.has_records():
|
||
return
|
||
for line in validator.format_summary_lines():
|
||
logger.info(f"[{self.node_type}] validate {line}")
|
||
|
||
def preprocess_compare_payload(self, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||
return payload
|
||
|
||
def normalize_compare_payload(
|
||
self,
|
||
data: Optional[Dict[str, Any]],
|
||
data_id_map: Optional[Dict[str, str]] = None,
|
||
) -> Dict[str, Any]:
|
||
normalized = normalized_data_for_compare(
|
||
data,
|
||
data_id_map,
|
||
ignore_fields=set(self.config.compare_ignore_fields),
|
||
ignore_list_item_fields=self.config.post_check_ignore_list_item_fields,
|
||
)
|
||
return self.preprocess_compare_payload(copy.deepcopy(normalized))
|
||
|
||
async def bind(self, **kwargs):
|
||
"""进行绑定逻辑判定,设置 node.binding_status 和 node.action"""
|
||
raise NotImplementedError
|
||
|
||
async def create(self) -> List[SyncNode]:
|
||
"""准备 CREATE 操作"""
|
||
raise NotImplementedError
|
||
|
||
async def update(self) -> List[SyncNode]:
|
||
"""准备 UPDATE 操作"""
|
||
raise NotImplementedError
|
||
|
||
def _needs_update(
|
||
self,
|
||
source_node: SyncNode,
|
||
target_node: SyncNode,
|
||
resolved_data: Optional[Dict[str, Any]] = None,
|
||
data_id_map: Optional[Dict[str, str]] = None,
|
||
) -> bool:
|
||
from .strategy_ops.update_ops import default_needs_update
|
||
|
||
return default_needs_update(self, source_node, target_node, resolved_data, data_id_map)
|
||
|
||
@classmethod
|
||
def get_phase1_reset_defaults(cls) -> Dict[str, Any]:
|
||
return get_phase1_reset_defaults()
|
||
|
||
@classmethod
|
||
async def run_reset(
|
||
cls,
|
||
*,
|
||
node_types: List[str],
|
||
local_collection: DataCollection,
|
||
remote_collection: DataCollection,
|
||
binding_manager: BindingManager,
|
||
runtime: StateMachineRuntime,
|
||
) -> int:
|
||
"""
|
||
执行加载后的重置清理(僵尸 CREATE 节点清理)。
|
||
|
||
说明:
|
||
- `DataCollection.load_from_persistence()` 仅恢复持久化状态;
|
||
- 这里触发 E01:create_zombie -> S15(删除),其余 -> S00。
|
||
"""
|
||
return await run_phase2_cleanup(
|
||
node_types=node_types,
|
||
local_collection=local_collection,
|
||
remote_collection=remote_collection,
|
||
binding_manager=binding_manager,
|
||
runtime=runtime,
|
||
)
|
||
|
||
@classmethod
|
||
async def run_phase2_reset(
|
||
cls,
|
||
*,
|
||
node_types: List[str],
|
||
local_collection: DataCollection,
|
||
remote_collection: DataCollection,
|
||
binding_manager: BindingManager,
|
||
runtime: StateMachineRuntime,
|
||
) -> int:
|
||
"""兼容旧命名,等价于 `run_reset`。"""
|
||
return await cls.run_reset(
|
||
node_types=node_types,
|
||
local_collection=local_collection,
|
||
remote_collection=remote_collection,
|
||
binding_manager=binding_manager,
|
||
runtime=runtime,
|
||
)
|
||
|
||
|
||
class DefaultSyncStrategy(BaseSyncStrategy[T]):
|
||
"""
|
||
基于 STATE_DEFINITIONS.md 实现的标准化同步策略。
|
||
采用四阶段管线:核心状态 -> 依赖检查 -> 自动匹配 -> 动作映射。
|
||
"""
|
||
|
||
async def bind(self, **kwargs):
|
||
self.ensure_runtime()
|
||
await run_bind(self, **kwargs)
|
||
|
||
|
||
|
||
def _get_id_field_hints(self) -> Dict[str, str]:
|
||
"""
|
||
获取 ID 字段的 node_type 提示。
|
||
|
||
默认实现:从 depend_fields 中提取。
|
||
子类可以重写此方法提供更精确的映射。
|
||
|
||
Returns:
|
||
字段名到 node_type 的映射,例如 {"project_id": "project"}
|
||
"""
|
||
return dict(self.config.depend_fields) if self.config.depend_fields else {}
|
||
async def create(self) -> List[SyncNode]:
|
||
self.ensure_runtime()
|
||
return await run_create(self)
|
||
|
||
async def update(self) -> List[SyncNode]:
|
||
self.ensure_runtime()
|
||
return await run_update(self)
|
||
|
||
async def delete(self) -> List[SyncNode]:
|
||
self.ensure_runtime()
|
||
return await run_delete(self)
|
||
|
||
|