修改多个问题,优化代码结构
This commit is contained in:
@@ -1,35 +1,45 @@
|
||||
from abc import ABC, abstractmethod
|
||||
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 ..logging import get_logger
|
||||
from .config import StrategyConfig, OrphanAction, UpdateDirection
|
||||
from ..config.strategy_config import resolve_domain_option_config
|
||||
from .strategy_ops import (
|
||||
run_bind,
|
||||
run_create,
|
||||
run_delete,
|
||||
)
|
||||
from .strategy_ops.bind_ops import (
|
||||
apply_auto_bind_updates,
|
||||
collect_auto_bind_ready_nodes,
|
||||
collect_bind_entry_nodes,
|
||||
plan_auto_bind_updates,
|
||||
phase_core_state,
|
||||
phase_dependency_check,
|
||||
refresh_bind_data_id,
|
||||
)
|
||||
from .strategy_ops.create_ops import add_create_action
|
||||
from .strategy_ops.update_ops import (
|
||||
BoundNodePair,
|
||||
_build_schema_diff_validator,
|
||||
collect_bound_node_pairs,
|
||||
default_get_node_update_payload,
|
||||
default_needs_update,
|
||||
default_update_pair,
|
||||
emit_schema_diff_report,
|
||||
prepare_directional_update,
|
||||
run_update,
|
||||
)
|
||||
from ..engine import (
|
||||
StateMachineConfig,
|
||||
StateMachineRuntime,
|
||||
)
|
||||
from .strategy_ops.compare_ops import normalized_data_for_compare
|
||||
from .strategy_ops.compare_ops import build_data_id_normalization_map, normalized_data_for_compare
|
||||
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class BaseSyncStrategy(ABC, Generic[T]):
|
||||
"""
|
||||
@@ -69,7 +79,7 @@ class BaseSyncStrategy(ABC, Generic[T]):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
async def bind(self, **kwargs):
|
||||
async def bind(self):
|
||||
"""进行绑定逻辑判定,设置 node.binding_status 和 node.action"""
|
||||
pass
|
||||
|
||||
@@ -191,17 +201,124 @@ class DefaultSyncStrategy(BaseSyncStrategy[T]):
|
||||
)
|
||||
return normalized
|
||||
|
||||
async def bind(self, **kwargs):
|
||||
self.ensure_runtime()
|
||||
await run_bind(self, **kwargs)
|
||||
async def bind(self):
|
||||
runtime = self.ensure_runtime()
|
||||
bind_local_nodes, bind_remote_nodes = collect_bind_entry_nodes(
|
||||
node_type=self.node_type,
|
||||
local_collection=self.local_collection,
|
||||
remote_collection=self.remote_collection,
|
||||
)
|
||||
await phase_core_state(
|
||||
node_type=self.node_type,
|
||||
runtime=runtime,
|
||||
binding_manager=self.binding_manager,
|
||||
local_nodes=bind_local_nodes,
|
||||
remote_nodes=bind_remote_nodes,
|
||||
)
|
||||
await phase_dependency_check(
|
||||
node_type=self.node_type,
|
||||
runtime=runtime,
|
||||
depend_fields=dict(self.config.depend_fields),
|
||||
local_nodes=bind_local_nodes,
|
||||
remote_nodes=bind_remote_nodes,
|
||||
local_collection=self.local_collection,
|
||||
remote_collection=self.remote_collection,
|
||||
)
|
||||
auto_bind_local_nodes, auto_bind_remote_nodes = collect_auto_bind_ready_nodes(
|
||||
node_type=self.node_type,
|
||||
local_collection=self.local_collection,
|
||||
remote_collection=self.remote_collection,
|
||||
)
|
||||
local_create_enabled = self.config.local_orphan_action == OrphanAction.CREATE_REMOTE
|
||||
remote_create_enabled = self.config.remote_orphan_action == OrphanAction.CREATE_LOCAL
|
||||
local_orphan_create_enabled_by_node = {
|
||||
node.node_id: local_create_enabled for node in auto_bind_local_nodes
|
||||
}
|
||||
remote_orphan_create_enabled_by_node = {
|
||||
node.node_id: remote_create_enabled for node in auto_bind_remote_nodes
|
||||
}
|
||||
pending_updates = await plan_auto_bind_updates(
|
||||
node_type=self.node_type,
|
||||
config=self.config,
|
||||
local_collection=self.local_collection,
|
||||
remote_collection=self.remote_collection,
|
||||
binding_manager=self.binding_manager,
|
||||
local_nodes=auto_bind_local_nodes,
|
||||
remote_nodes=auto_bind_remote_nodes,
|
||||
id_field_hints=dict(self.config.depend_fields),
|
||||
local_orphan_create_enabled_by_node=local_orphan_create_enabled_by_node,
|
||||
remote_orphan_create_enabled_by_node=remote_orphan_create_enabled_by_node,
|
||||
)
|
||||
await apply_auto_bind_updates(
|
||||
node_type=self.node_type,
|
||||
runtime=runtime,
|
||||
binding_manager=self.binding_manager,
|
||||
pending_auto_bind_updates=pending_updates,
|
||||
)
|
||||
await refresh_bind_data_id(
|
||||
node_type=self.node_type,
|
||||
local_collection=self.local_collection,
|
||||
remote_collection=self.remote_collection,
|
||||
binding_manager=self.binding_manager,
|
||||
local_node_ids={
|
||||
*(node.node_id for node in bind_local_nodes),
|
||||
*(node.node_id for node in auto_bind_local_nodes),
|
||||
},
|
||||
remote_node_ids={
|
||||
*(node.node_id for node in bind_remote_nodes),
|
||||
*(node.node_id for node in auto_bind_remote_nodes),
|
||||
},
|
||||
)
|
||||
|
||||
async def create(self) -> List[SyncNode]:
|
||||
self.ensure_runtime()
|
||||
return await run_create(self)
|
||||
created_nodes: List[SyncNode] = []
|
||||
created_nodes.extend(await self.create_for_direction(source_is_local=True))
|
||||
created_nodes.extend(await self.create_for_direction(source_is_local=False))
|
||||
return created_nodes
|
||||
|
||||
async def create_for_direction(self, *, source_is_local: bool) -> List[SyncNode]:
|
||||
from_collection = self.local_collection if source_is_local else self.remote_collection
|
||||
to_collection = self.remote_collection if source_is_local else self.local_collection
|
||||
return await add_create_action(
|
||||
self,
|
||||
from_collection=from_collection,
|
||||
to_collection=to_collection,
|
||||
source_is_local=source_is_local,
|
||||
)
|
||||
|
||||
async def update(self) -> List[SyncNode]:
|
||||
self.ensure_runtime()
|
||||
return await run_update(self)
|
||||
validator = _build_schema_diff_validator(self)
|
||||
self._active_schema_diff_validator = validator
|
||||
data_id_map = await self.build_update_data_id_map()
|
||||
try:
|
||||
pairs = await self.collect_update_pairs()
|
||||
updated_by_id = await self.process_update_pairs(pairs, data_id_map)
|
||||
finally:
|
||||
self._active_schema_diff_validator = None
|
||||
|
||||
emit_schema_diff_report(self, validator)
|
||||
return list(updated_by_id.values())
|
||||
|
||||
async def build_update_data_id_map(self) -> Dict[str, str]:
|
||||
return await build_data_id_normalization_map(
|
||||
node_type=self.node_type,
|
||||
local_collection=self.local_collection,
|
||||
remote_collection=self.remote_collection,
|
||||
binding_manager=self.binding_manager,
|
||||
)
|
||||
|
||||
async def process_update_pairs(
|
||||
self,
|
||||
pairs: List[BoundNodePair],
|
||||
data_id_map: Optional[Dict[str, str]] = None,
|
||||
) -> Dict[str, SyncNode]:
|
||||
updated_by_id: Dict[str, SyncNode] = {}
|
||||
for pair in pairs:
|
||||
for updated_node in await self.update_pair(pair.local_node, pair.remote_node, data_id_map):
|
||||
updated_by_id[updated_node.node_id] = updated_node
|
||||
return updated_by_id
|
||||
|
||||
def get_node_update_payload(self, node: SyncNode) -> Dict[str, Any]:
|
||||
return default_get_node_update_payload(node)
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
from .create_ops import add_create_action, run_create, finalize_peer_creating_sources
|
||||
from .create_ops import add_create_action, finalize_peer_creating_sources
|
||||
from .update_ops import (
|
||||
BoundNodePair,
|
||||
add_update_action,
|
||||
collect_bound_node_pairs,
|
||||
prepare_directional_update,
|
||||
run_update,
|
||||
)
|
||||
from .delete_ops import run_delete
|
||||
from .bind_ops import (
|
||||
run_bind,
|
||||
phase_core_state,
|
||||
phase_dependency_check,
|
||||
check_dependencies_for_nodes,
|
||||
phase_auto_binding,
|
||||
check_dependencies_for_nodes,
|
||||
check_dependency_satisfied,
|
||||
collect_dependency_errors,
|
||||
)
|
||||
@@ -20,19 +18,16 @@ from .reset_ops import run_phase2_cleanup
|
||||
|
||||
__all__ = [
|
||||
"add_create_action",
|
||||
"run_create",
|
||||
"finalize_peer_creating_sources",
|
||||
"BoundNodePair",
|
||||
"add_update_action",
|
||||
"collect_bound_node_pairs",
|
||||
"prepare_directional_update",
|
||||
"run_update",
|
||||
"run_delete",
|
||||
"run_bind",
|
||||
"phase_core_state",
|
||||
"phase_dependency_check",
|
||||
"check_dependencies_for_nodes",
|
||||
"phase_auto_binding",
|
||||
"check_dependencies_for_nodes",
|
||||
"check_dependency_satisfied",
|
||||
"collect_dependency_errors",
|
||||
"run_phase2_cleanup",
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Mapping, Optional, Set, Tuple
|
||||
|
||||
from ...common.types import BindingStatus
|
||||
from ...sync_system.config import OrphanAction
|
||||
from ...logging import get_logger
|
||||
from ...sync_system.resolve_ids import IDResolver
|
||||
from ...sync_system.utils import extract_biz_key_dict, dict_to_biz_key_tuple, resolve_id_fields_in_key_dict
|
||||
from ...engine import e10_bind_core, decision_note, e15_bind_dependency, e16_bind_auto
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...common.binding import BindingManager
|
||||
from ...common.collection import DataCollection
|
||||
from ...common.sync_node import SyncNode
|
||||
from ...config.strategy_config import StrategyConfig
|
||||
from ...engine import StateMachineRuntime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def get_id_field_hints(strategy) -> Dict[str, str]:
|
||||
return dict(strategy.config.depend_fields) if strategy.config.depend_fields else {}
|
||||
@dataclass(frozen=True)
|
||||
class PendingAutoBindUpdate:
|
||||
node: "SyncNode"
|
||||
outcome: Optional[str]
|
||||
create_enabled: Optional[bool]
|
||||
bind_remote_node_id: Optional[str] = None
|
||||
detail: Optional[str] = None
|
||||
|
||||
|
||||
def _stable_pick_bind_pair(local_candidates: List["SyncNode"], remote_candidates: List["SyncNode"]):
|
||||
@@ -26,55 +34,118 @@ def _stable_pick_bind_pair(local_candidates: List["SyncNode"], remote_candidates
|
||||
return local_node, remote_node
|
||||
|
||||
|
||||
async def run_bind(strategy, **kwargs):
|
||||
logger.info(f"[{strategy.node_type}] Starting Binding Pipeline...")
|
||||
|
||||
local_nodes: List["SyncNode"] = strategy.local_collection.filter_by_state_ids(
|
||||
node_type=strategy.node_type,
|
||||
def collect_bind_entry_nodes(
|
||||
*,
|
||||
node_type: str,
|
||||
local_collection: "DataCollection",
|
||||
remote_collection: "DataCollection",
|
||||
) -> tuple[List["SyncNode"], List["SyncNode"]]:
|
||||
local_nodes = local_collection.filter_by_state_ids(
|
||||
node_type=node_type,
|
||||
state_ids=["S00"],
|
||||
)
|
||||
remote_nodes: List["SyncNode"] = strategy.remote_collection.filter_by_state_ids(
|
||||
node_type=strategy.node_type,
|
||||
remote_nodes = remote_collection.filter_by_state_ids(
|
||||
node_type=node_type,
|
||||
state_ids=["S00"],
|
||||
)
|
||||
|
||||
await phase_core_state(strategy, local_nodes, remote_nodes)
|
||||
await phase_dependency_check(strategy, local_nodes, remote_nodes)
|
||||
await phase_auto_binding(strategy, local_nodes, remote_nodes)
|
||||
await refresh_bind_data_id(strategy)
|
||||
|
||||
logger.info(f"[{strategy.node_type}] Binding Pipeline Completed.")
|
||||
return local_nodes, remote_nodes
|
||||
|
||||
|
||||
async def refresh_bind_data_id(strategy) -> None:
|
||||
binding_pairs = await strategy.binding_manager.get_all_bindings(strategy.node_type)
|
||||
def collect_auto_bind_ready_nodes(
|
||||
*,
|
||||
node_type: str,
|
||||
local_collection: "DataCollection",
|
||||
remote_collection: "DataCollection",
|
||||
) -> tuple[List["SyncNode"], List["SyncNode"]]:
|
||||
local_nodes = [
|
||||
node
|
||||
for node in local_collection.filter_by_state_ids(
|
||||
node_type=node_type,
|
||||
state_ids=["S02"],
|
||||
)
|
||||
if node.binding_status == BindingStatus.MISSING
|
||||
]
|
||||
remote_nodes = [
|
||||
node
|
||||
for node in remote_collection.filter_by_state_ids(
|
||||
node_type=node_type,
|
||||
state_ids=["S02"],
|
||||
)
|
||||
if node.binding_status == BindingStatus.MISSING
|
||||
]
|
||||
return local_nodes, remote_nodes
|
||||
|
||||
|
||||
async def refresh_bind_data_id(
|
||||
*,
|
||||
node_type: str,
|
||||
local_collection: "DataCollection",
|
||||
remote_collection: "DataCollection",
|
||||
binding_manager: "BindingManager",
|
||||
local_node_ids: Optional[Set[str]] = None,
|
||||
remote_node_ids: Optional[Set[str]] = None,
|
||||
) -> None:
|
||||
binding_pairs = await binding_manager.get_all_bindings(node_type)
|
||||
local_to_remote = {local_id: remote_id for local_id, remote_id in binding_pairs if local_id}
|
||||
remote_to_local = {remote_id: local_id for local_id, remote_id in binding_pairs if local_id and remote_id}
|
||||
|
||||
for local_node in strategy.local_collection.filter(node_type=strategy.node_type):
|
||||
if local_node_ids is None:
|
||||
target_local_node_ids = {node.node_id for node in local_collection.filter(node_type=node_type)}
|
||||
else:
|
||||
target_local_node_ids = {node_id for node_id in local_node_ids if node_id}
|
||||
|
||||
if remote_node_ids is None:
|
||||
target_remote_node_ids = {node.node_id for node in remote_collection.filter(node_type=node_type)}
|
||||
else:
|
||||
target_remote_node_ids = {node_id for node_id in remote_node_ids if node_id}
|
||||
|
||||
for local_node_id in list(target_local_node_ids):
|
||||
remote_node_id = local_to_remote.get(local_node_id)
|
||||
if remote_node_id:
|
||||
target_remote_node_ids.add(remote_node_id)
|
||||
|
||||
for remote_node_id in list(target_remote_node_ids):
|
||||
local_node_id = remote_to_local.get(remote_node_id)
|
||||
if local_node_id:
|
||||
target_local_node_ids.add(local_node_id)
|
||||
|
||||
for local_node_id in target_local_node_ids:
|
||||
local_node = local_collection.get_by_node_id(local_node_id)
|
||||
if local_node is None:
|
||||
continue
|
||||
remote_node_id = local_to_remote.get(local_node.node_id)
|
||||
remote_node = strategy.remote_collection.get_by_node_id(remote_node_id) if remote_node_id else None
|
||||
remote_node = remote_collection.get_by_node_id(remote_node_id) if remote_node_id else None
|
||||
if remote_node and remote_node.data_id:
|
||||
local_node.context["bind_data_id"] = remote_node.data_id
|
||||
else:
|
||||
local_node.context.pop("bind_data_id", None)
|
||||
|
||||
for remote_node in strategy.remote_collection.filter(node_type=strategy.node_type):
|
||||
for remote_node_id in target_remote_node_ids:
|
||||
remote_node = remote_collection.get_by_node_id(remote_node_id)
|
||||
if remote_node is None:
|
||||
continue
|
||||
local_node_id = remote_to_local.get(remote_node.node_id)
|
||||
local_node = strategy.local_collection.get_by_node_id(local_node_id) if local_node_id else None
|
||||
local_node = local_collection.get_by_node_id(local_node_id) if local_node_id else None
|
||||
if local_node and local_node.data_id:
|
||||
remote_node.context["bind_data_id"] = local_node.data_id
|
||||
else:
|
||||
remote_node.context.pop("bind_data_id", None)
|
||||
|
||||
|
||||
async def phase_core_state(strategy, local_nodes: List["SyncNode"], remote_nodes: List["SyncNode"]):
|
||||
async def phase_core_state(
|
||||
*,
|
||||
node_type: str,
|
||||
runtime: "StateMachineRuntime",
|
||||
binding_manager: "BindingManager",
|
||||
local_nodes: List["SyncNode"],
|
||||
remote_nodes: List["SyncNode"],
|
||||
) -> None:
|
||||
remote_map = {n.node_id: n for n in remote_nodes}
|
||||
processed_local_ids: Set[str] = set()
|
||||
processed_remote_ids: Set[str] = set()
|
||||
|
||||
for node in local_nodes:
|
||||
remote_id = await strategy.binding_manager.get_remote_id(strategy.node_type, node.node_id)
|
||||
remote_id = await binding_manager.get_remote_id(node_type, node.node_id)
|
||||
|
||||
if remote_id:
|
||||
remote_node = remote_map.get(remote_id)
|
||||
@@ -84,7 +155,7 @@ async def phase_core_state(strategy, local_nodes: List["SyncNode"], remote_nodes
|
||||
local_valid = local_data is not None
|
||||
peer_valid = remote_data is not None
|
||||
local_decision = e10_bind_core(
|
||||
strategy.ensure_runtime(),
|
||||
runtime,
|
||||
node=node,
|
||||
has_binding_record=True,
|
||||
local_data=local_data,
|
||||
@@ -94,14 +165,14 @@ async def phase_core_state(strategy, local_nodes: List["SyncNode"], remote_nodes
|
||||
)
|
||||
local_reason = decision_note(local_decision)
|
||||
if local_decision.to_state in {"S03", "S04"}:
|
||||
logger.debug(f"[{strategy.node_type}] 核心绑定阻断: node={node.node_id}, reason={local_reason}")
|
||||
logger.debug(f"[{node_type}] 核心绑定阻断: node={node.node_id}, reason={local_reason}")
|
||||
else:
|
||||
logger.debug(f"[{strategy.node_type}] 核心绑定完成: node={node.node_id}, reason={local_reason}")
|
||||
logger.debug(f"[{node_type}] 核心绑定完成: node={node.node_id}, reason={local_reason}")
|
||||
processed_local_ids.add(node.node_id)
|
||||
|
||||
if remote_node:
|
||||
peer_decision = e10_bind_core(
|
||||
strategy.ensure_runtime(),
|
||||
runtime,
|
||||
node=remote_node,
|
||||
has_binding_record=True,
|
||||
local_data=remote_data,
|
||||
@@ -111,32 +182,22 @@ async def phase_core_state(strategy, local_nodes: List["SyncNode"], remote_nodes
|
||||
)
|
||||
peer_reason = decision_note(peer_decision)
|
||||
if peer_decision.to_state in {"S03", "S04"}:
|
||||
logger.debug(
|
||||
f"[{strategy.node_type}] 核心绑定阻断: node={remote_node.node_id}, reason={peer_reason}"
|
||||
)
|
||||
logger.debug(f"[{node_type}] 核心绑定阻断: node={remote_node.node_id}, reason={peer_reason}")
|
||||
else:
|
||||
logger.debug(f"[{strategy.node_type}] 核心绑定完成: node={remote_node.node_id}, reason={peer_reason}")
|
||||
logger.debug(f"[{node_type}] 核心绑定完成: node={remote_node.node_id}, reason={peer_reason}")
|
||||
processed_remote_ids.add(remote_node.node_id)
|
||||
|
||||
for node in local_nodes:
|
||||
if node.node_id not in processed_local_ids:
|
||||
decision = e10_bind_core(
|
||||
strategy.ensure_runtime(),
|
||||
node=node,
|
||||
has_binding_record=False,
|
||||
)
|
||||
decision = e10_bind_core(runtime, node=node, has_binding_record=False)
|
||||
reason = decision_note(decision)
|
||||
logger.debug(f"[{strategy.node_type}] 核心绑定完成: node={node.node_id}, reason={reason}")
|
||||
logger.debug(f"[{node_type}] 核心绑定完成: node={node.node_id}, reason={reason}")
|
||||
|
||||
for r_node in remote_nodes:
|
||||
if r_node.node_id not in processed_remote_ids:
|
||||
decision = e10_bind_core(
|
||||
strategy.ensure_runtime(),
|
||||
node=r_node,
|
||||
has_binding_record=False,
|
||||
)
|
||||
for remote_node in remote_nodes:
|
||||
if remote_node.node_id not in processed_remote_ids:
|
||||
decision = e10_bind_core(runtime, node=remote_node, has_binding_record=False)
|
||||
reason = decision_note(decision)
|
||||
logger.debug(f"[{strategy.node_type}] 核心绑定完成: node={r_node.node_id}, reason={reason}")
|
||||
logger.debug(f"[{node_type}] 核心绑定完成: node={remote_node.node_id}, reason={reason}")
|
||||
|
||||
|
||||
def check_dependency_satisfied(
|
||||
@@ -206,17 +267,45 @@ def collect_dependency_errors(
|
||||
return errors
|
||||
|
||||
|
||||
async def phase_dependency_check(strategy, local_nodes: List["SyncNode"], remote_nodes: List["SyncNode"]):
|
||||
await check_dependencies_for_nodes(strategy, local_nodes, strategy.local_collection)
|
||||
await check_dependencies_for_nodes(strategy, remote_nodes, strategy.remote_collection)
|
||||
async def phase_dependency_check(
|
||||
*,
|
||||
node_type: str,
|
||||
runtime: "StateMachineRuntime",
|
||||
depend_fields: Mapping[str, str],
|
||||
local_nodes: List["SyncNode"],
|
||||
remote_nodes: List["SyncNode"],
|
||||
local_collection: "DataCollection",
|
||||
remote_collection: "DataCollection",
|
||||
) -> None:
|
||||
await check_dependencies_for_nodes(
|
||||
node_type=node_type,
|
||||
runtime=runtime,
|
||||
depend_fields=depend_fields,
|
||||
nodes=local_nodes,
|
||||
collection=local_collection,
|
||||
)
|
||||
await check_dependencies_for_nodes(
|
||||
node_type=node_type,
|
||||
runtime=runtime,
|
||||
depend_fields=depend_fields,
|
||||
nodes=remote_nodes,
|
||||
collection=remote_collection,
|
||||
)
|
||||
|
||||
|
||||
async def check_dependencies_for_nodes(strategy, nodes: List["SyncNode"], collection: "DataCollection"):
|
||||
async def check_dependencies_for_nodes(
|
||||
*,
|
||||
node_type: str,
|
||||
runtime: "StateMachineRuntime",
|
||||
depend_fields: Mapping[str, str],
|
||||
nodes: List["SyncNode"],
|
||||
collection: "DataCollection",
|
||||
) -> None:
|
||||
for node in nodes:
|
||||
if node.binding_status != BindingStatus.MISSING:
|
||||
continue
|
||||
|
||||
has_depend_fields = bool(strategy.config.depend_fields)
|
||||
has_depend_fields = bool(depend_fields)
|
||||
node_data = node.get_data()
|
||||
data_present = bool(node_data)
|
||||
|
||||
@@ -228,7 +317,7 @@ async def check_dependencies_for_nodes(strategy, nodes: List["SyncNode"], collec
|
||||
semantic_errors: List[str] = []
|
||||
|
||||
if has_depend_fields and data_present:
|
||||
for field_name, dep_node_type in strategy.config.depend_fields.items():
|
||||
for field_name, dep_node_type in depend_fields.items():
|
||||
dep_data_values = _normalize_dependency_field_values(node_data.get(field_name))
|
||||
if not dep_data_values:
|
||||
continue
|
||||
@@ -282,7 +371,7 @@ async def check_dependencies_for_nodes(strategy, nodes: List["SyncNode"], collec
|
||||
)
|
||||
|
||||
decision = e15_bind_dependency(
|
||||
strategy.ensure_runtime(),
|
||||
runtime,
|
||||
node=node,
|
||||
data_present=data_present,
|
||||
has_depend_fields=has_depend_fields,
|
||||
@@ -293,34 +382,77 @@ async def check_dependencies_for_nodes(strategy, nodes: List["SyncNode"], collec
|
||||
reason = decision_note(decision)
|
||||
if decision.to_state in {"S03", "S04", "S05"}:
|
||||
detail_reason = node.error or reason
|
||||
logger.debug(f"[{strategy.node_type}] 依赖检查阻断: node={node.node_id}, reason={detail_reason}")
|
||||
logger.debug(f"[{node_type}] 依赖检查阻断: node={node.node_id}, reason={detail_reason}")
|
||||
|
||||
|
||||
async def phase_auto_binding(strategy, local_nodes: List["SyncNode"], remote_nodes: List["SyncNode"]):
|
||||
pending_auto_bind_updates: List[Dict[str, Any]] = []
|
||||
async def phase_auto_binding(
|
||||
*,
|
||||
node_type: str,
|
||||
runtime: "StateMachineRuntime",
|
||||
config: "StrategyConfig",
|
||||
local_collection: "DataCollection",
|
||||
remote_collection: "DataCollection",
|
||||
binding_manager: "BindingManager",
|
||||
local_nodes: List["SyncNode"],
|
||||
remote_nodes: List["SyncNode"],
|
||||
id_field_hints: Mapping[str, str],
|
||||
local_orphan_create_enabled_by_node: Mapping[str, bool],
|
||||
remote_orphan_create_enabled_by_node: Mapping[str, bool],
|
||||
) -> None:
|
||||
pending_auto_bind_updates = await plan_auto_bind_updates(
|
||||
node_type=node_type,
|
||||
config=config,
|
||||
local_collection=local_collection,
|
||||
remote_collection=remote_collection,
|
||||
binding_manager=binding_manager,
|
||||
local_nodes=local_nodes,
|
||||
remote_nodes=remote_nodes,
|
||||
id_field_hints=id_field_hints,
|
||||
local_orphan_create_enabled_by_node=local_orphan_create_enabled_by_node,
|
||||
remote_orphan_create_enabled_by_node=remote_orphan_create_enabled_by_node,
|
||||
)
|
||||
await apply_auto_bind_updates(
|
||||
node_type=node_type,
|
||||
runtime=runtime,
|
||||
binding_manager=binding_manager,
|
||||
pending_auto_bind_updates=pending_auto_bind_updates,
|
||||
)
|
||||
|
||||
|
||||
async def plan_auto_bind_updates(
|
||||
*,
|
||||
node_type: str,
|
||||
config: "StrategyConfig",
|
||||
local_collection: "DataCollection",
|
||||
remote_collection: "DataCollection",
|
||||
binding_manager: "BindingManager",
|
||||
local_nodes: List["SyncNode"],
|
||||
remote_nodes: List["SyncNode"],
|
||||
id_field_hints: Mapping[str, str],
|
||||
local_orphan_create_enabled_by_node: Mapping[str, bool],
|
||||
remote_orphan_create_enabled_by_node: Mapping[str, bool],
|
||||
) -> List[PendingAutoBindUpdate]:
|
||||
pending_auto_bind_updates: List[PendingAutoBindUpdate] = []
|
||||
prebound_node_ids: Set[str] = set()
|
||||
|
||||
local_s02_ids = {
|
||||
n.node_id
|
||||
for n in strategy.local_collection.filter_by_state_ids(
|
||||
node_type=strategy.node_type,
|
||||
for n in local_collection.filter_by_state_ids(
|
||||
node_type=node_type,
|
||||
state_ids=["S02"],
|
||||
)
|
||||
}
|
||||
remote_s02_ids = {
|
||||
n.node_id
|
||||
for n in strategy.remote_collection.filter_by_state_ids(
|
||||
node_type=strategy.node_type,
|
||||
for n in remote_collection.filter_by_state_ids(
|
||||
node_type=node_type,
|
||||
state_ids=["S02"],
|
||||
)
|
||||
}
|
||||
|
||||
local_create_enabled = strategy.config.local_orphan_action == OrphanAction.CREATE_REMOTE
|
||||
remote_create_enabled = strategy.config.remote_orphan_action == OrphanAction.CREATE_LOCAL
|
||||
|
||||
for prebind_pair in strategy.config.pre_bind_data_id:
|
||||
local_node = strategy.local_collection.get_by_data_id(strategy.node_type, prebind_pair.local_data_id)
|
||||
remote_node = strategy.remote_collection.get_by_data_id(strategy.node_type, prebind_pair.remote_data_id)
|
||||
for prebind_pair in config.pre_bind_data_id:
|
||||
local_node = local_collection.get_by_data_id(node_type, prebind_pair.local_data_id)
|
||||
remote_node = remote_collection.get_by_data_id(node_type, prebind_pair.remote_data_id)
|
||||
|
||||
if local_node is None or remote_node is None:
|
||||
continue
|
||||
@@ -329,325 +461,269 @@ async def phase_auto_binding(strategy, local_nodes: List["SyncNode"], remote_nod
|
||||
if local_node.binding_status != BindingStatus.MISSING or remote_node.binding_status != BindingStatus.MISSING:
|
||||
continue
|
||||
|
||||
existing_remote = await strategy.binding_manager.get_remote_id(strategy.node_type, local_node.node_id)
|
||||
existing_local = await strategy.binding_manager.get_local_id(strategy.node_type, remote_node.node_id)
|
||||
existing_remote = await binding_manager.get_remote_id(node_type, local_node.node_id)
|
||||
existing_local = await binding_manager.get_local_id(node_type, remote_node.node_id)
|
||||
if existing_remote or existing_local:
|
||||
continue
|
||||
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": local_node,
|
||||
"outcome": "ONE_TO_ONE",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": remote_node.node_id,
|
||||
}
|
||||
PendingAutoBindUpdate(
|
||||
node=local_node,
|
||||
outcome="ONE_TO_ONE",
|
||||
create_enabled=None,
|
||||
bind_remote_node_id=remote_node.node_id,
|
||||
)
|
||||
)
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": remote_node,
|
||||
"outcome": "ONE_TO_ONE",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
PendingAutoBindUpdate(node=remote_node, outcome="ONE_TO_ONE", create_enabled=None)
|
||||
)
|
||||
prebound_node_ids.add(local_node.node_id)
|
||||
prebound_node_ids.add(remote_node.node_id)
|
||||
|
||||
if not strategy.config.auto_bind or not strategy.config.auto_bind_fields:
|
||||
if not config.auto_bind or not config.auto_bind_fields:
|
||||
for node in local_nodes:
|
||||
if node.node_id in prebound_node_ids:
|
||||
continue
|
||||
if node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append({"node": node, "outcome": None, "create_enabled": None})
|
||||
pending_auto_bind_updates.append(PendingAutoBindUpdate(node=node, outcome=None, create_enabled=None))
|
||||
for node in remote_nodes:
|
||||
if node.node_id in prebound_node_ids:
|
||||
continue
|
||||
if node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append({"node": node, "outcome": None, "create_enabled": None})
|
||||
else:
|
||||
id_resolver = IDResolver(strategy.local_collection, strategy.remote_collection, strategy.binding_manager)
|
||||
pending_auto_bind_updates.append(PendingAutoBindUpdate(node=node, outcome=None, create_enabled=None))
|
||||
return pending_auto_bind_updates
|
||||
|
||||
local_key_map: Dict[Tuple, List["SyncNode"]] = {}
|
||||
remote_key_map: Dict[Tuple, List["SyncNode"]] = {}
|
||||
id_resolver = IDResolver(local_collection, remote_collection, binding_manager)
|
||||
local_key_map: Dict[Tuple, List["SyncNode"]] = {}
|
||||
remote_key_map: Dict[Tuple, List["SyncNode"]] = {}
|
||||
|
||||
for node in local_nodes:
|
||||
if node.node_id in prebound_node_ids:
|
||||
continue
|
||||
if node.node_id not in local_s02_ids:
|
||||
async def _collect_side_key_map(
|
||||
*,
|
||||
nodes: List["SyncNode"],
|
||||
s02_ids: Set[str],
|
||||
key_map: Dict[Tuple, List["SyncNode"]],
|
||||
source_is_local: bool,
|
||||
) -> None:
|
||||
for node in nodes:
|
||||
if node.node_id in prebound_node_ids or node.node_id not in s02_ids:
|
||||
continue
|
||||
node_data = node.get_data()
|
||||
if not node_data:
|
||||
if node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": node,
|
||||
"outcome": "DATA_MISSING",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
PendingAutoBindUpdate(node=node, outcome="DATA_MISSING", create_enabled=None)
|
||||
)
|
||||
continue
|
||||
|
||||
missing_fields = [fn for fn in strategy.config.auto_bind_fields if fn not in node_data]
|
||||
missing_fields = [fn for fn in config.auto_bind_fields if fn not in node_data]
|
||||
if missing_fields:
|
||||
if node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{"node": node, "outcome": "KEY_MISSING", "create_enabled": None, "bind_remote_node_id": None}
|
||||
PendingAutoBindUpdate(node=node, outcome="KEY_MISSING", create_enabled=None)
|
||||
)
|
||||
continue
|
||||
|
||||
key_dict = extract_biz_key_dict(node_data, strategy.config.auto_bind_fields)
|
||||
key_dict = extract_biz_key_dict(node_data, config.auto_bind_fields)
|
||||
if not key_dict:
|
||||
continue
|
||||
|
||||
resolved_key_dict, resolve_error = await resolve_id_fields_in_key_dict(
|
||||
key_dict,
|
||||
id_resolver,
|
||||
get_id_field_hints(strategy),
|
||||
)
|
||||
if not resolved_key_dict:
|
||||
if source_is_local:
|
||||
key_dict, resolve_error = await resolve_id_fields_in_key_dict(
|
||||
key_dict,
|
||||
id_resolver,
|
||||
dict(id_field_hints),
|
||||
)
|
||||
if not key_dict:
|
||||
if node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
PendingAutoBindUpdate(
|
||||
node=node,
|
||||
outcome="KEY_ID_RESOLVE_FAIL",
|
||||
create_enabled=None,
|
||||
detail=resolve_error,
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
key = dict_to_biz_key_tuple(key_dict, config.auto_bind_fields)
|
||||
key_map.setdefault(key, []).append(node)
|
||||
|
||||
await _collect_side_key_map(
|
||||
nodes=local_nodes,
|
||||
s02_ids=local_s02_ids,
|
||||
key_map=local_key_map,
|
||||
source_is_local=True,
|
||||
)
|
||||
await _collect_side_key_map(
|
||||
nodes=remote_nodes,
|
||||
s02_ids=remote_s02_ids,
|
||||
key_map=remote_key_map,
|
||||
source_is_local=False,
|
||||
)
|
||||
|
||||
all_keys = set(local_key_map.keys()) | set(remote_key_map.keys())
|
||||
for biz_key in all_keys:
|
||||
local_candidates = local_key_map.get(biz_key, [])
|
||||
remote_candidates = remote_key_map.get(biz_key, [])
|
||||
|
||||
excluded_local = set()
|
||||
excluded_remote = set()
|
||||
for l_node in local_candidates:
|
||||
if l_node.binding_status != BindingStatus.NORMAL:
|
||||
continue
|
||||
|
||||
remote_id = await binding_manager.get_remote_id(node_type, l_node.node_id)
|
||||
if not remote_id:
|
||||
continue
|
||||
|
||||
for r_node in remote_candidates:
|
||||
if r_node.node_id == remote_id and r_node.binding_status == BindingStatus.NORMAL:
|
||||
excluded_local.add(l_node.node_id)
|
||||
excluded_remote.add(r_node.node_id)
|
||||
break
|
||||
|
||||
l_remain = [n for n in local_candidates if n.node_id not in excluded_local]
|
||||
r_remain = [n for n in remote_candidates if n.node_id not in excluded_remote]
|
||||
|
||||
if len(l_remain) == 1 and len(r_remain) == 1:
|
||||
l_node = l_remain[0]
|
||||
r_node = r_remain[0]
|
||||
if l_node.binding_status == BindingStatus.MISSING and r_node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
PendingAutoBindUpdate(
|
||||
node=l_node,
|
||||
outcome="ONE_TO_ONE",
|
||||
create_enabled=None,
|
||||
bind_remote_node_id=r_node.node_id,
|
||||
)
|
||||
)
|
||||
pending_auto_bind_updates.append(
|
||||
PendingAutoBindUpdate(node=r_node, outcome="ONE_TO_ONE", create_enabled=None)
|
||||
)
|
||||
continue
|
||||
|
||||
if len(l_remain) >= 1 and len(r_remain) == 0:
|
||||
for node in l_remain:
|
||||
if node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": node,
|
||||
"outcome": "KEY_ID_RESOLVE_FAIL",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": None,
|
||||
"detail": resolve_error,
|
||||
}
|
||||
PendingAutoBindUpdate(
|
||||
node=node,
|
||||
outcome="ZERO",
|
||||
create_enabled=bool(local_orphan_create_enabled_by_node.get(node.node_id, False)),
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
key = dict_to_biz_key_tuple(resolved_key_dict, strategy.config.auto_bind_fields)
|
||||
local_key_map.setdefault(key, []).append(node)
|
||||
|
||||
for r_node in remote_nodes:
|
||||
if r_node.node_id in prebound_node_ids:
|
||||
continue
|
||||
if r_node.node_id not in remote_s02_ids:
|
||||
continue
|
||||
r_node_data = r_node.get_data()
|
||||
if not r_node_data:
|
||||
if r_node.binding_status == BindingStatus.MISSING:
|
||||
if len(r_remain) >= 1 and len(l_remain) == 0:
|
||||
for node in r_remain:
|
||||
if node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": r_node,
|
||||
"outcome": "DATA_MISSING",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
PendingAutoBindUpdate(
|
||||
node=node,
|
||||
outcome="ZERO",
|
||||
create_enabled=bool(remote_orphan_create_enabled_by_node.get(node.node_id, False)),
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
missing_fields = [fn for fn in strategy.config.auto_bind_fields if fn not in r_node_data]
|
||||
if missing_fields:
|
||||
if r_node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": r_node,
|
||||
"outcome": "KEY_MISSING",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
many_local_one_remote = len(l_remain) > 1 and len(r_remain) == 1
|
||||
one_local_many_remote = len(l_remain) == 1 and len(r_remain) > 1
|
||||
if config.allow_many_to_one_auto_bind and (many_local_one_remote or one_local_many_remote):
|
||||
chosen_local, chosen_remote = _stable_pick_bind_pair(l_remain, r_remain)
|
||||
if chosen_local.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
PendingAutoBindUpdate(
|
||||
node=chosen_local,
|
||||
outcome="ONE_TO_ONE",
|
||||
create_enabled=None,
|
||||
bind_remote_node_id=chosen_remote.node_id,
|
||||
)
|
||||
continue
|
||||
)
|
||||
if chosen_remote.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
PendingAutoBindUpdate(node=chosen_remote, outcome="ONE_TO_ONE", create_enabled=None)
|
||||
)
|
||||
|
||||
key_dict = extract_biz_key_dict(r_node_data, strategy.config.auto_bind_fields)
|
||||
if key_dict:
|
||||
key = dict_to_biz_key_tuple(key_dict, strategy.config.auto_bind_fields)
|
||||
remote_key_map.setdefault(key, []).append(r_node)
|
||||
|
||||
all_keys = set(local_key_map.keys()) | set(remote_key_map.keys())
|
||||
|
||||
for biz_key in all_keys:
|
||||
local_candidates = local_key_map.get(biz_key, [])
|
||||
remote_candidates = remote_key_map.get(biz_key, [])
|
||||
|
||||
excluded_local = set()
|
||||
excluded_remote = set()
|
||||
|
||||
for l_node in local_candidates:
|
||||
if l_node.binding_status != BindingStatus.NORMAL:
|
||||
for node in l_remain:
|
||||
if node.node_id == chosen_local.node_id or node.binding_status != BindingStatus.MISSING:
|
||||
continue
|
||||
|
||||
remote_id = await strategy.binding_manager.get_remote_id(strategy.node_type, l_node.node_id)
|
||||
if not remote_id:
|
||||
pending_auto_bind_updates.append(
|
||||
PendingAutoBindUpdate(
|
||||
node=node,
|
||||
outcome="ZERO",
|
||||
create_enabled=bool(local_orphan_create_enabled_by_node.get(node.node_id, False)),
|
||||
)
|
||||
)
|
||||
for node in r_remain:
|
||||
if node.node_id == chosen_remote.node_id or node.binding_status != BindingStatus.MISSING:
|
||||
continue
|
||||
|
||||
for r_node in remote_candidates:
|
||||
if r_node.node_id == remote_id and r_node.binding_status == BindingStatus.NORMAL:
|
||||
excluded_local.add(l_node.node_id)
|
||||
excluded_remote.add(r_node.node_id)
|
||||
break
|
||||
|
||||
l_remain = [n for n in local_candidates if n.node_id not in excluded_local]
|
||||
r_remain = [n for n in remote_candidates if n.node_id not in excluded_remote]
|
||||
|
||||
if len(l_remain) == 1 and len(r_remain) == 1:
|
||||
l_node = l_remain[0]
|
||||
r_node = r_remain[0]
|
||||
|
||||
if l_node.binding_status == BindingStatus.MISSING and r_node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": l_node,
|
||||
"outcome": "ONE_TO_ONE",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": r_node.node_id,
|
||||
}
|
||||
pending_auto_bind_updates.append(
|
||||
PendingAutoBindUpdate(
|
||||
node=node,
|
||||
outcome="ZERO",
|
||||
create_enabled=bool(remote_orphan_create_enabled_by_node.get(node.node_id, False)),
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
if (len(l_remain) > 1 and len(r_remain) >= 1) or (len(l_remain) == 1 and len(r_remain) > 1):
|
||||
for node in l_remain:
|
||||
if node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": r_node,
|
||||
"outcome": "ONE_TO_ONE",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
PendingAutoBindUpdate(node=node, outcome="AMBIGUOUS", create_enabled=None)
|
||||
)
|
||||
if (len(r_remain) > 1 and len(l_remain) >= 1) or (len(r_remain) == 1 and len(l_remain) > 1):
|
||||
for node in r_remain:
|
||||
if node.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
PendingAutoBindUpdate(node=node, outcome="AMBIGUOUS", create_enabled=None)
|
||||
)
|
||||
else:
|
||||
if len(l_remain) >= 1 and len(r_remain) == 0:
|
||||
for n in l_remain:
|
||||
if n.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": n,
|
||||
"outcome": "ZERO",
|
||||
"create_enabled": local_create_enabled,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
)
|
||||
|
||||
if len(r_remain) >= 1 and len(l_remain) == 0:
|
||||
for n in r_remain:
|
||||
if n.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": n,
|
||||
"outcome": "ZERO",
|
||||
"create_enabled": remote_create_enabled,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
)
|
||||
return pending_auto_bind_updates
|
||||
|
||||
many_local_one_remote = len(l_remain) > 1 and len(r_remain) == 1
|
||||
one_local_many_remote = len(l_remain) == 1 and len(r_remain) > 1
|
||||
|
||||
if strategy.config.allow_many_to_one_auto_bind and (many_local_one_remote or one_local_many_remote):
|
||||
chosen_local, chosen_remote = _stable_pick_bind_pair(l_remain, r_remain)
|
||||
|
||||
if chosen_local.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": chosen_local,
|
||||
"outcome": "ONE_TO_ONE",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": chosen_remote.node_id,
|
||||
}
|
||||
)
|
||||
if chosen_remote.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": chosen_remote,
|
||||
"outcome": "ONE_TO_ONE",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
)
|
||||
|
||||
for n in l_remain:
|
||||
if n.node_id == chosen_local.node_id or n.binding_status != BindingStatus.MISSING:
|
||||
continue
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": n,
|
||||
"outcome": "ZERO",
|
||||
"create_enabled": local_create_enabled,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
)
|
||||
|
||||
for n in r_remain:
|
||||
if n.node_id == chosen_remote.node_id or n.binding_status != BindingStatus.MISSING:
|
||||
continue
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": n,
|
||||
"outcome": "ZERO",
|
||||
"create_enabled": remote_create_enabled,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
)
|
||||
else:
|
||||
if (len(l_remain) > 1 and len(r_remain) >= 1) or (len(l_remain) == 1 and len(r_remain) > 1):
|
||||
for n in l_remain:
|
||||
if n.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": n,
|
||||
"outcome": "AMBIGUOUS",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
)
|
||||
|
||||
if (len(r_remain) > 1 and len(l_remain) >= 1) or (len(r_remain) == 1 and len(l_remain) > 1):
|
||||
for n in r_remain:
|
||||
if n.binding_status == BindingStatus.MISSING:
|
||||
pending_auto_bind_updates.append(
|
||||
{
|
||||
"node": n,
|
||||
"outcome": "AMBIGUOUS",
|
||||
"create_enabled": None,
|
||||
"bind_remote_node_id": None,
|
||||
}
|
||||
)
|
||||
|
||||
async def apply_auto_bind_updates(
|
||||
*,
|
||||
node_type: str,
|
||||
runtime: "StateMachineRuntime",
|
||||
binding_manager: "BindingManager",
|
||||
pending_auto_bind_updates: List[PendingAutoBindUpdate],
|
||||
) -> None:
|
||||
for item in pending_auto_bind_updates:
|
||||
node = item["node"]
|
||||
auto_bind_outcome = item["outcome"]
|
||||
create_enabled = item["create_enabled"]
|
||||
bind_remote_node_id = item.get("bind_remote_node_id")
|
||||
detail = item.get("detail")
|
||||
auto_bind_enabled = auto_bind_outcome is not None
|
||||
auto_bind_enabled = item.outcome is not None
|
||||
decision = e16_bind_auto(
|
||||
strategy.ensure_runtime(),
|
||||
node=node,
|
||||
runtime,
|
||||
node=item.node,
|
||||
auto_bind_enabled=auto_bind_enabled,
|
||||
auto_bind_outcome=auto_bind_outcome,
|
||||
create_enabled=create_enabled,
|
||||
id_resolve_detail=detail,
|
||||
auto_bind_outcome=item.outcome,
|
||||
create_enabled=item.create_enabled,
|
||||
id_resolve_detail=item.detail,
|
||||
)
|
||||
reason = decision_note(decision)
|
||||
if decision.to_state == "S01" and auto_bind_outcome == "ONE_TO_ONE":
|
||||
if bind_remote_node_id:
|
||||
await strategy.binding_manager.bind(
|
||||
strategy.node_type,
|
||||
node.node_id,
|
||||
bind_remote_node_id,
|
||||
)
|
||||
logger.debug(f"[{strategy.node_type}] 自动绑定成功: node={node.node_id}")
|
||||
if decision.to_state == "S01" and item.outcome == "ONE_TO_ONE":
|
||||
if item.bind_remote_node_id:
|
||||
await binding_manager.bind(node_type, item.node.node_id, item.bind_remote_node_id)
|
||||
logger.debug(f"[{node_type}] 自动绑定成功: node={item.node.node_id}")
|
||||
if decision.to_state in {"S04", "S05"}:
|
||||
if detail:
|
||||
reason = f"{reason} | detail={detail}"
|
||||
node.error = reason
|
||||
logger.debug(f"[{strategy.node_type}] 自动绑定阻断: node={node.node_id}, reason={reason}")
|
||||
if item.detail:
|
||||
reason = f"{reason} | detail={item.detail}"
|
||||
item.node.error = reason
|
||||
logger.debug(f"[{node_type}] 自动绑定阻断: node={item.node.node_id}, reason={reason}")
|
||||
|
||||
outcome_counts: Dict[str, int] = {}
|
||||
create_enabled_count = 0
|
||||
bound_pairs = 0
|
||||
for item in pending_auto_bind_updates:
|
||||
outcome = str(item.get("outcome") or "NONE")
|
||||
outcome = str(item.outcome or "NONE")
|
||||
outcome_counts[outcome] = outcome_counts.get(outcome, 0) + 1
|
||||
if item.get("create_enabled"):
|
||||
if item.create_enabled:
|
||||
create_enabled_count += 1
|
||||
if item.get("bind_remote_node_id"):
|
||||
if item.bind_remote_node_id:
|
||||
bound_pairs += 1
|
||||
|
||||
if pending_auto_bind_updates:
|
||||
ordered = ", ".join(f"{key}={outcome_counts[key]}" for key in sorted(outcome_counts))
|
||||
logger.info(
|
||||
"[%s] Auto-bind summary: candidates=%d, bind_pairs=%d, create_enabled=%d, outcomes={%s}",
|
||||
strategy.node_type,
|
||||
node_type,
|
||||
len(pending_auto_bind_updates),
|
||||
bound_pairs,
|
||||
create_enabled_count,
|
||||
|
||||
@@ -113,40 +113,6 @@ async def add_create_action(
|
||||
return created_nodes
|
||||
|
||||
|
||||
async def run_create(strategy) -> List["SyncNode"]:
|
||||
created_nodes: List["SyncNode"] = []
|
||||
|
||||
if strategy.config.local_orphan_action == OrphanAction.CREATE_REMOTE:
|
||||
local_nodes = await add_create_action(
|
||||
strategy,
|
||||
from_collection=strategy.local_collection,
|
||||
to_collection=strategy.remote_collection,
|
||||
source_is_local=True,
|
||||
)
|
||||
created_nodes.extend(local_nodes)
|
||||
logger.info(f"[{strategy.node_type}] Created {len(local_nodes)} nodes in remote collection (Push)")
|
||||
elif strategy.config.local_orphan_action == OrphanAction.DELETE_LOCAL:
|
||||
raise NotImplementedError(
|
||||
f"[{strategy.node_type}] local_orphan_action=DELETE_LOCAL is not supported in run_create; "
|
||||
"use delete stage (delete_ops) explicitly."
|
||||
)
|
||||
|
||||
if strategy.config.remote_orphan_action == OrphanAction.CREATE_LOCAL:
|
||||
remote_nodes = await add_create_action(
|
||||
strategy,
|
||||
from_collection=strategy.remote_collection,
|
||||
to_collection=strategy.local_collection,
|
||||
source_is_local=False,
|
||||
)
|
||||
created_nodes.extend(remote_nodes)
|
||||
logger.info(f"[{strategy.node_type}] Created {len(remote_nodes)} nodes in local collection (Pull)")
|
||||
elif strategy.config.remote_orphan_action == OrphanAction.DELETE_REMOTE:
|
||||
raise NotImplementedError(
|
||||
f"[{strategy.node_type}] remote_orphan_action=DELETE_REMOTE is not supported in run_create; "
|
||||
"use delete stage (delete_ops) explicitly."
|
||||
)
|
||||
|
||||
return created_nodes
|
||||
|
||||
|
||||
async def finalize_peer_creating_sources(
|
||||
|
||||
@@ -4,6 +4,7 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
from ...common.types import BindingStatus, SyncAction, SyncStatus
|
||||
from ...logging import get_logger
|
||||
from ...engine import e01_bootstrap
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -11,7 +12,7 @@ if TYPE_CHECKING:
|
||||
from ...common.collection import DataCollection
|
||||
from ...engine.dispatcher import StateMachineRuntime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
async def _resolve_local_id_for_unbind(
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any, Iterable, List, Optional, Sequence
|
||||
|
||||
from ...common.types import SyncAction
|
||||
from ...logging import get_logger
|
||||
from ...sync_system.config import UpdateDirection
|
||||
from ...sync_system.resolve_ids import IDResolver
|
||||
from ...engine import decision_note, e30_update_prepare
|
||||
@@ -20,7 +21,7 @@ if TYPE_CHECKING:
|
||||
from ...common.collection import DataCollection
|
||||
from ...common.sync_node import SyncNode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def _build_schema_diff_validator(strategy) -> SchemaDiffValidator:
|
||||
@@ -396,23 +397,3 @@ async def add_update_action(
|
||||
return updated_nodes
|
||||
|
||||
|
||||
async def run_update(strategy) -> List["SyncNode"]:
|
||||
updated_by_id: dict[str, "SyncNode"] = {}
|
||||
validator = _build_schema_diff_validator(strategy)
|
||||
strategy._active_schema_diff_validator = validator
|
||||
data_id_map = await build_data_id_normalization_map(
|
||||
node_type=strategy.node_type,
|
||||
local_collection=strategy.local_collection,
|
||||
remote_collection=strategy.remote_collection,
|
||||
binding_manager=strategy.binding_manager,
|
||||
)
|
||||
try:
|
||||
for pair in await collect_bound_node_pairs(strategy):
|
||||
nodes = await strategy.update_pair(pair.local_node, pair.remote_node, data_id_map)
|
||||
for node in nodes:
|
||||
updated_by_id[node.node_id] = node
|
||||
finally:
|
||||
strategy._active_schema_diff_validator = None
|
||||
|
||||
emit_schema_diff_report(strategy, validator)
|
||||
return list(updated_by_id.values())
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
from typing import List, Tuple, Optional, Any, Dict
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from ..logging import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def is_id_field(field_name: str) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user