优化加载后删除节点的逻辑;和数据库查询性能

This commit is contained in:
strepsiades
2026-04-02 19:31:04 +08:00
parent 74df53889c
commit 543c3ff906
17 changed files with 571 additions and 32 deletions
+2
View File
@@ -488,6 +488,7 @@ class DataCollection:
context=context_value,
)
node.context["_loaded_from_persistence"] = True
node.append_log("加载来源: persistence 恢复节点")
except Exception:
import logging
logger = logging.getLogger(__name__)
@@ -623,6 +624,7 @@ class DataCollection:
context=context_value,
)
node.context["_loaded_from_persistence"] = True
node.append_log("加载来源: persistence 恢复节点")
self._nodes[node.node_id] = node
if node.data_id and node.data_id != "":
@@ -112,6 +112,9 @@ class SQLitePersistenceBackend(PersistenceBackend):
await self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_nodes_collection_scope_data_id ON nodes (collection_id, scope, data_id)"
)
await self.conn.execute(
"CREATE INDEX IF NOT EXISTS idx_nodes_scope_type_node_id ON nodes (scope, node_type, node_id)"
)
async def _ensure_bindings_schema(self) -> None:
assert self.conn is not None
@@ -17,6 +17,7 @@ meta:
# represented as separate invocations with different inputs.
context:
is_create_zombie: { type: bool, desc: "初始化时判定为 CREATE 僵尸:action=CREATE 且 (status=FAILED 或 data_id 为空)" }
bootstrap_source_present: { type: bool, desc: "bootstrap 阶段 datasource 刷新后,当前节点是否在最新数据源中命中" }
has_binding_record: { type: bool, desc: "binding_manager 里存在绑定记录 (remote_id/local_id 非空)" }
core_binding_result: { type: enum, values: [NORMAL, WARNING, ABNORMAL], desc: "SyncDecisionEngine.determine_core_status() 给本节点的结果(仅在 has_binding_record=true 时有效)" }
@@ -304,19 +305,30 @@ states:
status: SKIPPED
data_id_exist: true
S17:
binding_status: ABNORMAL
action: NONE
status: FAILED
data_id_exist: [false, true]
desc: |
bootstrap 加载异常隔离态
持久化恢复后存在绑定,但 datasource 刷新未命中最新数据
binding_status: ABNORMAL
action: NONE
status: FAILED
data_id_exist: false | true
pseudo_states:
S15:
desc: "节点被清理删除(zombie cleanup"
S16:
desc: "持久化加载输入态:本轮 E01 的输入节点集合(可包含上轮遗留与已稳定节点)"
S17:
desc: "加载异常隔离态:持久化枚举非法,保留错误供人工处理,不进入自动流程"
stages:
bootstrap:
desc: "启动/初始化(仅针对持久化加载出的节点):僵尸清理与归并到 S00"
desc: "启动/初始化:持久化恢复后先做 datasource 刷新,再按 E01 清理僵尸或归并/隔离"
entry_states: [S16]
exit_states: [S00, S15]
exit_states: [S00, S15, S17]
bind:
desc: "绑定判定:core matrix +(可选)依赖检查 +(可选)自动绑定;stage 仅约束边界,不要求内部顺序"
@@ -346,13 +358,23 @@ stages:
transitions:
E01:
stage: bootstrap
desc: "初始化:删CREATE僵尸;其余重置后进入起点"
desc: "初始化:先依据 datasource 刷新结果判定 source 是否存在,再做僵尸清理/隔离/归并"
from: [S16]
outcomes:
- when: { is_create_zombie: true }
to: S15
- when: { is_create_zombie: false }
- when: { is_create_zombie: false, bootstrap_source_present: false, has_binding_record: false }
to: S15
emit:
note: "持久化孤儿节点未命中最新数据源,按僵尸清理"
- when: { is_create_zombie: false, bootstrap_source_present: false, has_binding_record: true }
to: S17
emit:
note: "存在绑定但最新数据源未返回节点,进入加载异常隔离"
- when: { is_create_zombie: false, bootstrap_source_present: true }
to: S00
emit:
note: "bootstrap 已刷新到最新数据,归并到起点"
# --- Bind phase1 core ---
E10:
@@ -101,7 +101,7 @@ class ConstructionTaskApiHandler(BaseApiHandler):
else:
results.append(TaskResult.in_progress(node_id=node.node_id, task_id=push_id))
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))
results.append(TaskResult.failed(node_id=node.node_id, error=str(e), push_id=push_id))
return results
@@ -135,6 +135,7 @@ class ConstructionTaskApiHandler(BaseApiHandler):
node_updated = False
error = None
push_failure_id = None
schema_status_lines: List[str] = []
# 1. 基础信息更新 (ConstructionUpdate)
@@ -186,6 +187,7 @@ class ConstructionTaskApiHandler(BaseApiHandler):
except Exception as e:
error = str(e)
base_update_error = str(e)
push_failure_id = push_id
else:
base_update_error = "show_name changed but task_type != QT"
@@ -236,6 +238,7 @@ class ConstructionTaskApiHandler(BaseApiHandler):
except Exception as e:
error = str(e)
plan_update_error = str(e)
push_failure_id = push_id
schema_status_lines.append(
self.build_update_schema_status(
@@ -253,7 +256,7 @@ class ConstructionTaskApiHandler(BaseApiHandler):
# 汇总结果
if error:
results.append(TaskResult.failed(node_id=node.node_id, error=error))
results.append(TaskResult.failed(node_id=node.node_id, error=error, push_id=push_failure_id))
elif node_updated:
results.append(TaskResult.success(node_id=node.node_id))
else:
@@ -279,7 +282,7 @@ class ConstructionTaskApiHandler(BaseApiHandler):
await api_delete_construction_task(self.api_client, biz_id, push_id)
results.append(TaskResult.success(node_id=node.node_id))
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))
results.append(TaskResult.failed(node_id=node.node_id, error=str(e), push_id=push_id))
return results
@@ -130,7 +130,7 @@ class ConstructionTaskDetailApiHandler(BaseApiHandler):
else:
results.append(TaskResult.in_progress(node_id=node.node_id, task_id=push_id))
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))
results.append(TaskResult.failed(node_id=node.node_id, error=str(e), push_id=push_id))
return results
@@ -222,7 +222,7 @@ class ConstructionTaskDetailApiHandler(BaseApiHandler):
# 整组失败
error_msg = str(e)
for node, _ in group_items:
results.append(TaskResult.failed(node_id=node.node_id, error=error_msg))
results.append(TaskResult.failed(node_id=node.node_id, error=error_msg, push_id=push_id))
return results
@@ -240,7 +240,7 @@ class ConstructionTaskDetailApiHandler(BaseApiHandler):
await api_delete_construction_task_detail(self.api_client, biz_id, push_id)
results.append(TaskResult.success(node_id=node.node_id))
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))
results.append(TaskResult.failed(node_id=node.node_id, error=str(e), push_id=push_id))
return results
+16 -1
View File
@@ -276,8 +276,14 @@ def e01_bootstrap(
*,
node: "SyncNode",
is_create_zombie: bool,
bootstrap_source_present: bool = True,
has_binding_record: bool = False,
) -> Decision:
context = {"is_create_zombie": is_create_zombie}
context = {
"is_create_zombie": is_create_zombie,
"bootstrap_source_present": bootstrap_source_present,
"has_binding_record": has_binding_record,
}
decision = _dispatch_required(
runtime,
node=node,
@@ -289,6 +295,15 @@ def e01_bootstrap(
_apply_decision(runtime, node=node, decision=decision, fields={"binding_status", "action", "status"})
node.error = None
return decision
if decision.to_state == "S17":
_apply_decision(runtime, node=node, decision=decision, fields={"binding_status", "action", "status"})
reason_text = decision_note(decision) or "<no-reason>"
node.context["bootstrap_blocked"] = True
node.context["bootstrap_block_reason"] = "source_missing_after_persistence_refresh"
node.context["bootstrap_block_errors"] = [reason_text]
node.context["bootstrap_source_present"] = False
node.error = f"BOOTSTRAP_SOURCE_MISSING: {reason_text}"
return decision
if decision.to_state == "S15":
reason_text = decision_note(decision) or "<no-reason>"
node.append_log(
@@ -256,6 +256,12 @@ class FullSyncPipeline:
self._prepare_datasources()
self._logger.info("✅ Bootstrap: datasource prepared")
await self._bootstrap_refresh_latest_data()
self._logger.info("✅ Bootstrap: datasource refresh completed")
await self._run_bootstrap_reset_cleanup()
self._logger.info("✅ Bootstrap: reset cleanup completed")
await self._apply_project_scope_cleanup()
async def phase_bind(self) -> None:
@@ -359,7 +365,7 @@ class FullSyncPipeline:
await _safe_close("persistence", self.persistence.close())
async def _load_persistence_state(self) -> None:
"""加载持久化数据并清理僵尸节点"""
"""加载持久化数据到内存,等待 bootstrap datasource refresh 与 E01 cleanup。"""
if not self.enable_persistence:
return
@@ -376,10 +382,50 @@ class FullSyncPipeline:
for node_type in binding_node_types:
await self.binding_manager.load_from_persistence(node_type)
self._logger.info("🔄 Persistence loaded (state preserved; pending E01 normalization)")
# 加载后 reset 清理:清理 CREATE 失败的僵尸绑定
def _resolve_bootstrap_refresh_node_types(self) -> List[str]:
local_types = {node.node_type for node in self.local_collection.filter()}
remote_types = {node.node_type for node in self.remote_collection.filter()}
candidate_types = local_types | remote_types
if not candidate_types:
return []
ordered: List[str] = []
for node_type in [*self.load_order, *self.bootstrap_binding_node_types, *sorted(candidate_types)]:
if node_type in candidate_types and node_type not in ordered:
ordered.append(node_type)
return ordered
async def _bootstrap_refresh_latest_data(self) -> None:
if not self.enable_persistence:
return
node_types = self._resolve_bootstrap_refresh_node_types()
if not node_types:
self._logger.info("⏭️ Bootstrap datasource refresh skipped: no persisted node types")
return
for node_type in node_types:
self._logger.info("🔄 Bootstrap refresh node_type=%s from datasource", node_type)
await self._load_node_type_from_datasource(
self.local_datasource,
datasource_role="local",
node_type=node_type,
reason="bootstrap persistence refresh",
)
await self._load_node_type_from_datasource(
self.remote_datasource,
datasource_role="remote",
node_type=node_type,
reason="bootstrap persistence refresh",
)
async def _run_bootstrap_reset_cleanup(self) -> None:
if not self.enable_persistence:
return
if not self.strategies:
raise RuntimeError("no strategies configured: cannot run bootstrap event E01")
total_cleaned = await run_phase2_cleanup(
node_types=self.node_types,
local_collection=self.local_collection,
@@ -388,8 +434,7 @@ class FullSyncPipeline:
runtime=self.sm_runtime,
)
if total_cleaned > 0:
self._logger.info(f"🧹 Reset cleanup: {total_cleaned} CREATE-failed zombies cleaned")
self._logger.info("🔄 Post-load reset cleanup completed")
self._logger.info(f"🧹 Reset cleanup: {total_cleaned} bootstrap zombies cleaned")
async def _apply_preloaded_state(self) -> None:
if self._preloaded_local_nodes:
@@ -30,6 +30,25 @@ async def _resolve_local_id_for_unbind(
return None
async def _has_binding_record(
*,
node_type: str,
any_side_node_id: str,
binding_manager: "BindingManager",
) -> bool:
local_id = await _resolve_local_id_for_unbind(
node_type=node_type,
any_side_node_id=any_side_node_id,
binding_manager=binding_manager,
)
return local_id is not None
def _bootstrap_source_present(node) -> bool:
context = node.context if isinstance(node.context, dict) else {}
return not bool(context.get("_loaded_from_persistence"))
def _is_create_zombie(node) -> bool:
if node.action != SyncAction.CREATE:
return False
@@ -55,8 +74,27 @@ async def _cleanup_one_side(
)
continue
decision = e01_bootstrap(runtime, node=node, is_create_zombie=_is_create_zombie(node))
has_binding_record = await _has_binding_record(
node_type=node_type,
any_side_node_id=node.node_id,
binding_manager=binding_manager,
)
bootstrap_source_present = _bootstrap_source_present(node)
decision = e01_bootstrap(
runtime,
node=node,
is_create_zombie=_is_create_zombie(node),
bootstrap_source_present=bootstrap_source_present,
has_binding_record=has_binding_record,
)
if decision.to_state != "S15":
if decision.to_state == "S17":
node.context.pop("_loaded_from_persistence", None)
logger.warning(
f"[{node_type}] Bootstrap isolated node ({label}): "
f"node_id={node.node_id}, reason={node.error}"
)
continue
local_id_for_unbind = await _resolve_local_id_for_unbind(
@@ -74,9 +112,12 @@ async def _cleanup_one_side(
else:
bind_note = "no_binding"
reason = "status=FAILED" if node.status == SyncStatus.FAILED else "data_id empty"
if _is_create_zombie(node):
reason = "status=FAILED" if node.status == SyncStatus.FAILED else "data_id empty"
else:
reason = "persistence_only_source_missing_without_binding"
logger.info(
f"[{node_type}] Cleaning up CREATE zombie ({label}): "
f"[{node_type}] Cleaning up bootstrap zombie ({label}): "
f"node_id={node.node_id}, reason={reason}, {bind_note}"
)
@@ -118,7 +159,7 @@ async def run_phase2_cleanup(
total_cleaned += cleaned_count
if cleaned_count > 0:
logger.warning(
f"[{node_type}] Cleaned up CREATE-failed zombies: "
f"[{node_type}] Cleaned up bootstrap zombies: "
f"local_deleted={len(cleaned_local)}, remote_deleted={len(cleaned_remote)}"
)