264 lines
11 KiB
Python
264 lines
11 KiB
Python
import json
|
|
import logging
|
|
from typing import Optional, Dict, Any, List
|
|
|
|
from ..logging import get_logger, get_scope_display_name
|
|
from .persistence import PersistenceBackend
|
|
from .persistence_service import ScopedPersistenceView
|
|
from .types import BindingStatus, BindingRecord
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class BindingManager:
|
|
"""
|
|
BindingManager 负责维护不同 DataCollection 间节点的绑定关系。
|
|
所有查询均在内存中进行,持久化仅作为备份。
|
|
"""
|
|
def __init__(
|
|
self,
|
|
persistence: PersistenceBackend | ScopedPersistenceView,
|
|
auto_persist: bool = False,
|
|
scope: str = "global",
|
|
global_fallback_manager: Optional["BindingManager"] = None,
|
|
):
|
|
self.persistence = persistence
|
|
self.auto_persist = auto_persist
|
|
self.scope = scope
|
|
self._global_fallback_manager = global_fallback_manager
|
|
# { node_type: { local_id: remote_id } }
|
|
self._bindings: Dict[str, Dict[str, Optional[str]]] = {}
|
|
self._deleted: Dict[str, set[str]] = {}
|
|
|
|
def set_global_fallback_manager(self, manager: Optional["BindingManager"]) -> None:
|
|
self._global_fallback_manager = manager
|
|
|
|
async def _persistence_save_binding(self, node_type: str, local_id: str, payload_dict: Dict[str, Any]) -> None:
|
|
if isinstance(self.persistence, ScopedPersistenceView):
|
|
await self.persistence.save_binding(node_type, local_id, payload_dict)
|
|
return
|
|
await self.persistence.save_binding(self.scope, node_type, local_id, payload_dict)
|
|
|
|
async def _persistence_delete_binding(self, node_type: str, local_id: str) -> None:
|
|
if isinstance(self.persistence, ScopedPersistenceView):
|
|
await self.persistence.delete_binding(node_type, local_id)
|
|
return
|
|
await self.persistence.delete_binding(self.scope, node_type, local_id)
|
|
|
|
async def _persistence_delete_bindings_bulk(self, node_type: str, local_ids: List[str]) -> None:
|
|
if isinstance(self.persistence, ScopedPersistenceView):
|
|
await self.persistence.delete_bindings_bulk(node_type, local_ids)
|
|
return
|
|
await self.persistence.delete_bindings_bulk(self.scope, node_type, local_ids)
|
|
|
|
async def _persistence_save_bindings_bulk(self, node_type: str, bindings: Dict[str, Optional[str]]) -> None:
|
|
if isinstance(self.persistence, ScopedPersistenceView):
|
|
await self.persistence.save_bindings_bulk(node_type, bindings)
|
|
return
|
|
await self.persistence.save_bindings_bulk(self.scope, node_type, bindings)
|
|
|
|
async def _persistence_load_bindings(self, node_type: str) -> List[Dict[str, Any]]:
|
|
if isinstance(self.persistence, ScopedPersistenceView):
|
|
return await self.persistence.load_bindings(node_type)
|
|
return await self.persistence.load_bindings(self.scope, node_type)
|
|
|
|
def _get_type_bindings(self, node_type: str) -> Dict[str, Optional[str]]:
|
|
if node_type not in self._bindings:
|
|
self._bindings[node_type] = {}
|
|
return self._bindings[node_type]
|
|
|
|
def _get_deleted(self, node_type: str) -> set[str]:
|
|
if node_type not in self._deleted:
|
|
self._deleted[node_type] = set()
|
|
return self._deleted[node_type]
|
|
|
|
async def bind(self, node_type: str, local_id: str, remote_id: Optional[str], manual: bool = False):
|
|
"""
|
|
建立本地节点与远程节点间的映射(带中文日志)
|
|
|
|
Args:
|
|
node_type: 节点类型
|
|
local_id: 本地节点 ID
|
|
remote_id: 远程节点 ID
|
|
manual: 是否为手工绑定(用于日志区分)
|
|
"""
|
|
bindings = self._get_type_bindings(node_type)
|
|
bindings[local_id] = remote_id
|
|
self._get_deleted(node_type).discard(local_id)
|
|
|
|
# 记录日志:手工绑定保留 info,自动绑定降为 debug 避免日志过量
|
|
op_type = "手工" if manual else "自动"
|
|
log_line = (
|
|
f"[{op_type}绑定] 节点类型={node_type}, "
|
|
f"本地节点={local_id} <-> 远程节点={remote_id}"
|
|
)
|
|
if manual:
|
|
logger.info(log_line)
|
|
else:
|
|
logger.debug(log_line)
|
|
|
|
if self.auto_persist:
|
|
await self._persistence_save_binding(node_type, local_id, {"remote_id": remote_id})
|
|
|
|
async def unbind(self, node_type: str, local_id: str, manual: bool = False):
|
|
"""
|
|
解除映射关系并物理删除(带中文日志)
|
|
|
|
Args:
|
|
node_type: 节点类型
|
|
local_id: 本地节点 ID
|
|
manual: 是否为手工解绑(用于日志区分)
|
|
"""
|
|
bindings = self._get_type_bindings(node_type)
|
|
remote_id = bindings.get(local_id)
|
|
|
|
if local_id in bindings:
|
|
del bindings[local_id]
|
|
|
|
# 记录日志(中文,区分手工/自动)
|
|
op_type = "手工" if manual else "自动"
|
|
logger.info(
|
|
f"[{op_type}解绑] 节点类型={node_type}, "
|
|
f"本地节点={local_id} (原远程节点={remote_id})"
|
|
)
|
|
|
|
if self.auto_persist:
|
|
await self._persistence_delete_binding(node_type, local_id)
|
|
else:
|
|
self._get_deleted(node_type).add(local_id)
|
|
|
|
async def get_remote_id(self, node_type: str, local_id: str) -> Optional[str]:
|
|
"""查询本地节点对应的远程 ID"""
|
|
bindings = self._get_type_bindings(node_type)
|
|
remote_id = bindings.get(local_id)
|
|
if remote_id is not None or local_id in bindings:
|
|
return remote_id
|
|
if self._global_fallback_manager is None:
|
|
return None
|
|
return await self._global_fallback_manager.get_remote_id(node_type, local_id)
|
|
|
|
async def get_local_id(self, node_type: str, remote_id: str) -> Optional[str]:
|
|
"""查询远程节点对应的本地 ID (内存遍历)"""
|
|
bindings = self._get_type_bindings(node_type)
|
|
for lid, rid in bindings.items():
|
|
if rid == remote_id:
|
|
return lid
|
|
if self._global_fallback_manager is None:
|
|
return None
|
|
return await self._global_fallback_manager.get_local_id(node_type, remote_id)
|
|
|
|
async def get_record(self, node_type: str, local_id: str) -> Optional[BindingRecord]:
|
|
"""获取结构化绑定记录"""
|
|
remote_id = await self.get_remote_id(node_type, local_id)
|
|
if remote_id is None:
|
|
return None
|
|
return BindingRecord(
|
|
local_id=local_id,
|
|
remote_id=remote_id
|
|
)
|
|
|
|
async def get_all_records(self, node_type: str) -> List[BindingRecord]:
|
|
"""获取指定类型的所有完整绑定记录 (结构化对象)"""
|
|
bindings = self._get_type_bindings(node_type)
|
|
return [BindingRecord(local_id=lid, remote_id=rid) for lid, rid in bindings.items()]
|
|
|
|
async def get_all_bindings(self, node_type: str) -> List[tuple[str, Optional[str]]]:
|
|
"""获取指定类型的所有 ID 映射关系 (local_id, remote_id)"""
|
|
bindings = self._get_type_bindings(node_type)
|
|
return [(lid, rid) for lid, rid in bindings.items()]
|
|
|
|
async def load_from_persistence(self, node_type: str):
|
|
"""从持久化层加载特定类型的绑定关系到内存"""
|
|
raw_bindings = await self._persistence_load_bindings(node_type)
|
|
bindings = self._get_type_bindings(node_type)
|
|
bindings.clear()
|
|
self._get_deleted(node_type).clear()
|
|
for rb in raw_bindings:
|
|
payload = rb.get("payload") or {}
|
|
bindings[rb["local_id"]] = payload.get("remote_id")
|
|
|
|
def import_bindings(self, node_type: str, bindings: Dict[str, Optional[str]]) -> None:
|
|
"""导入既有绑定到当前 manager,仅写内存,不触发持久化。"""
|
|
target = self._get_type_bindings(node_type)
|
|
target.clear()
|
|
target.update(dict(bindings))
|
|
self._get_deleted(node_type).clear()
|
|
|
|
async def prune_missing_nodes(
|
|
self,
|
|
*,
|
|
local_collection,
|
|
remote_collection,
|
|
node_types: Optional[List[str]] = None,
|
|
) -> int:
|
|
target_types = list(node_types) if node_types is not None else list(self._bindings.keys())
|
|
deleted_count = 0
|
|
|
|
for node_type in target_types:
|
|
bindings = self._get_type_bindings(node_type)
|
|
if not bindings:
|
|
continue
|
|
|
|
valid_local_ids = {node.node_id for node in local_collection.filter(node_type=node_type)}
|
|
valid_remote_ids = {node.node_id for node in remote_collection.filter(node_type=node_type)}
|
|
|
|
stale_local_ids = [
|
|
local_id
|
|
for local_id, remote_id in bindings.items()
|
|
if local_id not in valid_local_ids or (remote_id is not None and remote_id not in valid_remote_ids)
|
|
]
|
|
|
|
for local_id in stale_local_ids:
|
|
del bindings[local_id]
|
|
deleted_count += 1
|
|
if self.auto_persist:
|
|
await self._persistence_delete_binding(node_type, local_id)
|
|
else:
|
|
self._get_deleted(node_type).add(local_id)
|
|
|
|
return deleted_count
|
|
|
|
async def persist(self, *, exclude_node_types: Optional[List[str]] = None) -> None:
|
|
"""将所有绑定关系写回持久化层"""
|
|
if not self._bindings and not self._deleted:
|
|
logger.info("🔍 [BindingManager.persist] 跳过:无绑定变更")
|
|
return
|
|
|
|
excluded = set(node_type for node_type in (exclude_node_types or []) if node_type)
|
|
|
|
type_count = 0
|
|
saved_total = 0
|
|
deleted_total = 0
|
|
touched_types: List[str] = []
|
|
|
|
for node_type, bindings in self._bindings.items():
|
|
if node_type in excluded:
|
|
continue
|
|
deleted = list(self._get_deleted(node_type))
|
|
binding_count = len(bindings)
|
|
deleted_count = len(deleted)
|
|
|
|
type_count += 1
|
|
saved_total += binding_count
|
|
deleted_total += deleted_count
|
|
if binding_count > 0 or deleted_count > 0:
|
|
touched_types.append(node_type)
|
|
|
|
if deleted:
|
|
await self._persistence_delete_bindings_bulk(node_type, deleted)
|
|
self._get_deleted(node_type).clear()
|
|
|
|
if bindings:
|
|
await self._persistence_save_bindings_bulk(node_type, bindings)
|
|
|
|
touched_text = ",".join(touched_types) if touched_types else "none"
|
|
logger.info(
|
|
f"🔍 [BindingManager.persist] 完成: scope={get_scope_display_name(self.scope)}, types={type_count}, saved={saved_total}, deleted={deleted_total}, touched=[{touched_text}]"
|
|
)
|
|
|
|
async def unload(self, *, node_types: Optional[List[str]] = None) -> None:
|
|
target_types = list(node_types) if node_types is not None else list(self._bindings.keys())
|
|
for node_type in target_types:
|
|
self._bindings.pop(node_type, None)
|
|
self._deleted.pop(node_type, None)
|