|
|
|
@@ -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,
|
|
|
|
|