修改多个问题,优化代码结构

This commit is contained in:
strepsiades
2026-04-01 16:23:43 +08:00
parent 8f4727a772
commit 7c49df94fc
29 changed files with 968 additions and 504 deletions
+8 -3
View File
@@ -5,9 +5,13 @@ import asyncio
import logging
from pathlib import Path
from .logging import get_logger, init_app_logger
from .pipeline import run_profile_from_file
logger = get_logger(__name__)
def package_project_root() -> Path:
return Path(__file__).resolve().parents[1]
@@ -20,6 +24,7 @@ def resolve_config_path(raw_path: str, *, project_root: Path) -> Path:
async def _main() -> int:
init_app_logger(replace_handlers=True)
project_root = package_project_root()
parser = argparse.ArgumentParser(description="Run sync pipeline from config override file")
@@ -34,7 +39,7 @@ async def _main() -> int:
if not cfg_path.exists() or not cfg_path.is_file():
raise FileNotFoundError(f"Config file not found: {cfg_path}")
print(f"🧩 Loaded config file: {cfg_path}")
logger.info("🧩 Loaded config file: %s", cfg_path)
await run_profile_from_file(
project_root=project_root,
config_path=cfg_path,
@@ -47,10 +52,10 @@ def main() -> int:
try:
return asyncio.run(_main())
except KeyboardInterrupt:
print("\n⚠️ Interrupted by user")
logger.warning("⚠️ Interrupted by user")
return 130
except Exception as exc:
print(f"\n❌ Pipeline failed: {type(exc).__name__}: {exc}")
logger.exception("❌ Pipeline failed: %s: %s", type(exc).__name__, exc)
return 1
finally:
logging.shutdown()
+3 -1
View File
@@ -1,10 +1,12 @@
import json
import logging
from typing import Optional, Dict, Any, List
from ..logging import get_logger
from .persistence import PersistenceBackend
from .types import BindingStatus, BindingRecord
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
class BindingManager:
+4 -3
View File
@@ -23,6 +23,7 @@ from datetime import datetime
import httpx
from schemas.common.push_system import PushIdsSchema
from ...logging import get_logger
def _build_default_push_steps() -> List[Dict[str, str]]:
@@ -68,7 +69,7 @@ class ApiClient:
self._trace_by_biz_id: Dict[str, deque[Dict[str, Any]]] = defaultdict(deque)
self._trace_by_op: Dict[str, deque[Dict[str, Any]]] = defaultdict(deque)
self._load_trace_by_item: Dict[tuple[str, str], deque[Dict[str, Any]]] = defaultdict(deque)
self._logger = logging.getLogger(__name__)
self._logger = get_logger(__name__)
self._rate_limit_max_requests = int(rate_limit_max_requests)
self._rate_limit_window_seconds = float(rate_limit_window_seconds)
self._rate_limit_lock = asyncio.Lock()
@@ -138,8 +139,8 @@ class ApiClient:
if not self._debug:
return
self._log(
f"ApiClient request: method={method.upper()} endpoint={endpoint} elapsed={elapsed_seconds:.3f}s",
level="DEBUG",
f"🔎 ApiClient request: method={method.upper()} endpoint={endpoint} elapsed={elapsed_seconds:.3f}s",
level="INFO",
)
def _log(self, message: str, level: str = "DEBUG"):
@@ -3,8 +3,10 @@ from __future__ import annotations
import logging
from pathlib import Path
from ...logging import get_logger
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
def prepare_remote_jsonl_dir(remote_dir: Path, *, project_root: Path) -> Path:
@@ -1,9 +1,18 @@
from pydantic import BaseModel, Field
from ...common.sync_node import SyncNode
from ...sync_system.strategy import DefaultSyncStrategy
from ...sync_system.config import StrategyConfig, OrphanAction, UpdateDirection
from ...sync_system.strategy_ops.compare_ops import build_data_id_normalization_map
from ...sync_system.strategy_ops.update_ops import emit_schema_diff_report, _build_schema_diff_validator
from ...sync_system.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 ...sync_system.strategy_ops import BoundNodePair
from schemas.project_extensions.project_detail import ProjectDetailKey, ProjectDetailProject
@@ -37,45 +46,108 @@ class ProjectDetailSyncStrategy(DefaultSyncStrategy[ProjectDetailProject]):
update_direction=UpdateDirection.PUSH,
)
async def update(self):
self.ensure_runtime()
validator = _build_schema_diff_validator(self)
self._active_schema_diff_validator = validator
data_id_map = await build_data_id_normalization_map(
async def bind(self) -> None:
runtime = self.ensure_runtime()
local_nodes, 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=local_nodes,
remote_nodes=remote_nodes,
)
await phase_dependency_check(
node_type=self.node_type,
runtime=runtime,
depend_fields=dict(self.config.depend_fields),
local_nodes=local_nodes,
remote_nodes=remote_nodes,
local_collection=self.local_collection,
remote_collection=self.remote_collection,
)
local_nodes, 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 local_nodes
}
remote_orphan_create_enabled_by_node = {
node.node_id: remote_create_enabled for node in remote_nodes
}
if self.domain_option.pull_group_production_plans:
for node in local_nodes:
if self._extract_node_key(node) in self._GROUP_PRODUCTION_PLAN_KEYS:
local_orphan_create_enabled_by_node[node.node_id] = False
for node in remote_nodes:
if self._extract_node_key(node) in self._GROUP_PRODUCTION_PLAN_KEYS:
remote_orphan_create_enabled_by_node[node.node_id] = True
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=local_nodes,
remote_nodes=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,
)
async def process_update_pairs(
self,
pairs: list[BoundNodePair],
data_id_map: dict[str, str] | None = None,
) -> dict[str, SyncNode]:
pull_keys = self._GROUP_PRODUCTION_PLAN_KEYS if self.domain_option.pull_group_production_plans else set()
pull_pairs = []
generic_pairs = []
for pair in await self.collect_update_pairs():
if self._should_pull_pair(pair.local_node, pair.remote_node):
for pair in pairs:
pair_key = self._extract_pair_key(pair.local_node, pair.remote_node)
if pair_key in pull_keys:
pull_pairs.append(pair)
else:
generic_pairs.append(pair)
updated_by_id = {}
try:
for pair in pull_pairs:
updated_node = await self.prepare_update_for_direction(
pair.local_node,
pair.remote_node,
UpdateDirection.PULL,
data_id_map,
)
if updated_node is not None:
updated_by_id[updated_node.node_id] = updated_node
updated_by_id: dict[str, SyncNode] = {}
for pair in pull_pairs:
updated_node = await self.prepare_update_for_direction(
pair.local_node,
pair.remote_node,
UpdateDirection.PULL,
data_id_map,
)
if updated_node is not None:
updated_by_id[updated_node.node_id] = updated_node
for pair in generic_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
finally:
self._active_schema_diff_validator = None
for pair in generic_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
emit_schema_diff_report(self, validator)
return list(updated_by_id.values())
return updated_by_id
@property
def domain_option(self) -> ProjectDetailDomainOption:
@@ -129,6 +201,16 @@ class ProjectDetailSyncStrategy(DefaultSyncStrategy[ProjectDetailProject]):
return items
@staticmethod
def _extract_node_key(node: SyncNode) -> str | None:
data = node.get_data() or {}
key = data.get("key")
if isinstance(key, ProjectDetailKey):
return key.value
if isinstance(key, str):
return key
return None
@staticmethod
def _extract_pair_key(local_node, remote_node) -> str | None:
local_data = local_node.get_data() or {}
@@ -141,7 +223,3 @@ class ProjectDetailSyncStrategy(DefaultSyncStrategy[ProjectDetailProject]):
return key
return None
def _should_pull_pair(self, local_node, remote_node) -> bool:
if not self.domain_option.pull_group_production_plans:
return False
return self._extract_pair_key(local_node, remote_node) in self._GROUP_PRODUCTION_PLAN_KEYS
+1
View File
@@ -39,6 +39,7 @@ def clear_app_logger_handlers(logger: Optional[logging.Logger] = None) -> None:
handler.close()
except Exception:
pass
target.propagate = True
def init_app_logger(
+9 -3
View File
@@ -48,7 +48,6 @@ from ..config import (
from ..config.strategy_config import resolve_domain_option_config
from ..sync_system.strategy import BaseSyncStrategy
from ..logging import configure_app_logging, ensure_app_logging, get_logger
from .run_logger import init_pipeline_logger
from .full_sync_pipeline import FullSyncPipeline
@@ -458,9 +457,16 @@ async def run_profile_from_file(
cfg_path = Path(config_path)
raw = load_overrides_from_file(cfg_path, project_root)
if is_multi_project_payload(raw):
from .multi_project_pipeline import MultiProjectPipeline
from .multi_project_pipeline import MultiProjectPipeline, _resolve_embedded_config_path
multi_cfg = MultiProjectRunConfig.model_validate(raw)
global_cfg_path = _resolve_embedded_config_path(
multi_cfg.global_config,
project_root=project_root,
profile_path=cfg_path,
)
global_config = build_config_from_file(project_root=project_root, file_path=global_cfg_path)
configure_app_logging(global_config.logging, replace_handlers=True)
pipeline = MultiProjectPipeline(project_root=project_root, config_path=cfg_path, config=multi_cfg)
if print_summary:
logger.info("\n%s", "=" * 80)
@@ -469,7 +475,7 @@ async def run_profile_from_file(
return await pipeline.run()
config = build_config(project_root=project_root, overrides=raw)
configure_app_logging(config.logging, replace_handlers=False)
configure_app_logging(config.logging, replace_handlers=True)
mode = f"{config.local_datasource.type}->{config.remote_datasource.type}"
return await run_pipeline_from_config(
config,
@@ -15,17 +15,19 @@ if TYPE_CHECKING:
from ..datasource.jsonl import JsonlDataSource
from ..common.collection import DataCollection
from ..common.binding import BindingManager
from ..logging import get_logger
from ..common.persistence import PersistenceBackend
from ..sync_system.strategy import BaseSyncStrategy
from ..sync_system.config import OrphanAction, UpdateDirection
from ..engine import StateMachineConfig, StateMachineRuntime
from ..engine import e41_post_create_ready
from ..sync_system.strategy_ops import finalize_peer_creating_sources, run_phase2_cleanup
from ..sync_system.strategy_ops.bind_ops import refresh_bind_data_id
from ..sync_system.strategy_ops.post_check_ops import evaluate_consistency_for_node_type
from .summary_report import print_pipeline_summary
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
class FullSyncPipeline:
@@ -211,10 +213,24 @@ class FullSyncPipeline:
try:
created = await strategy.create()
self._logger.info(f"✅ Strategy create: {node_type} (created={len(created)})")
created_local_node_ids = {
node.node_id for node in created if self.local_collection.get_by_node_id(node.node_id) is not None
}
created_remote_node_ids = {
node.node_id for node in created if self.remote_collection.get_by_node_id(node.node_id) is not None
}
create_written = await self.commit_creates(node_type)
await self.normalize_create_success_to_update_entry(node_type)
if create_written:
await self._reload_node_type(node_type, reason="create wrote data")
await refresh_bind_data_id(
node_type=node_type,
local_collection=self.local_collection,
remote_collection=self.remote_collection,
binding_manager=self.binding_manager,
local_node_ids=created_local_node_ids,
remote_node_ids=created_remote_node_ids,
)
except Exception as exc:
self.stats["failed"] += 1
self._logger.error(f"❌ Strategy create failed but pipeline continues: {node_type} | {type(exc).__name__}: {exc}")
@@ -12,10 +12,11 @@ from ..config import (
apply_config_overrides,
build_config_from_file,
)
from ..logging import get_logger
from .factory import create_pipeline_from_config
logger = logging.getLogger(__name__)
logger = get_logger(__name__)
class _SharedStateSnapshot(TypedDict):
+129 -12
View File
@@ -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())
+3 -1
View File
@@ -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: