- cloud/evidence_binding.py: bind/validate/nonce single-use store; release endpoint consumes nonce on first validated submission, replay -> 409 evidence_replayed, stale/future -> 409 evidence_stale, partial -> 400, absent -> old path + label. Binding blocks exact replay and stale evidence, NOT restamping (unsigned evidence cannot prove content freshness). - cloud/quota.py docstring: actual-only is the v2 integration position (already converged in v1 base via fef09ae, NOT in main); original PR semantics (#86 all-rows vs #90 actual-only) still pending coordinator confirmation at merge time. - cloud/ledger_onboarding.py: DDL checklist referencing PG_DDL/PG_MARKER_DDL constants, read-only verify_ledger_ready and ledger_setup_report; default stays OFF, PG DDL stays an operator action (never claimed as executed). - cloud/ledger_outbox.py + worker hook: ledger write failures go to a JSONL outbox in addition to the log and can be replayed; task outcome still unaffected (rule 7 kept), audit fail-closed untouched -- asymmetry documented, not silently changed. - cloud/gate_verification.py: read-only verifier over an injected reader (match/mismatch/skipped); live wiring in merge/release is NOT possible on the current contract (no repo/ref in body or task row, no guaranteed trusted token), proven in the module header; no fake verification claimed. - cloud/tests/test_v2_evidence.py: 18 new tests, all passing. (cherry picked from commit bd85e33cd5e846fa6ee5d7e2440182dbd9c758b0)
191 lines
7.9 KiB
Python
191 lines
7.9 KiB
Python
"""台账钩子开箱闭合方案(集成 v2,代码层)。
|
||
|
||
首轮缺口(成本与台账 8.5 扣 0.5):Worker(attempt_ledger_hook
|
||
=False) 默认关,CLI 须显式 --attempt-ledger-hook;生产 PG
|
||
上启用前须先手动执行台账 PG_DDL 与标记表 PG_MARKER_DDL
|
||
并验证。默认状态下指标率是 unknown(诚实),不是“已用
|
||
真实台账”——“接通”只在“开 flag + 手动跑 DDL + 验证”
|
||
之后成立,开箱未闭合。
|
||
|
||
本轨的闭合方案(不改任何已定语义):
|
||
|
||
1. 默认行为不变:钩子默认仍关。默认关是 #255 的设计
|
||
(显式启用才写行,关时行为与 gap 3 完全一致、无新表
|
||
无新行),不是 bug,不为“开箱即用”悄悄改成默认开。
|
||
把默认改成开,是用户/协调员在合轨时才能定的语义变
|
||
更,本轨不做。
|
||
2. 显式开启路径唯一且可验证:Worker(attempt_ledger_hook
|
||
=True) 或 CLI --attempt-ledger-hook;开启后 run_one
|
||
按 attempt_hook.py 规则 1-4 写行。本模块的
|
||
ledger_setup_report() 把“表是否齐、钩子是否开”分开
|
||
报告:表齐但钩子关 ≠ 就绪,钩子开但表不齐 ≠ 就绪。
|
||
3. DDL 清单不另造:清单直接引用 attempt_ledger.PG_DDL
|
||
与 attempt_hook.PG_MARKER_DDL 两个常量(本模块不
|
||
复制 DDL 文本,复制就会漂移)。另含 gap 3 旧表升级句
|
||
attempt_ledger.OLD_TABLE_ALTER(仅当旧 DDL 建过表时
|
||
需要;gap 3 未合并,生产尚无旧表,属防御项)。
|
||
4. 校验函数 verify_ledger_ready(conn):只读探针——查两
|
||
张表的存在与必需列,不建表、不写行(读路径绝不改
|
||
schema,与 metrics-report 端点的只读纪律一致)。
|
||
5. 生产 PG 手动 DDL 仍是运维侧动作:本轨无生产 PG、无
|
||
本地 PG,PG 证据与此前各轨同款——适配器级(? 占位符
|
||
经 cloud.db 翻译、DDL 仅用 PG 合法类型、无自增列),
|
||
绝不写成“生产 PG 已执行”。运维执行清单见
|
||
LEDGER_DDL_CHECKLIST(顺序执行 + 每步验证命令)。
|
||
|
||
零第三方依赖,只用标准库。conn 约定与 attempt_ledger
|
||
相同:DBAPI 连接,? 占位符(PG 经 cloud.db 翻译)。
|
||
"""
|
||
|
||
from cloud.attempt_hook import MARKER_DDL as _MARKER_DDL
|
||
from cloud.attempt_hook import PG_MARKER_DDL as _PG_MARKER_DDL
|
||
from cloud.attempt_ledger import OLD_TABLE_ALTER as _OLD_TABLE_ALTER
|
||
from cloud.attempt_ledger import PG_DDL as _LEDGER_PG_DDL
|
||
|
||
# 必需列:缺任一列即视为表不齐(旧 DDL 建的表缺
|
||
# corrects_attempt_no 时走 OLD_TABLE_ALTER 补列)。
|
||
LEDGER_REQUIRED_COLUMNS = (
|
||
"task_id", "attempt_no", "outcome", "is_defect",
|
||
"defect_category", "created_at", "corrects_attempt_no",
|
||
)
|
||
MARKER_REQUIRED_COLUMNS = ("task_id", "origin", "queued_at")
|
||
|
||
# 运维执行清单(顺序):每步含“做什么 + 怎么验证”。本
|
||
# 清单是文档数据,不是执行器——生产 PG 上的执行仍由
|
||
# 运维手工完成,本模块不代执行、不连生产库。
|
||
LEDGER_DDL_CHECKLIST = (
|
||
{
|
||
"step": 1,
|
||
"action": "在生产 PG 上执行 attempt_ledger.PG_DDL"
|
||
"(建 task_attempts 表)",
|
||
"ddl_source": "cloud.attempt_ledger.PG_DDL",
|
||
"verify": "\\d task_attempts 或 information_schema"
|
||
" 查表与列齐全",
|
||
},
|
||
{
|
||
"step": 2,
|
||
"action": "在生产 PG 上执行 attempt_hook.PG_MARKER_DDL"
|
||
"(建 task_attempt_origin_markers 表)",
|
||
"ddl_source": "cloud.attempt_hook.PG_MARKER_DDL",
|
||
"verify": "\\d task_attempt_origin_markers 或"
|
||
" information_schema 查表与列齐全",
|
||
},
|
||
{
|
||
"step": 3,
|
||
"action": "仅当表由 gap 3 旧 DDL 建过时:执行"
|
||
" attempt_ledger.OLD_TABLE_ALTER 补"
|
||
" corrects_attempt_no 列(gap 3 未合并,"
|
||
"生产正常路径跳过此步)",
|
||
"ddl_source": "cloud.attempt_ledger.OLD_TABLE_ALTER",
|
||
"verify": "task_attempts 含 corrects_attempt_no 列",
|
||
},
|
||
{
|
||
"step": 4,
|
||
"action": "启用钩子:worker 加 --attempt-ledger-hook"
|
||
"(或 Worker(attempt_ledger_hook=True))后重启",
|
||
"ddl_source": None,
|
||
"verify": "新派发产生 task_attempts 行;"
|
||
"平台重派发不产生计数行(marker 生效)",
|
||
},
|
||
{
|
||
"step": 5,
|
||
"action": "用 verify_ledger_ready(conn) 复核:"
|
||
"tables_ready 且 hook_enabled 方为就绪",
|
||
"ddl_source": None,
|
||
"verify": "ledger_setup_report(conn, hook_enabled=True)"
|
||
" 的 ready 为 True",
|
||
},
|
||
)
|
||
|
||
|
||
def ledger_ddl_sources():
|
||
"""DDL 清单的权威来源(返回常量本身,供运维原样执行
|
||
与单测钉住“清单与实现同一来源”,防止清单漂移)。"""
|
||
return {
|
||
"task_attempts_pg_ddl": _LEDGER_PG_DDL,
|
||
"marker_pg_ddl": _PG_MARKER_DDL,
|
||
"old_table_alter": _OLD_TABLE_ALTER,
|
||
"marker_ddl": _MARKER_DDL,
|
||
}
|
||
|
||
|
||
def _table_columns(conn, table):
|
||
"""只读探针:表不存在返回 None;存在返回列名集合。
|
||
|
||
PRAGMA table_info 经 cloud.db 适配层翻译(与
|
||
CostStore._ensure_cached_column 同款做法);探针失败
|
||
(如 PG 适配差异)返回 None,按“不齐”处理,绝不
|
||
按“齐”冒充。
|
||
"""
|
||
try:
|
||
rows = conn.execute(
|
||
"PRAGMA table_info(%s)" % table).fetchall()
|
||
except Exception:
|
||
return None
|
||
if not rows:
|
||
return None
|
||
try:
|
||
return {row[1] for row in rows}
|
||
except (IndexError, TypeError, KeyError):
|
||
return None
|
||
|
||
|
||
def verify_ledger_ready(conn):
|
||
"""只读校验两张台账表:返回 {tables_ready, missing}。
|
||
|
||
missing 列出缺失项,如 "task_attempts table missing"、
|
||
"task_attempts missing columns: corrects_attempt_no"、
|
||
"task_attempt_origin_markers table missing"。本函数不
|
||
建表、不写行、不提交。
|
||
"""
|
||
missing = []
|
||
ledger_cols = _table_columns(conn, "task_attempts")
|
||
if ledger_cols is None:
|
||
missing.append("task_attempts table missing")
|
||
else:
|
||
absent = [c for c in LEDGER_REQUIRED_COLUMNS
|
||
if c not in ledger_cols]
|
||
if absent:
|
||
missing.append("task_attempts missing columns: %s"
|
||
% ", ".join(absent))
|
||
marker_cols = _table_columns(conn, "task_attempt_origin_markers")
|
||
if marker_cols is None:
|
||
missing.append("task_attempt_origin_markers table missing")
|
||
else:
|
||
absent = [c for c in MARKER_REQUIRED_COLUMNS
|
||
if c not in marker_cols]
|
||
if absent:
|
||
missing.append(
|
||
"task_attempt_origin_markers missing columns: %s"
|
||
% ", ".join(absent))
|
||
return {"tables_ready": not missing, "missing": missing}
|
||
|
||
|
||
def ledger_setup_report(conn, hook_enabled=False):
|
||
"""开箱就绪报告:表齐 + 钩子开,两者缺一都不算就绪。
|
||
|
||
返回 {ready, tables_ready, hook_enabled, missing,
|
||
next_steps}:next_steps 是运维下一步动作的文字清单
|
||
(按 LEDGER_DDL_CHECKLIST 顺序,未做项才列出),
|
||
不是自动执行。本函数同样只读,不建表、不写行。
|
||
"""
|
||
check = verify_ledger_ready(conn)
|
||
enabled = bool(hook_enabled)
|
||
steps = []
|
||
if check["missing"]:
|
||
steps.append("run the PG DDL steps in LEDGER_DDL_CHECKLIST"
|
||
" (steps 1-3) on production Postgres and verify")
|
||
if not enabled:
|
||
steps.append("enable the hook with --attempt-ledger-hook"
|
||
" (Worker(attempt_ledger_hook=True)) and restart")
|
||
if not steps:
|
||
steps.append("ledger is live: counted dispatches write rows;"
|
||
" rates are real, not unknown")
|
||
return {
|
||
"ready": bool(check["tables_ready"] and enabled),
|
||
"tables_ready": check["tables_ready"],
|
||
"hook_enabled": enabled,
|
||
"missing": check["missing"],
|
||
"next_steps": steps,
|
||
}
|