news 2026/9/15 11:04:12

Polar Ship Safety:面向非原子化部署的代码评审安全核查实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Polar Ship Safety:面向非原子化部署的代码评审安全核查实战指南

Polar Ship Safety:面向非原子化部署的代码评审安全核查实战指南

【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar

导读

本文围绕 Polar 开源仓库(Billing platform for the intelligence era)中的部署安全评审 Skill——ship-safety(见 .agents/skills/ship-safety/SKILL.md)展开。它回答一个非常具体的问题:在一个迁移先跑、API 先于 Worker 上线、旧前端可能对着新后端运行、队列里还残留旧代码入队任务的非原子化部署环境里,一个 diff 在「合并瞬间」到「完全部署完成」之间到底会不会把生产环境打挂。读完本文,你将掌握一套可执行的五类检查清单(Schema 先于代码、阻塞性 DDL、在途任务、锁与吞吐量、是否拆分 PR),并能在合并涉及迁移、任务、模型、端点删除的 PR 之前,产出一份规范的 Ship Safety 评审报告。


一、为什么需要 Ship Safety:Polar 的部署窗口

Ship Safety 的核心句是:handle 是 diff,证据是「合并那一刻到完全部署完成之间到底什么会坏」(见 .agents/skills/ship-safety/SKILL.md)。

Polar 并不做原子化部署,部署窗口里存在四类真实风险:

  1. 迁移先于代码执行:migrations 先跑,随后 API 才上线。
  2. API 先于 Worker 部署:API 先于 worker 上线,worker 滞后。
  3. 旧前端可以对着新后端运行:前端与后端发布不同步。
  4. 队列中残留旧名字入队的任务:合并时队列里已经躺着由旧代码按旧 actor_name、旧参数入队的任务。

因此在最终状态下「逻辑正确」的代码,在通往最终状态的路上依然可能把生产环境打崩。Ship Safety 检查的就是这段"在路上"的窗口。

适用范围(Scope)

需要跑 Ship Safety 的 diff 覆盖以下五类(.agents/skills/ship-safety/SKILL.md):

  • 触及server/migrations/versions/的迁移文件;
  • 触及**/tasks.py的任务代码;
  • 触及polar/models/的模型;
  • 触及server/scripts/的运维脚本;
  • 删除或重命名了某个 endpoint;
  • 同时跨越server/clients/且二者存在依赖关系的改动。

也就是说,只要 PR 动了数据库结构、异步任务、模型、脚本,或动了对外 API 契约,就应当跑一遍 Ship Safety。

与 ADR-0006 的职责边界

文档明确划定了"Owned elsewhere"(由其他机制负责、不要重复阐述)的部分(.agents/skills/ship-safety/SKILL.md):

  • 迁移规则ADR-0006覆盖:锁超时、nullable → 批量回填脚本(run_batched_update)→ 跨多个 PR 再收紧为 NOT NULL、enforce 迁移中的无条件UPDATE、迁移 PR 与代码 PR 隔离。CI 通过Migration Isolation Check强制执行,adr-check报告违规;
  • 重复造轮子的 helperreuse-check负责;
  • 计费相关的锁与循环规则billing-review负责。

Ship Safety 只负责ADR-0006 没有说到的剩余部分——即上面的部署窗口本身。


二、检查 1:Schema 先于代码(Schema ahead of code)

ADR-0006 阻止了「代码跑在 schema 之前」,但反过来依然会咬人:迁移和新代码之间,旧代码会跑在新 schema 上(.agents/skills/ship-safety/SKILL.md)。

两条硬规则:

  • 删除列或表会立即打挂正在运行的应用。原因在源码层面有明确依据:SQLAlchemy 在 import 时就会把每个模型映射到表(polar/models/下的模型文件会在模块加载阶段建立表映射),所以一张被 drop 的表可能在任何查询执行之前就导致导入失败。正确顺序是:先停止使用该列/表 → 部署 → 在后续的另一个 PR里再 drop。
  • 重命名永远是两步走:加新名 → 部署 → 删旧名,拆成两个 PR。

对每一个 schema 变更都要问同一个问题:当前已部署的旧代码,在迁移执行后依然正确吗?

源码佐证:Polar 的迁移模板在upgrade()/downgrade()开头都强制执行SET LOCAL lock_timeout = '5s'(见 server/migrations/script.py.mako),而 ADR-0006 的完整决策记录在 handbook/engineering/decisions/0006-migration-and-backfill-safety.mdx。生成迁移的入口命令是alembic revision --autogenerate -m "your message"(见 server/migrations/README.md)。


三、检查 2:阻塞性 DDL(Blocking DDL)

针对大表上的结构性变更,Ship Safety 要求给出明确的锁行为判断(.agents/skills/ship-safety/SKILL.md):

  • 大表建索引必须带postgresql_concurrently=True,且迁移必须放在事务外执行(CONCURRENTLY 不能在事务块内跑)。
  • 大表加 NOT NULL:优先CHECK ... NOT VALIDVALIDATE CONSTRAINT,让 Postgres 跳过全表锁(ACCESS EXCLUSIVE)。如果表很小,这套操作就是仪式感——评审时要明确说明你认为哪种情况适用。
  • 涉及金额(money)表的新外键必须带ondelete="restrict",防止级联删除破坏财务数据完整性。

