周六早上定下的砍需求决定,撑不过四个小时。到了周日晚上,这个决定就变成了一场和疲惫的人的谈判——对方只想让演示看起来完整。半成品功能被重新打开,然后悄悄把演示搞崩,往往就发生在这两个状态之间的缝隙里。

有个周末项目的做法是:把过期时间戳直接写进每一个功能开关。功能不是"暂时关掉",而是"周日21:00 UTC之后自动关闭",而且这个事实躺在源码里。不需要凌晨一点重新部署,也不需要谁记住口头承诺。

演示本身故意做得很小:一个文本工具,三个依赖AI的功能,跑在一台免费服务器实例上。约束才是重点——两天日历时间、一个进程,以及一份不该花在没做完的分支上的模型调用预算。

四个活动部件

  • 一个冻结的 Feature 记录,携带 expires_at 和预估的 model_calls 成本。
  • 一个 FastAPI 依赖,时钟一过就返回 503 和兜底提示。
  • 一个测试,不用等真实时间流逝就能验证过期路径。
  • 一个部署前检查,开关活得比演示窗口还久就直接失败。

注册表刻意保持无聊:冻结的数据类、显式的 UTC 时间戳,以及规划者之后能读懂的成本估算。Feature 记录里带一个 open_now 方法,拿当前时间和 expires_at 比大小,就这么简单。

依赖层负责把"过期"翻译成 HTTP 语义。开关还开着,请求正常往下走;时钟过了,直接 503 加一句兜底文案。测试则把时钟当成参数传进去,伪造一个过期后的时间点,断言返回的是 503——不用真的等到周日晚上。

部署前检查是最后一道闸。它遍历所有开关,找出 expires_at 超出演示窗口加宽限期的那些,打印出名字然后以退出码 1 结束。本地跑完整套流程大约一分钟:建虚拟环境、装 FastAPI 和 pytest、跑检查脚本、起服务、curl 一个 POST 请求。

返回 200 说明开关还开着,503 说明砍需求已经生效,而检查脚本失败说明这个砍需求压根没被排期。

砍的顺序比砍什么更重要

给砍需求排序,是大多数人会跳过的一步。这个项目用的规则是分层:第一层先砍,因为那是唯一一个半残功能能在几分钟内把请求预算抽干的层级;第三层没有边际成本,砍了也换不来什么。

免费服务器加免费模型访问改变了这笔账。模型调用是计费的那部分,一个每次请求扇出三次调用的路由,和一个只做文本格式化的路由,属于不同的风险等级,注册表应该把这件事写清楚。

有两点需要提醒。免费额度、模型列表和实例条款随时可能变,具体细节应该去项目页面确认,而不是相信一篇博客里的数字——包括这一篇。另外,免费层不是可靠性保证;过期设计本身就假设进程随时可能消失,这个假设即使在付费基础设施上也值得保留。

被跳过的部分

  • 流式响应。砍掉的开关返回完整的 503,半截流式回答比干净拒绝的体验更糟。
  • 按用户定向。没有百分比放量,没有白名单,一个时钟一个决定。
  • 开关持久化。开关住在源码里,不在数据库里,改一个就是一次提交。
  • 模型调用的重试和退避。两天工期里属于范围外,而且开关让风险路由不需要它。
  • 指标面板。服务器日志里的 503 计数就是面板。

源码里的过期时间不是一套功能开关系统。它没有审计日志,没有灰度放量,也没有比重部署更快的急停开关。生命周期长的功能应该用真正的开关服务,而不是数据类里的时间戳。

功能必须活过周末的产品,根本不该编码一个硬停止——上线后还得改的过期时间,本质上就是一次计划内的故障。多实例且缓存配置的环境会出现行为偏差,因为每个进程读的是自己那份 FEATURES。请求进行到一半时过期也不处理:20:59:59 开始的请求会跑完,只有下一个请求被拒。

成本控制是按功能算的,不是按请求算的。model_calls 字段用来给砍需求排序,它不封顶开销。

这个模式能活过周末的原因很简单:它在代码里把那个无聊的决定做了一次,然后就不再问了。如果下一个演示快到了,从检查脚本开始写,在写功能之前先把过期时间写好。