> 优雅降级设计好了,却没人被告知——退化必须在产物上留痕
同一个项目在两个月里撞了三次同一个失败:系统按设计降级了, 产物看起来仍然合法,于是没人知道。
结论先行¶
同一个项目在两个月里撞了三次同一个失败:系统按设计降级了, 产物看起来仍然合法,于是没人知道。
① cron 的 PATH 只有 /usr/bin:/bin。 要调的 CLI 在
~/.local/bin,于是每天 FileNotFoundError,收据静默退化成纯证据行。
手动跑一切正常,只有 cron 挂 —— 四天。
这正是本项目要防的那类失败:退化是设计好的,但没人被告知。
② 解析器归零的时候报的是绿灯。 一个每天 8:57 跑的截止日期提醒,
连续多天报"🟢 No upcoming deadlines",而当时有一个会议只剩 1 天、
另一个已经逾期。原因是三处独立的格式不匹配(缺小节、缺某个前缀行、
日期写成 09-09 而不是 9/9),任意一处都足以让它归零。
③ 产出每天都在写,但没有一步会提交它。 23 天的数据只在工作树里, 日志里一个字都没有(见 进 git 的判据只有一句:明天这个仓库能不能把它重算出来)。
三次都是同一个形状:降级后的输出是一个合法的输出。
为什么¶
"优雅降级"这个词把两件事绑在了一起,而只有第一件被实现了:
- 出问题时继续产出有用的东西 —— 这是降级的目的。
- 让消费者知道这是降级的产出 —— 这是降级能被接受的前提。
少了第 2 条,降级就从"韧性"变成"静默劣化"。而且它比直接崩溃更难发现, 因为崩溃有一个明确的时刻和一条栈,静默降级只有一段"看起来略薄"的历史, 而你并不知道正常的厚度是多少。
第 ② 条值得单独说:"解析出 0 条"是坏解析器最可能的输出, 而它和"真的什么都没有"在显示上通常是同一个符号。 一个刮 markdown 的解析器,它的格式依赖有几处,就有几个单点故障, 而每一个都失败成零,不是失败成报错。
PATH 那条的诊断线索也值得记:手动跑正常、cron 挂,
是环境问题而不是代码问题的强信号。 cron 的环境是所有执行环境里最瘦的一个
(没有 .bashrc、没有 .profile、PATH 两项、没有交互式 shell 的任何设置)。
怎么用¶
降级必须在产物上可见。 判据是:拿到产物的人只看产物, 能不能分辨这次是完整跑还是降级跑。做法按产物类型:
| 产物 | 留痕方式 |
|---|---|
| 一页纸/报告 | 整节消失(不是印"不可用"),或顶部一行 DEGRADED: <原因> |
| 文件 | 文件名或内容里带标记;stages.error 里记原话 |
| CLI | 退出码分档:0 正常 / 2 "判不出来" / 5 "拒绝" |
| 日报类 | "解析出 0 条" 换一个符号,而且 exit 2 |
"0 条"和"一切正常"在显示上必须不同。 那个截止提醒的修法有两步,
顺序重要:先把 0 条从 🟢 改成 ⚠️ 并 exit 2(治症状,立刻生效),
再不刮 markdown 了,直接读数据库字段(治病因)。
—— 结构化的真相源只有一个格式依赖,人写的文档有无数个。
同 断言"它拒绝了"不能用非零退出码——任何东西都会非零退出:
断言"它退出了"和断言"它拒绝了"是两件事。
在代码里解析绝对路径,不要指望 crontab 带对 PATH。
configured = cfg["digest"].get("claude_binary")
if configured: return os.path.expanduser(configured)
found = shutil.which("claude")
if found: return found
fallback = os.path.expanduser("~/.local/bin/claude")
return fallback if os.path.exists(fallback) else "claude"
比在 crontab 里写 PATH=... 好:它对任何启动器都成立
(systemd timer、udev、CI、别人的脚本),而不只是对这一个 crontab。
"做完了"的标记必须记下是谁做完的。 这个项目的
spool/.printed/<date> 第一行记着是哪个后端写的。file 后端写的标记
只代表"字节进了 spool",不代表"纸出来了" —— 所以后端一改成真打印机,
这些天自动回到队列:
python3 printit.py --list-pending # backend=file 时为空;改成 usb 后 13 天全部回来
原先这里要手动 rm -rf spool/.printed/:忘了就静默丢天,清过头就重复出纸,
两个方向都是坑。一个只记"完成了"的幂等标记,在"完成的含义变了"之后
就是错的;记下完成它的方式,标记就能自己失效。
这是比"幂等"更强的一个性质,而且几乎不要钱:多存一行字符串。
补跑要能指定范围。 四天的叙述是事后补跑的;一个只能"全量重跑"
的补偿入口会顺带覆盖掉别的东西(我自己在另一处就踩过:一个
--restore 没有选择参数,于是恢复了全部,而我只想恢复一条)。
出处¶
~/Engineering/worklog-printer:digest.py:141-157(_claude_binary
与那段解释 08-03…08-06 四天静默降级的 docstring)、
README.md §0 的三小节(cron PATH、假标记、deadline-parse 的静默失败)
与 §2「归档进 git」里"这个项目自己那个老毛病换了张脸:静默退化"、
printit.py 的 already_printed()。