庆典已取消,完成按钮却还是红色
目标
编写一个协调器,接收太空庆典的变更请求,并把它连接到批准、执行、观测、报告、独立补偿。用真实的数据库和 CLI 确认:即使 SQL 操作是对的,协调器出错也会造成的事故。
为什么重要
请在完成前面的批准版本、补偿、证据报告课程之后再进行。需要会使用 Python 函数、dict、list、异常、JSON、CLI。这次不把已经学过的数据库代码全部重写,而是组合提供的操作。预计 140 分钟,所以请在到期前用“+时间”延长。最长 180 分钟,会话结束后文件会消失。需要的代码请另外保存。
环境与产出物
产出物是 /root/change-capstone/coordinator.py。镜像里有 PostgreSQL 16、psycopg 3.2.3、Python 3,不需要额外安装、网络或权限。学员用 postgres 用户写入 /root。
提供的适配器是 /opt/lab/fixtures/change_capstone/operations.py 的 Operations(dsn)。同一文件夹的 revision_ops.py、compensation_ops.py、evidence_ops.py 是从前面三个单元的累积答案生成的库。可以阅读,其中不包含本次协调器的答案。评分器会在本地 labdb 的 UUID 临时 schema 中创建虚构的订单、批准、审计、补偿表,并且只清理自己建立的 schema。使用传入的 ops、DSN、输出路径,不修改 public 或生产数据库。
请求的准确格式
所有请求都是 dict 本身,按 action 只允许下面的键。
- preview:action、change_id、tenant、ids、report_path。
- apply:action、change_id、tenant、targets、approved、report_path。
- report:action、change_id、report_path。
- undo:action、change_id、undo_id、reason、approved。
action 是对应的英文 str 其中之一。change_id、tenant、undo_id 是 str 本身,ASCII 英文、数字、下划线、连字符 1–64 个字符。apply、undo 的 approved 只允许真正的 True,拒绝 1、字符串、False。这个标志不是认证或签名,而是虚构业务的输入契约。
ids 是装有类型恰好为 int 的 1–2147483647 的 list 本身,有 1–16 个,规范化为无重复的升序。targets 是 1–16 个只有 id、revision、qty 三个键的 dict 本身所组成的 list。id 的范围同上,revision 是类型恰好为 int 的 0–2147483646,qty 是类型恰好为 int 的 1–1000。不把 bool 当作整数接受。禁止重复 ID,并生成按 ID 排序的深拷贝。所有函数都不修改原请求。
reason 是 1–200 个字符的 str 本身,禁止首尾空白、控制字符 U+0000–001F 和 U+007F。report_path 是 str 本身的绝对路径,是已有的、自己拥有的目录里的普通文件位置。允许文件不存在,但拒绝符号链接、目录、相对路径,也不自动创建父目录。以可信的、自己拥有的文件夹为前提,这份契约并不能防住所有替换父路径的攻击。
提供的操作契约
每次调用都会打开并清理自己的数据库连接。协调器不要用外部事务把它们包起来,也不要另外关闭连接。不要把值、ID、路径写死,也不要重新实现 SQL。
- ops.Conflict:提供的操作中,批准与当前条件冲突的异常类。
- ops.preview(tenant,ids):当前 pending 对象的 id、revision、qty 列表。不修改业务数据库。
- ops.apply(change_id,tenant,targets):按原批准条件,一并确认取消和审计。新应用为 True,同一 ID、内容的再次调用为 False。不同的批准或当前条件不一致是 ops.Conflict。审计和批准是在一般执行路径上不修改的材料。
- ops.observe(change_id):没有批准则为 None,否则是含 change_id、tenant、targets、report 四个键的 dict。targets 是规范化的原批准,report 是前面单元的证据分析结果。使用只读的一致观测,对损坏的材料以异常告知。
- ops.publish(change_id,report_path):采集新证据,安全地替换报告文件,并返回 SHA-256 str。不修改业务数据库。普通异常时替换前后的文件保留契约,与前面的证据报告课程相同。
- ops.undo(undo_id,change_id,reason):在原审计和当前版本条件相符时,一并确认补偿和审计。新应用为 True,相同补偿为 False,条件冲突为 ops.Conflict。不删除原批准和审计。
评分时也会提供让每个方法抛出 Exception 或做出契约之外返回的替身。只把校验过的请求传过去之后的操作错误,归类为各阶段的状态。这并不是让你捕获 KeyboardInterrupt、SystemExit 这类全部的 BaseException。ops.Conflict 与学员自己的 Conflict 是不同的类。
协调结果与 CLI 契约
apply 的 dispatch 结果有 change_id、execution、observed、report、sha256 五个键。execution 是 applied、replayed、conflict、uncertain 之一,不因观测结果而抹掉或改变。observed 是 complete、incomplete、hold、unknown、mismatch、not_checked。report 是 saved、failed、not_attempted,保存失败或未尝试时 sha256=None。
preview 的 dispatch 只返回 approved=False 的 apply 提议。report 的 dispatch 返回 change_id 和 report 函数结果这三个键,undo 的 dispatch 返回 compensate 结果的三个键。dispatch 要先校验请求,并遵守各动作的边界。即使是 applied,只要观测是 unknown,就不尝试报告;即使是 replayed,如果观测是 hold,也可以原样报告那个结果。
decode(raw) 把类型恰好为 bytes 的 1–65536 个字节按 UTF-8 JSON 解析,并返回经过 validate 的请求。重复键、非有限常量、被截断的 JSON、错误的 UTF-8、过大的输入是 ValueError。main(argv,ops) 只接收一个请求文件路径,最多读取 65537 个字节,然后 decode→dispatch。成功时,把一行结果 JSON 输出到 stdout 并返回 0。如果参数、读取、解析、处理出现 Exception,则返回一行 {"error":"request_failed"} 和 2,不输出 DSN、原始异常和 traceback。
以脚本运行时,要把 /opt/lab/fixtures/change_capstone 加入 sys.path,并读取 operations.Operations。用环境变量 LABHUB_CAPSTONE_DSN 里的仅限本地的 DSN 创建 Operations,并以 main(sys.argv[1:],ops) 的返回值退出。这个环境变量在正常的 CLI 验证中一定会提供。作为模块被 import 时,不能运行 CLI。CLI 的代码 0 是请求处理成功,并不意味着业务完成。
观测和报告材料的采集是另外的调用。observed 是之前的观测,report 是之后文件的保存状态,不保证两次调用在同一时点。不要仅因为 saved,就假定文件里的业务结论是 complete。请另外确认报告的实际观测时间和分类。
步骤
- 只把明确的批准当作输入——实现 Conflict(Exception) 和 validate(value)。检查下面按动作区分的准确的键、类型、范围,并返回把列表排序后的深拷贝。apply、undo 只有在 approved 是真正的 True 时才允许,错误是 ValueError。
- 不自动批准预览——preview(value,ops) 校验 preview 请求,并以排序后的 ids 调用一次 ops.preview(tenant,...)。校验并排序收到的 targets,核对 ID 列表是否与请求完全相同。不同则是 Conflict。相同则返回 action=apply、原 change_id、tenant、report_path、观测的 targets、approved=False 的提议 dict。
- 区分同一个 ID 的不同批准——compare(value,observation) 校验 apply 请求。observation 是 None 则返回 unknown。否则,在下面的观测格式中,核对 change_id、tenant、规范化 targets 是否与请求相同,不同则是 Conflict。相同则原样返回 report.decision 的 complete、incomplete、hold。其他顶层键或未知判定是 Conflict。
- 把执行响应和结果不明分开——execute(value,ops) 校验 apply 请求之后,恰好调用一次 ops.apply(change_id,tenant,targets)。True 是 applied,False 是 replayed,ops.Conflict 是 conflict,其他 Exception 或非 bool 的返回是 uncertain。调用之外的校验错误原样传递。
- 观测已确认的记录,把不确定性暴露出来——settle(value,ops,outcome) 校验 apply 请求和四种执行结果值。如果是 conflict,不调用 observe,observed=not_checked。其他情况调用一次 ops.observe(change_id),并用 compare 确定 observed。查询的 Exception 是 unknown,比较中的 Conflict、ValueError、KeyError、TypeError 是 mismatch。返回 change_id、execution=原 outcome、observed 的 dict。
- 不碰业务,重新生成报告——report(value,ops) 只允许 apply 或 report 请求。调用一次 ops.publish(change_id,report_path),如果返回小写 64 位 SHA-256 str,就返回 report=saved、sha256=该值的 dict;调用 Exception 或错误的返回,则返回 report=failed、sha256=None 的 dict。
- 只执行另外批准的补偿——compensate(value,ops) 校验 undo 请求之后,调用一次 ops.undo(undo_id,change_id,reason)。把 True、False、ops.Conflict、其他 Exception 或返回,分别归类为 applied、replayed、conflict、uncertain。返回 change_id、undo_id、compensation 的 dict,不做后续调用。
- 把真实的 JSON 请求和 CLI 贯通到底——按下面的契约实现 dispatch(value,ops)、decode(raw)、main(argv,ops) 和 CLI 入口。apply 在 execute→settle 之后,只对 complete、incomplete、hold 的观测调用 report。其余情况是 report=not_attempted、sha256=None。其他三个动作只调用对应的函数。用真实的数据库、文件、CLI 检查整个流程。
参考
- 累积的直接诊断:python3 -B /opt/lab/fixtures/change_capstone/check.py 8 /root/change-capstone/coordinator.py。请把数字换成当前的步骤。一次评分的限制是 40 秒。
- 通过真实的调用记录,确认 report 模式下是否调用了 apply 或 undo、preview 是否造出了 True 批准、是否允许了同一 ID 的不同内容。
- 最终步骤会用单独的写入连接修改第二笔订单,并确认过期批准的整体回滚。会同时注入提交之后丢失响应和报告替换之前的异常,并分别确认订单、审计、文件。
- 验证的是普通异常注入以及真实数据库和 CLI,并不证明真实的网络中断、数据库服务器终止、断电的耐久性。提供的数据库是学习用的,fsync、full_page_writes 是关闭的。
- 本课不提供用户认证、对其他客户的访问权限、电子批准签名、外部发送。不要放入真实的个人信息和生产 DSN。uncertain 的补偿要接续前面课程中的回执、当前状态确认,不要用其他 ID 自动重试。
只把明确的批准当作输入
实现 Conflict(Exception) 和 validate(value)。检查下面按动作区分的准确的键、类型、范围,并返回把列表排序后的深拷贝。apply、undo 只有在 approved 是真正的 True 时才允许,错误是 ValueError。
bool 是 int 的子类型。有真值,与明确的批准,是两回事。
不自动批准预览
preview(value,ops) 校验 preview 请求,并以排序后的 ids 调用一次 ops.preview(tenant,...)。校验并排序收到的 targets,核对 ID 列表是否与请求完全相同。不同则是 Conflict。相同则返回 action=apply、原 change_id、tenant、report_path、观测的 targets、approved=False 的提议 dict。
提议必须无法通过 validate 直接执行。不要在这里调用执行、补偿、报告操作。
区分同一个 ID 的不同批准
compare(value,observation) 校验 apply 请求。observation 是 None 则返回 unknown。否则,在下面的观测格式中,核对 change_id、tenant、规范化 targets 是否与请求相同,不同则是 Conflict。相同则原样返回 report.decision 的 complete、incomplete、hold。其他顶层键或未知判定是 Conflict。
提供的 observe 会执行前面单元的证据采集和分类。这次的职责,是确认那次观测与当前批准是否相同。
把执行响应和结果不明分开
execute(value,ops) 校验 apply 请求之后,恰好调用一次 ops.apply(change_id,tenant,targets)。True 是 applied,False 是 replayed,ops.Conflict 是 conflict,其他 Exception 或非 bool 的返回是 uncertain。调用之外的校验错误原样传递。
即使收到了异常,也可能已经提交。不要创造新的 ID、新的目标和自动重试。
观测已确认的记录,把不确定性暴露出来
settle(value,ops,outcome) 校验 apply 请求和四种执行结果值。如果是 conflict,不调用 observe,observed=not_checked。其他情况调用一次 ops.observe(change_id),并用 compare 确定 observed。查询的 Exception 是 unknown,比较中的 Conflict、ValueError、KeyError、TypeError 是 mismatch。返回 change_id、execution=原 outcome、observed 的 dict。
不要把查询失败和“是另一个批准”这个事实合并成同一个状态。这一步不做写入操作。
不碰业务,重新生成报告
report(value,ops) 只允许 apply 或 report 请求。调用一次 ops.publish(change_id,report_path),如果返回小写 64 位 SHA-256 str,就返回 report=saved、sha256=该值的 dict;调用 Exception 或错误的返回,则返回 report=failed、sha256=None 的 dict。
不重新解读执行响应,也不做补偿。即使保存失败之后,也不要追加调用业务操作。
只执行另外批准的补偿
compensate(value,ops) 校验 undo 请求之后,调用一次 ops.undo(undo_id,change_id,reason)。把 True、False、ops.Conflict、其他 Exception 或返回,分别归类为 applied、replayed、conflict、uncertain。返回 change_id、undo_id、compensation 的 dict,不做后续调用。
原批准并不意味着补偿的批准。补偿的异常也不要用新的 undo_id 自动重试。
把真实的 JSON 请求和 CLI 贯通到底
按下面的契约实现 dispatch(value,ops)、decode(raw)、main(argv,ops) 和 CLI 入口。apply 在 execute→settle 之后,只对 complete、incomplete、hold 的观测调用 report。其余情况是 report=not_attempted、sha256=None。其他三个动作只调用对应的函数。用真实的数据库、文件、CLI 检查整个流程。
退出码 0 不是业务完成,而是请求处理成功。要分别读取 JSON 中的执行、观测、文件状态。