gapfill G7: make _merge_task status commit and success audit atomic (same connection, audit_on_conn, one commit; audit failure rolls back to completed instead of merged-plus-500; ledger-head IntegrityError retries whole segment 5x mirroring AuthStore.audit); success semantics unchanged

This commit is contained in:
host committed 2026-11-04 09:16:25 +00:00
1 parent 7524a779f2
commit 5d0651e70a
2 files changed
+223 -8

No files matched your search

+39 -8
View File
@@ -68,6 +68,7 @@ from cloud.quota import quota_for_owner as _quota_for_owner
from cloud.timeouts import STALE_SWEEP_SECONDS as _STALE_SWEEP
from cloud.timeouts import approval_default_seconds as _approval_default
from cloud.timeouts import validate_server_timeout as _validate_timeout
from cloud.auth import audit_on_conn as _audit_on_conn
from cloud.db import connect as _db_connect
from cloud.db import create_database as _create_database
from cloud.db import init_postgres_schema as _init_postgres_schema
@@ -6578,15 +6579,47 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
"merge_gate": gate,
})
return
# G7(A4 时序残留):状态提交与成功审计必须同一事务。
# 旧写法先 commit 状态、再另开 AuthStore 连接写审计:
# 审计失败时状态已是 merged、响应却是 500——“合并了却
# 报失败”,调用方无法靠状态判断,也无法安全重试
# (重复合并 409)。新写法用 audit_on_conn 把审计对
# 写进同一连接、一次 commit:审计失败则整段回滚,状态
# 仍是 completed、响应 500(_dispatch_safe 兜底),调用
# 方可原样重试合并。链头唯一冲突的重试口径与
# AuthStore.audit 完全一致(5 次、只重试
# IntegrityError,其它错误直接抛)。成功语义不变:
# 200 体、审计内容(merge_task/task_id)、门禁响应全同。
# 前提:tasks 与 audit_log/audit_ledger 在同一库
# (_db 与 _auth_store 都连 server.db_path;PG 模式下
# _connect 同样分发到同一 PG 库,? 占位由适配层转 %s)。
now = time.time()
conn = self._db()
try:
conn.execute(
"UPDATE tasks SET status = 'merged', updated_at = ?"
" WHERE id = ?",
(now, task_id),
)
conn.commit()
last_exc = None
for _attempt in range(5):
try:
conn.execute(
"UPDATE tasks SET status = 'merged',"
" updated_at = ? WHERE id = ?",
(now, task_id),
)
_audit_on_conn(conn, user_id, task["project_id"],
"merge_task", task_id)
conn.commit()
break
except Exception as exc:
last_exc = exc
try:
conn.rollback()
except Exception:
pass
if not isinstance(
exc, (sqlite3.IntegrityError,
_PgIntegrityError)):
raise
else:
raise last_exc
task = _task_to_dict(
conn.execute(
"SELECT * FROM tasks WHERE id = ?", (task_id,)
@@ -6594,8 +6627,6 @@ class CloudControlPlaneHandler(OAuthMixin, IdentityMemoryMixin, ProjectMixin, Au
)
finally:
conn.close()
with _auth_store(self.server.db_path) as store:
store.audit(user_id, task["project_id"], "merge_task", task_id)
task["merge_gate"] = gate
self._send_json(200, task)