Files
ecm_sync_system/sync_state_machine/common/sync_node.py
T
strepsiades 4a9c444b10 first commit
2026-03-09 16:31:42 +08:00

436 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
SyncNode - 核心状态机容器
将data存储为Dict,但支持schema验证和Pydantic模型转换。
提供统一的数据访问接口:get_data() / put_data()
"""
import copy
from contextlib import contextmanager
from typing import TypeVar, Generic, Optional, Any, Dict, List, Type
from pydantic import BaseModel, ValidationError
from .types import SyncAction, SyncStatus, BindingStatus
T = TypeVar("T", bound=BaseModel)
class SyncNode(Generic[T]):
"""
核心状态机容器
数据存储策略:
- origin_data/data 内部存储为 Dict
- 通过 get_data() 获取数据副本(深拷贝)
- 通过 set_data() 设置数据(自动验证+深拷贝)
- 通过 to_model() 转换为 Pydantic 模型实例
基类 SyncNode 不应直接实例化,应通过子类使用。
子类应设置类变量 node_type 和 schema
```python
class ContractSyncNode(SyncNode[ContractResponse]):
node_type = "contract"
schema = ContractResponse
```
"""
# 类变量:子类必须设置
node_type: str = None # type: ignore
schema: Type[T] = None # type: ignore
_CORE_FIELDS = {"binding_status", "action", "status", "data_id"}
def __setattr__(self, name: str, value: Any) -> None:
if (
name in self._CORE_FIELDS
and self.__dict__.get("_core_guard_enabled", False)
and self.__dict__.get("_core_write_depth", 0) <= 0
):
raise RuntimeError(
f"Core field '{name}' can only be modified via event apply path for node={self.__dict__.get('node_id', '<uninitialized>')}"
)
object.__setattr__(self, name, value)
@contextmanager
def allow_core_state_write(self, source: str = "unknown"):
depth = self.__dict__.get("_core_write_depth", 0)
prev_source = self.__dict__.get("_core_write_source", "")
object.__setattr__(self, "_core_write_depth", depth + 1)
object.__setattr__(self, "_core_write_source", source)
try:
yield
finally:
object.__setattr__(self, "_core_write_depth", depth)
object.__setattr__(self, "_core_write_source", prev_source)
def __init__(
self,
node_id: str,
data_id: str = "",
depend_ids: Optional[List[str]] = None,
data: Optional[Dict[str, Any]] = None,
origin_data: Optional[Dict[str, Any]] = None,
action: SyncAction = SyncAction.NONE,
status: SyncStatus = SyncStatus.PENDING,
binding_status: BindingStatus = BindingStatus.UNCHECKED,
error: Optional[str] = None,
context: Optional[Dict[str, Any]] = None,
sync_log: Optional[str] = None
):
object.__setattr__(self, "_core_guard_enabled", False)
object.__setattr__(self, "_core_write_depth", 0)
# 检查类变量是否设置
if self.node_type is None:
raise ValueError(
f"{self.__class__.__name__} must define class variable 'node_type'. "
f"Example: node_type = 'contract'"
)
if self.schema is None:
raise ValueError(
f"{self.__class__.__name__} must define class variable 'schema'. "
f"Example: schema = YourSchema"
)
self.node_id = node_id
self.data_id = data_id
self.depend_ids = depend_ids if depend_ids is not None else []
if origin_data is not None:
self.origin_data = copy.deepcopy(origin_data)
else:
self.origin_data = None
self.action = action
self.status = status
self.binding_status = binding_status
self.error = error
self.sync_log = sync_log # Debug/info级别的日志信息
# Context 字典:存储额外的上下文信息(如 project_id, contract_id 等)
self.context: Dict[str, Any] = context if context is not None else {}
# 在初始化时验证并设置 data
if data is not None:
self._validate_and_set_data(data)
if self.origin_data is None:
self.origin_data = copy.deepcopy(self.data)
else:
self.data = None
object.__setattr__(self, "_core_guard_enabled", True)
def _validate_and_set_data(self, data: Dict[str, Any]) -> None:
"""验证并设置数据(内部方法)"""
if not isinstance(data, dict):
self.data = None
raise ValueError(f"Data validation failed for {self.node_type}: data must be a dict")
try:
# 基础兼容性:将 _id 转换为 id
data_to_validate = data.copy()
if "id" not in data_to_validate and "_id" in data_to_validate:
data_to_validate["id"] = data_to_validate["_id"]
model = self.schema.model_validate(data_to_validate)
# 使用 model_dump() 获得完整校验后的数据,去掉 schema 之外的字段
self.data = model.model_dump()
except Exception as e:
# 如果出错就存 None, 然后 error 里加个错
self.data = None
self.error = f"Validation failed: {str(e)}"
# 不要轻易降级错误。如果校验失败,应抛出 ValueError 供上层处理
raise ValueError(f"Data validation failed for {self.node_type}: {e}")
# ===== 数据访问方法 =====
def get_data(self, exclude_unset: bool = False) -> Optional[Dict[str, Any]]:
"""
获取数据的副本。
Args:
exclude_unset: 是否排除未设置(默认值)的字段。
Returns:
数据字典的副本,如果为None则返回None
"""
if self.data is None:
return None
if not exclude_unset:
return copy.deepcopy(self.data)
try:
model = self.schema.model_validate(self.data)
return model.model_dump(exclude_unset=True)
except Exception:
return copy.deepcopy(self.data)
def set_data(self, data: Optional[Dict[str, Any]], validate: bool = True) -> None:
"""
设置数据(自动深拷贝)。
Args:
data: 要设置的数据字典
validate: 是否进行schema验证(默认True
Raises:
ValueError: 如果数据验证失败
"""
if data is None:
self.data = None
return
if validate:
self._validate_and_set_data(data)
else:
self.data = copy.deepcopy(data)
def get_origin_data(self) -> Optional[Dict[str, Any]]:
"""
获取原始数据的深拷贝副本。
Returns:
原始数据字典的副本,如果为None则返回None
"""
if self.origin_data is None:
return None
return copy.deepcopy(self.origin_data)
def set_origin_data(self, data: Optional[Dict[str, Any]]) -> None:
"""
设置原始数据(自动深拷贝)。
Args:
data: 要设置的原始数据字典
"""
if data is None:
self.origin_data = None
return
if self.schema is None:
raise ValueError(f"Cannot set origin_data without schema for {self.node_type}")
try:
model = self.schema.model_validate(copy.deepcopy(data))
# 使用 exclude_unset=True 确保 origin_data 也只记录实际提供/返回的字段
self.origin_data = model.model_dump(exclude_unset=True)
except AttributeError as e:
raise ValueError(
f"Origin data validation failed for {self.node_type}: model_dump not available"
) from e
except (ValidationError, TypeError, ValueError) as e:
raise ValueError(f"Origin data validation failed for {self.node_type}: {e}") from e
# ===== Schema 相关方法 =====
def get_schema(self) -> Optional[Type[T]]:
"""
获取数据的 Schema 类型。
Returns:
Pydantic Model 类型,如果未设置则返回 None
"""
return self.schema
def set_schema(self, schema: Type[T]) -> None:
"""
设置数据的 Schema 类型。
Args:
schema: Pydantic Model 类型
"""
self.schema = schema
def validate(self) -> bool:
"""
验证当前数据是否符合 schema。
Returns:
True 如果验证通过或无 schemaFalse 如果验证失败
"""
if self.schema is None or self.data is None:
return True
try:
self.schema.model_validate(self.data)
return True
except ValidationError:
return False
def to_model(self) -> Optional[T]:
"""
将数据转换为 Pydantic 模型实例。
Returns:
模型实例,如果无数据或无schema则返回None
Raises:
ValidationError: 如果数据验证失败
"""
if self.schema is None or self.data is None:
return None
return self.schema.model_validate(self.data)
def from_model(self, model: T) -> None:
"""
从 Pydantic 模型实例设置数据。
Args:
model: Pydantic 模型实例
"""
try:
self.data = model.model_dump()
except AttributeError as e:
raise ValueError(
f"Cannot convert model to dict: model_dump not available ({type(model)})"
) from e
# 自动设置schema
if self.schema is None:
self.schema = type(model)
# ===== 辅助方法 =====
def get_field(self, field_name: str, default: Any = None) -> Any:
"""
从data中获取指定字段的值。
Args:
field_name: 字段名
default: 默认值
Returns:
字段值,如果不存在则返回default
"""
if self.data is None:
return default
return self.data.get(field_name, default)
def set_field(self, field_name: str, value: Any) -> None:
"""
设置data中的指定字段。
Args:
field_name: 字段名
value: 字段值
"""
if self.data is None:
self.data = {}
self.data[field_name] = value
def has_field(self, field_name: str) -> bool:
"""
检查data中是否存在指定字段。
Args:
field_name: 字段名
Returns:
True 如果字段存在
"""
if self.data is None:
return False
return field_name in self.data
# ===== 状态管理方法(带日志记录) =====
def append_log(self, message: str) -> None:
"""追加日志到 sync_log(中文)"""
import time
from datetime import datetime
timestamp = datetime.fromtimestamp(time.time()).strftime("%H:%M:%S")
log_entry = f"[{timestamp}] {message}"
if self.sync_log:
self.sync_log += "\n" + log_entry
else:
self.sync_log = log_entry
def set_status(self, new_status: SyncStatus, reason: str = "") -> None:
"""
设置status并更新时间戳和日志(如果状态改变)
Args:
new_status: 新的执行状态
reason: 状态变化原因(中文)
"""
if self.status != new_status:
old_status = self.status
self.status = new_status
reason_text = f" - {reason}" if reason else ""
# DEBUG 日志
import logging
logger = logging.getLogger(__name__)
logger.debug(
f"[{self.node_type}] 节点 {self.node_id}: "
f"执行状态 {old_status.value}{new_status.value}{reason_text}"
)
def set_data_id(self, new_data_id: str, reason: str = "") -> None:
"""
设置 data_id(仅允许在 event apply 路径内调用)
"""
if self.data_id != new_data_id:
old_data_id = self.data_id
self.data_id = new_data_id
reason_text = f" - {reason}" if reason else ""
self.append_log(f"数据ID: {old_data_id or '<empty>'}{new_data_id or '<empty>'}{reason_text}")
import logging
logger = logging.getLogger(__name__)
logger.debug(
f"[{self.node_type}] 节点 {self.node_id}: "
f"data_id {old_data_id or '<empty>'}{new_data_id or '<empty>'}{reason_text}"
)
def set_binding_status(self, new_status: BindingStatus, reason: str = "") -> None:
"""
设置binding_status并更新时间戳和日志(如果状态改变)
Args:
new_status: 新的绑定状态
reason: 状态变化原因(中文)
"""
if self.binding_status != new_status:
if self.__dict__.get("_core_guard_enabled", False):
write_source = self.__dict__.get("_core_write_source", "")
if write_source != "state_machine":
raise RuntimeError(
f"binding_status update must go through state machine for node={self.node_id}, source={write_source or '<none>'}"
)
old_status = self.binding_status
self.binding_status = new_status
reason_text = reason or "<no-reason>"
# DEBUG 日志
import logging
logger = logging.getLogger(__name__)
logger.debug(
f"[{self.node_type}] 节点 {self.node_id}: "
f"绑定状态 {old_status.value}{new_status.value} - {reason_text}"
)
def set_action(self, new_action: SyncAction, reason: str = "") -> None:
"""
设置action并更新时间戳和日志(如果动作改变)
Args:
new_action: 新的同步动作
reason: 动作变化原因(中文)
"""
if self.action != new_action:
old_action = self.action
self.action = new_action
reason_text = f" - {reason}" if reason else ""
# DEBUG 日志
import logging
logger = logging.getLogger(__name__)
logger.debug(
f"[{self.node_type}] 节点 {self.node_id}: "
f"同步动作 {old_action.value}{new_action.value}{reason_text}"
)