这也是 ADR-0006 决策背景的直接体现:阻塞性ALTER或未分批的UPDATE在热表上会拿ACCESS EXCLUSIVE锁,可能让线上 API 流量一直 stall 到操作完成。Polar 通过模板层的lock_timeout = '5s'保证锁等待最多 5 秒快速失败,而不是把数据库拖死(见 server/migrations/script.py.mako)。


四、检查 3:队列里已经在途的任务(Tasks already in flight)

当 PR 合并时,队列里已经存在由旧代码入队的任务。这一节是 Ship Safety 的重头戏(.agents/skills/ship-safety/SKILL.md)。

3.1 重命名 actor:会让所有排队任务搁浅

Worker 按actor_name查找任务处理器,改名字后旧任务全部找不到处理器。SKILL.md 给出的标准操作序列是:

  1. 新增order.invoice.v2并开始用新名字入队;
  2. 部署;
  3. 等待旧队列排空(drain);
  4. 删除旧 actor,把名字换回。

把 actor挪到另一个队列是同样的问题,同样适用"新旧并存 → 排空 → 退役"三步。

源码印证:Polar 的 order 任务里真实存在这种演进痕迹——order.invoice任务指定了queue_name=TaskQueue.INVOICES_AND_RECEIPTS(见 server/polar/order/tasks.py),说明「换队列」与「actor 重命名」都是会真实发生的操作。

3.2 修改签名:破坏用旧参数入队的任务

改了函数签名,会破坏所有按旧参数入队的任务。新参数必须带默认值,让旧任务仍能反序列化执行。源码印证:order.trigger_paymentpayment_trigger: str | None = None就是典型的可空默认参数写法(见 server/polar/order/tasks.py)。

3.3 队列优先级:HIGH 是 checkout 专用

  • TaskPriority.HIGH只能用于checkout 路径(支付主链路);
  • 分析(analytics)、导出(exports)、回填(backfills)一律走LOW
  • 慢任务放HIGH饿死 checkout

源码印证:checkout.handle_free_successcheckout.expired使用TaskPriority.HIGH,而checkout.expire_open_checkouts这种周期清扫用LOW(见 server/polar/checkout/tasks.py);order 域里order.invoiceorder_createdorder.confirmation_email等非关键路径全部是LOW(见 server/polar/order/tasks.py)。

3.4 Cron actor:新增定时任务必须回答"漏跑怎么办"

新增一个cron_trigger前,必须回答"错过一次运行怎么办"。一个正向遍历跳过周期的catch-up 循环通常会算错状态;更好的选择是不变量告警(invariant alert)——直接告警"这次运行没发生",而不是补跑。

源码印证:Polar 的 cron actor 使用CronTrigger.from_crontab(...)声明,如order.process_dunning(每小时)、order.enqueue_stale_payment_locks(每小时 15 分)、checkout.expire_open_checkouts(每 15 分钟),见 server/polar/order/tasks.py 与 server/polar/checkout/tasks.py。

3.5 重试语义:新失败路径必须 raise,而不是吞掉

Polar 依赖自动重试:

  • 新的失败路径必须raise,绝不能swallow异常;
  • max_retries=0必须是有意为之的决定,而不是默认值。

源码印证:order.trigger_payment对 Stripe 网络类错误(APIConnectionError/APIError/RateLimitError)用raise Retry()明确重试,而对PaymentFailed这类业务失败选择记录日志不重试(交给 dunning 流程处理),见 server/polar/order/tasks.py。


五、检查 4:锁与吞吐量(Locks and volume)

  • 新的with_for_update应该放在拥有该工作单元的 task 或 service 里,而不是塞进某个 processor 专用的 helper。每一个锁都必须回答:如果进程死了,谁来释放这把锁?
  • 只允许在一条路径上释放。一条锁在两条路径上释放,就是一个等着爆发的 bug。
  • 把每一行匹配数据都加载进内存的做法,在线上吞吐量下活不下去——必须用分批/流式查询。
  • 扫描整张繁忙表的定时清扫任务,必须带索引或限定窗口(bounded window)

源码印证:Polar 为此实现了"陈旧支付锁"的回收机制——order.enqueue_stale_payment_locks(cron,每小时 15 分)通过stream_stale_payment_lock()流式扫描带锁的订单,order.process_stale_payment_lock则用for_update=True加行锁、检查is_payment_lock_stale,再把过期锁按"手动重试失败"处理释放(见 server/polar/order/tasks.py)。这正是「每个锁都要回答进程死后谁来释放」的工程答案:由专用 cron 兜底恢复,而不是靠人工。


六、检查 5:拆分这个 PR(Split this PR)

