优化sync_system代码质量

This commit is contained in:
strepsiades
2026-04-01 11:52:57 +08:00
parent eb02fd32a5
commit 8f4727a772
46 changed files with 1431 additions and 890 deletions
+164 -247
View File
@@ -1,4 +1,4 @@
import copy
from abc import ABC, abstractmethod
from typing import TypeVar, Generic, Type, Dict, List, Optional, Any
from pydantic import BaseModel
import logging
@@ -7,31 +7,35 @@ 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 .config import StrategyConfig, OrphanAction, UpdateDirection
from ..config.strategy_config import resolve_domain_option_config
from .strategy_ops import (
get_phase1_reset_defaults,
run_bind,
run_create,
run_delete,
run_phase2_cleanup,
)
from .strategy_ops.update_ops import (
collect_bound_node_pairs,
default_get_node_update_payload,
default_needs_update,
default_update_pair,
prepare_directional_update,
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]):
class BaseSyncStrategy(ABC, Generic[T]):
"""
同步策略基类
纯逻辑层,不负责 IO 执行。
同步策略抽象接口
子类可以通过覆盖 default_config 来设置默认配置:
子类可以通过覆盖以下类变量提供静态配置:
```python
class ProjectStrategy(DefaultSyncStrategy):
default_config = StrategyConfig(
@@ -47,41 +51,122 @@ class BaseSyncStrategy(Generic[T]):
适用于只需要加载数据供其他类型引用的场景(如 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
default_config: StrategyConfig
domain_option_model: Type[BaseModel] | None
schema: Type[T]
node_type: str
config: StrategyConfig
sm_runtime: Optional[StateMachineRuntime]
@abstractmethod
def __init__(
self,
node_type: str,
local_collection: DataCollection,
remote_collection: DataCollection,
self,
node_type: str,
local_collection: DataCollection,
remote_collection: DataCollection,
binding_manager: BindingManager,
):
pass
@abstractmethod
async def bind(self, **kwargs):
"""进行绑定逻辑判定,设置 node.binding_status 和 node.action"""
pass
@abstractmethod
async def create(self) -> List[SyncNode]:
"""准备 CREATE 操作"""
pass
@abstractmethod
async def update(self) -> List[SyncNode]:
"""准备 UPDATE 操作"""
pass
@abstractmethod
async def delete(self) -> List[SyncNode]:
pass
@property
@abstractmethod
def skip_sync(self) -> bool:
pass
@property
@abstractmethod
def skip_post_check(self) -> bool:
pass
@abstractmethod
def normalize_compare_payload(
self,
data: Optional[Dict[str, Any]],
data_id_map: Optional[Dict[str, str]] = None,
) -> Dict[str, Any]:
pass
class DefaultSyncStrategy(BaseSyncStrategy[T]):
"""
基于 STATE_DEFINITIONS.md 实现的标准化同步策略。
采用四阶段管线:核心状态 -> 依赖检查 -> 自动匹配 -> 动作映射。
"""
default_config: StrategyConfig = StrategyConfig()
domain_option_model: Type[BaseModel] | None = None
schema: Type[T] = None # type: ignore
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()
self.config.domain_option = resolve_domain_option_config(
self.domain_option_model,
self.config.domain_option,
)
self.config.log_logic_warnings(node_type=self.node_type, logger=logger)
@classmethod
def get_default_domain_option(cls) -> Dict[str, Any]:
return resolve_domain_option_config(cls.domain_option_model, None)
def _validate_domain_option(self, domain_option: Optional[Dict[str, Any]]) -> BaseModel | Dict[str, Any]:
if self.domain_option_model is None:
return dict(domain_option or {})
return self.domain_option_model.model_validate(domain_option or {})
@property
def domain_option(self) -> BaseModel | Dict[str, Any]:
return self._validate_domain_option(self.config.domain_option)
@property
def skip_sync(self) -> bool:
return bool(self.config.skip_sync)
@skip_sync.setter
def skip_sync(self, value: bool) -> None:
self.config.skip_sync = bool(value)
@property
def skip_post_check(self) -> bool:
return bool(self.config.skip_post_check)
@skip_post_check.setter
def skip_post_check(self, value: bool) -> None:
self.config.skip_post_check = bool(value)
def ensure_runtime(self) -> StateMachineRuntime:
runtime = self.sm_runtime
@@ -93,128 +178,6 @@ class BaseSyncStrategy(Generic[T]):
self.remote_collection.set_state_machine_runtime(runtime)
return runtime
def _validate_config(self) -> None:
"""
校验配置的逻辑一致性。
规则:
- PUSH 模式:local_orphan_action 应为 CREATE_REMOTEremote_orphan_action 应为 NONE
- PULL 模式:local_orphan_action 应为 NONEremote_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]],
@@ -226,103 +189,12 @@ class BaseSyncStrategy(Generic[T]):
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()` 仅恢复持久化状态;
- 这里触发 E01create_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 实现的标准化同步策略。
采用四阶段管线:核心状态 -> 依赖检查 -> 自动匹配 -> 动作映射。
"""
return normalized
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)
@@ -331,6 +203,51 @@ class DefaultSyncStrategy(BaseSyncStrategy[T]):
self.ensure_runtime()
return await run_update(self)
def get_node_update_payload(self, node: SyncNode) -> Dict[str, Any]:
return default_get_node_update_payload(node)
def should_update_pair(
self,
source_node: SyncNode,
target_node: SyncNode,
*,
source_data: Dict[str, Any],
target_data: Dict[str, Any],
data_id_map: Optional[Dict[str, str]] = None,
) -> bool:
return default_needs_update(self, source_data, target_data, data_id_map)
async def collect_update_pairs(self):
return await collect_bound_node_pairs(self)
async def prepare_update_for_direction(
self,
local_node: SyncNode,
remote_node: SyncNode,
direction: UpdateDirection,
data_id_map: Optional[Dict[str, str]] = None,
*,
include_fields: Optional[List[str]] = None,
exclude_fields: Optional[List[str]] = None,
) -> Optional[SyncNode]:
return await prepare_directional_update(
self,
local_node=local_node,
remote_node=remote_node,
direction=direction,
data_id_map=data_id_map,
include_fields=include_fields,
exclude_fields=exclude_fields,
)
async def update_pair(
self,
local_node: SyncNode,
remote_node: SyncNode,
data_id_map: Optional[Dict[str, str]] = None,
) -> List[SyncNode]:
return await default_update_pair(self, local_node, remote_node, data_id_map)
async def delete(self) -> List[SyncNode]:
self.ensure_runtime()
return await run_delete(self)