以下四种情况应标记为需要拆分(.agents/skills/ship-safety/SKILL.md):

  1. 删除了后端 endpoint 同时又改了它的前端调用方——旧前端可能还 live 对着新后端跑;
  2. 重命名 actor 时同时删除旧 actor——要等旧队列排空再删;
  3. 原生移动端代码与 TypeScript 一起改——native 会阻断 over-the-air 发布,应先把纯 TS 改动发布出去;
  4. 评审中已被标注"应该移到另一个模块"——后续 PR 是合理答案,但要明确说出来,而不是默默推迟。

七、输出格式:一份可直接粘贴的评审报告模板

Ship Safety 的产出是一份结构化报告(.agents/skills/ship-safety/SKILL.md),完整模板如下:

## Ship Safety ### 🔴 Blocking - `file:line` — <what breaks, in which window>. Fix: <fix> ### 🟠 Should fix - `file:line` — <what happens under load>. Fix: <fix> ### 🟡 Question - `file:line` — <question, including "split this PR?"> ### Notes - run before merge: <script path, or none> - manual step after deploy: <e.g. "remove order.invoice v1 after 4h", or none> ### Verdict ✅ Safe to ship | ❌ n blocking, n should-fix

模板的使用要点:

  • 分级🔴 Blocking(会坏、在哪个窗口坏、怎么修)、🟠 Should fix(负载下会怎样)、🟡 Question(包括"要不要拆 PR");
  • Notes 即使 verdict 是绿色也必须填写——一条必须跑的脚本没写下来,就等于这条脚本不会跑;部署后的手动步骤(例如"4 小时后移除 order.invoice v1")也必须落到 Notes 里;
  • Verdict✅ Safe to ship❌ n blocking, n should-fix收尾。

这套格式非常适合直接作为 GitHub PR 评论或 Code Review 模板复用:它把「部署窗口风险」显式变成可审查、可追踪的条目,而不是评审者脑中的模糊担忧。


八、结语:把 Ship Safety 纳入合并前流程

把整个 Skill 串成一条工作流:

  1. 收到 diff 先看 Scope——是否触及migrations/versions/tasks.pymodels/scripts/、endpoint 契约,或server/+clients/联动;
  2. 对每个 schema 变更回答"旧代码在新 schema 上还正确吗";
  3. 判断 DDL 是否阻塞(CONCURRENTLY / NOT VALID / ondelete="restrict");
  4. 审视队列:actor 重命名、签名变更、优先级、cron 漏跑、重试语义;
  5. 审查锁与扫描:锁的归属、释放路径唯一性、内存加载量、清扫窗口;
  6. 判断是否拆 PR;
  7. 按模板输出报告,Notes 与 Verdict 一起提交

这套检查与 Polar 仓库内的 ADR-0006 决策(handbook/engineering/decisions/0006-migration-and-backfill-safety.mdx)、迁移模板(server/migrations/script.py.mako)以及真实的 task 实现(server/polar/order/tasks.py、server/polar/checkout/tasks.py)相互印证——它不是纸面规范,而是 Polar 日常发布前真实执行的部署安全守则。在你的项目里,即使没有这套 Skill 基础设施,把同样的检查清单落到 CI 评论或人工评审模板中,也能显著降低非原子化部署引入的事故面。

【免费下载链接】polarPolar — A billing platform for the intelligence era项目地址: https://gitcode.com/GitHub_Trending/po/polar

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 11:03:13

JuiceFS 如何用 fio 跑顺序读写基准测试并解读结果

JuiceFS 如何用 fio 跑顺序读写基准测试并解读结果 【免费下载链接】juicefs JuiceFS is a distributed POSIX file system built on top of Redis and S3. 项目地址: https://gitcode.com/GitHub_Trending/ju/juicefs 已经挂载好的 JuiceFS 文件系统能跑多快的顺序读写…

作者头像 李华
网站建设 2026/9/15 11:02:56

Python开发个人日程管理系统的设计与实现

1. 项目概述"Python个人日程计划管理系统"是一个基于Python开发的轻量级个人时间管理工具。作为一名长期使用Python进行自动化开发的程序员&#xff0c;我发现在日常工作和生活中&#xff0c;市面上大多数日程管理软件要么功能过于复杂&#xff0c;要么缺乏灵活性。于…

作者头像 李华
网站建设 2026/9/15 11:02:39

抖音批量下载工具:一键无水印下载的完整指南

抖音批量下载工具&#xff1a;一键无水印下载的完整指南 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback support. 抖音批…

作者头像 李华
网站建设 2026/9/15 10:59:14

深入ego-lite的Learnings机制:AI Agent越用越快的5倍加速原理

深入ego-lite的Learnings机制&#xff1a;AI Agent越用越快的5倍加速原理 【免费下载链接】ego-lite The fastest browser for AI agents to run browser automation, built for sharing your logged-in browser state with your AI agents, like Codex or Claude Code, withou…

作者头像 李华
网站建设 2026/9/15 10:54:57

escrcpy 完整指南:把 Android 投屏到电脑并远程操控

escrcpy 完整指南&#xff1a;把 Android 投屏到电脑并远程操控 【免费下载链接】escrcpy &#x1f4f1; Display and control your Android device graphically with scrcpy. 项目地址: https://gitcode.com/GitHub_Trending/es/escrcpy 手机用 USB 线插在电脑上&#…

作者头像 李华