Activepieces 架构决策记录(ADR)体系:编号永不复用、以"原因"为中心的决策日志
【免费下载链接】activepiecesAI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces
Activepieces 仓库在 brain/knowledge/decisions 目录维护了一份架构决策记录(Architecture Decision Record, ADR)日志:每条记录对应一个难以逆转的技术决策,以及产生该决策的完整推理。本篇文章围绕这份决策日志的组织约定展开,并结合仓库内已落地的 11 条决策记录,说明如何通过"编号永不复用 + 原因优先"的机制,让一段架构演进史保持可追溯、可引用、可学习。
一、决策日志是什么:一条记录 = 一个难以逆转的决策
打开 decisions/index.md,第一句话就给出了整个目录的定位:
One hard-to-reverse call per record, with the reasoning that produced it. (每条记录对应一个难以逆转的决策,以及产生该决策的推理。)
也就是说,这个目录不是变更日志(changelog),不记录"我们做了什么改动";它记录的是**"我们为什么做了这个决定"**——尤其是那些一旦做出就难以回头的架构选择。例如:
- 000001-worker-is-the-sandbox-one-job-per-worker-scale-by-replicas.md 决定"Worker 即沙箱":一个 worker 一次只轮询一个任务(concurrency 1),在进程内用 node child + isolated-vm 运行引擎,不再有独立的沙箱容器、不再有
/execute远程跳转、不再有 Docker socket、不再有资源池。这是一个牵一发动全身的执行架构决策,一旦 worker 镜像承载了完整执行工具链,就很难再退回去。 - 000010-async-webhook-ack-is-redis-durable-not-postgres-durable.md 决定异步 Webhook 的 ACK 依赖 Redis 持久化(AOF
everysec)而非先写 Postgres 行——这是对"Webhook 吞吐上限"与"最多 1 秒丢失窗口"之间权衡的一次不可逆取舍。
每条记录都遵循这一范式:先说决策,再说背景,再说为什么是它胜出,最后说它把你承诺到了什么地步。
二、编号约定:一次分配、永不复用
index.md 给出了目录最核心的治理规则——编号语义:
Each file in this folder is numbered, and the number is assigned once and never reused, so a link to a decision stays good and the sequence tells you what came after what.
- 编号一次分配、永不复用:即使某条决策后来被修订甚至推翻(例如 000001 被 000002 修订),原编号依然保留,不会回收给新决策使用。这保证了任何外部链接一旦指向某条记录,就永远有效。
- 顺序即历史:编号的先后就是决策产生的先后。阅读整个目录的文件列表(从
000001到000034),就能还原出 Activepieces 执行架构、文件分发、计费限额等主题的演进时间线。 - 链接永不失效:由于编号不复用,文档里引用"决策 000009"是稳定的锚点,不会因为后续新增决策而漂移。
一个很好的实例是 000001 与 000002 的承接关系:000002 的标题是Transitional multi-box concurrency (honor AP_WORKER_CONCURRENCY),其正文开篇就声明 "Amends, does not supersede, 'Worker is the Sandbox'."(修订而非取代 000001)。它没有改掉 000001 的编号,而是在新编号下记录"过渡期的多盒并发是向 concurrency-1 目标进发的向后兼容桥梁"。编号系统因此天然支持决策的修订链:旧决策保持原样可查,新决策在后续编号中记录对它的修订。
三、记录的标准结构:Decision / Context / Why / Consequences
index.md 明确要求每条记录必须写清四件事:
- Write the decision—— 决策本身;
- the context it was made in—— 做出决策时的背景;
- why this option won over the alternatives—— 为什么这个选项战胜了备选方案;
- what it commits you to—— 这个决策让你承诺承担了什么后果。
仓库内的记录几乎全部遵循这一骨架。以 000009-approval-links-require-a-post-confirmation-on-a-dedicated-route.md 为例:
- Decision:审批邮件不再直接放两个裸 GET 链接,而是指向专用的
/:id/waitpoints/:waitpointId/confirm路由;GET/HEAD 只渲染确认页(绝不消费 waitpoint),只有按钮触发的 POST 才会恢复流程。 - Context:暂停流程的恢复端点是无鉴权、单次使用的,唯一防护是难以猜测的 id。而邮件安全扫描器(Microsoft Safe Links、Mimecast、Proofpoint)会在投递前用 GET 预取 URL,预取与真人点击无法区分,导致 waitpoint 被提前消费、流程被以任意结果恢复(Pylon #5253 回归)。
- Why:不改变状态的 GET 对扫描器是安全的,只有有意的 POST 才决定结果;同时新路由不动旧路由,已投递的邮件继续可用,Slack 因其按钮由服务端 POST 触发而天然免疫。
- Consequences:新审批邮件对扫描器安全,预取只渲染页面绝不恢复流程;确认页事后只显示"已回应"而不显示具体决定(因为 waitpoint 恢复即删除、决定不持久化,持久化需要 schema 变更,明确划出范围)。
index.md 特别强调 Why 是整条记录中最不可省略的部分:
The why is the part that stops the same argument being had again in six months, and it is the part nobody remembers without a record. ("为什么"是阻止六个月后同一场争论重演的部分,也是没有记录就没有人记得住的部分。)
正因为如此,每条记录的 Why 小节都会明确列出被否决的备选方案及其落选理由。例如:
- 000001 记录了被取代的 LOCAL_POOL / GCP_CLOUD_RUN 探索(worker 作为池管理器通过 HTTP 分发),其最大风险是需要 Docker socket 与远程 HTTP 边界。
- 000006-pieces-are-distributed-as-links-resolved-lazily.md 拒绝了 PR #13865 的"预热的批量同步 + 全局
name@versionS3 key"方案,理由是冷启动成本、缺少租户隔离、鉴权更弱。 - 000008-streaming-file-writes-go-through-the-app-one-path.md 否决了"给沙箱直接发 S3 凭证"(沙箱要运行任意 piece 代码),也否决了引擎侧编排的分片直传协议(新协议 + 状态机 + 孤儿清理面)。
四、一次真实的决策修订:000008 的 Aug 2026 修正
决策记录不是"写完就冻结"的教条,编号机制允许在同一编号下追加修订说明。000008(流式文件写入统一走应用)提供了一个教科书级的例子:
原始决策是ctx.files.write()接受Readable,流式写入以无Content-Length的分块 PUT 发给应用,应用用@aws-sdk/lib-storage以约 5 MB 分片流式落 S3(或缓冲为 DBbytea)。但记录内追加了"Amended Aug 2026"修正段:
the engine no longer sends that chunked PUT. It drains a
Readableto aBuffer(capped byAP_MAX_FILE_SIZE_MB) and always declares aContent-Length.
原因是缓冲代理会在上游加上该请求头,导致应用做重定向(signed URL)时"无法重放一个已经被消费过的 body"。而单次写入本就远低于沙箱内存预算,流式传输"没有买到任何东西,反而付出了整段传输重试和 S3→DB 回退的代价"。应用侧的流式 ingest 保持不变,因为它在没有 signed-URL 重定向的部署里依然对所有部署生效。
这条修正展示了决策记录的自我纠错能力:原始决策、修正内容、修正理由、受影响边界(引擎侧 vs 应用侧)全部在同一条记录内留痕,读者不会被仓库里"看起来矛盾"的代码弄糊涂——文档层面已经解释了矛盾从何而来。相关细节可继续参阅 File Storage 的 gotchas 段落。
五、从记录看架构主题的演进链
把多条记录串起来读,就能还原出 Activepieces 几个关键架构主题的决策链条。
5.1 执行架构:000001 → 000002
- 000001 确立终态:worker 即沙箱,每 worker 一任务,靠副本数水平扩展。每个副本被限制在 0.5 CPU / 1 GB;一个任务一个受限容器意味着 OOM 只杀死一个 worker,进程内槽位复用带来的共享堆"棘轮"效应不可能跨任务发生;单一执行路径消除了 seam、远程传输、provisioner 和 HTTP 信封。
- 000002-transitional-multi-box-concurrency-honor-ap-worker-concurrency.md 则是过渡桥梁:直接只发布 concurrency-1 会让现有
AP_WORKER_CONCURRENCY=N部署一夜之间吞吐降到 1/N,因此 worker 先用createSandboxRuntime({ concurrency })维护 N 个进程内沙箱盒、按workerIndex路由任务,默认值恢复为 5。记录同时警告:N>1 时 N 个引擎子进程共享一个容器 cgroup,单个失控流程可能 OOM 杀死整个容器——这正是 000001 想要消除的共享容量棘轮,所以"临时性是有意为之,绝不能成为架构"。
5.2 分发与可复现性:000005 → 000006
- 000005-freeze-piece-versions-in-the-flow-bundle-manifest.md:Flow Bundle 的
pieces.json清单在构建时冻结 piece 的解析后版本,而不是每次运行重新解析^范围。因为已锁定的流程版本是不可变快照(bundle 已冻结流程定义和编译产物),让 piece 范围在"已锁定"版本下悄悄浮动到更新的补丁版本反而更令人意外。冻结后"锁定版本字节级可复现",还省去了运行时的 per-piecegetPiece往返。草案版本不受影响(始终实时解析 piece)。 - 000006:每个 piece 以单个可下载链接(
.tgz)分发,由引擎令牌、平台作用域的GET /v1/engine/pieces/bundle?name=&version=端点 307 重定向到签名 S3 对象 / 官方 piece 的 npm tarball / 自定义 ARCHIVE piece 的文件存储。沙箱下载链接后bun install,所有 piece 类型走同一条路径,piece 字节不经过 worker socket。S3 副本惰性预热,预热任务以jobId = bundle:<platformId|global>:<name>:<version>去重。平台作用域是强制性的(数据隔离规则),自定义 piece 的 S3 key 按平台命名空间隔离,堵住了原全局 key 方案的跨租户name@version碰撞。
5.3 数据路径与持久化取舍:000003 → 000008 → 000010 → 000011
- 000003-engine-posts-run-time-callbacks-directly-to-the-app.md:引擎把四个运行时回调(
updateRunProgress、updateStepProgress、sendFlowResponse、uploadRunLog)通过internalApiUrl+engineToken直接 HTTP POST 到应用的/v1/engine/*(ENGINE 主体),删除 engine→worker 中继。uploadRunLog是刻意保留的双源:worker 仍需以 WORKER 主体上报引擎自己无法上报的终态(crash、OOM、INTERNAL_ERROR),由同一应用侧服务承接两端入口。 - 000008:文件写入统一走应用这一条路径(含 5.1 节所述的 Aug 2026 引擎侧修正)。
- 000010:异步 Webhook 入队 Redis 即返回 200(带
x-webhook-id),不写 Postgres 行。吞吐由 Redis 延迟决定,且能在 Postgres 故障转移期间存活(流程解析由 Redis 缓存提供)。代价是 Redis 数据集丢失会在持久化窗口内静默丢弃"已确认但未开始"的 webhook——这是 Redis 中唯一不可重建的数据(其余都能从 Postgres 重建),因此记录给出的运维结论是 "Don't back up Redis; persist it"(不要备份 Redis,要持久化它)。持久化参数由运维者的redis.conf调节,对应文档见 disaster-recovery.mdx。 - 000011-webhook-files-stream-to-s3-by-dropping-global-multipart-buffering.md:删除
@fastify/multipart的全局attachFieldsToBody和fastify-raw-body的全局缓冲,Webhook 路由改用request.parts()流式读入,multipart 与二进制流直通 S3。代价是 multipart 的签名校验被放弃(HMAC-over-file-upload 罕见),且仅当FILE_STORAGE_LOCATION=S3时才真正流式(DB 存储缓冲为bytea)。
5.4 安全与计费治理:000009 → 000013
- 000009:见第三节,用"GET 只渲染、POST 才恢复"对抗邮件安全扫描器的预取。
- 000013-active-user-seat-floor-is-enforced-db-authoritatively.md:活跃用户数不得超过套餐席位上限(
usersLimit)。早期设计试图让 Autumn 控制台(console.activepieces.com)做独立 backstop,但因客户作用域 key 无法调用balances.update(403)、且控制台读回的正是 AP server 自己写入的数字(循环的、最终一致性的拷贝,存在 TOCTOU 窗口)而放弃。最终决定在唯一一处——AP server 自己的数据库上执行:新增/邀请由checkUsersExceededLimit把关(usedSeats= 活跃用户 + 预留邀请,对应决策 000014),降低限额由assertSeatsNotBelowActiveUsers把关(对应 000017 的effectiveUsersLimit),UI 在降级前主动弹出停用用户对话框,后端校验是权威检查。这组记录还展示了决策之间的相互引用:000013 引用 000014(邀请预留席位)与 000017(计划降级即时生效),共同构成计费限额的完整语义。
六、目录定位:它就是一个普通文件夹
index.md 最后一节特意澄清了目录的实现方式:
This folder is an ordinary folder. It has an
index.mdbecause every folder page does, and the decisions inside it are its children. Nothing special-cases it.
即该目录没有任何特殊处理:index.md只是因为文档系统的每个目录页都需要一个索引页而存在,各条决策记录就是它的子页面。这个说明对读者有两层含义:
- 新增记录的约定即规范:不需要任何工具或脚本配合,照编号续写 markdown 文件即可;
- 链接与检索都是普通的:记录之间的相互引用(如 000013 指向 000014、000017,000008 指向 File Storage)就是仓库内的普通相对链接,全库可搜、可跳转。
七、如何阅读与借鉴这套 ADR 体系
对想要快速了解 Activepieces 架构内核的读者,推荐按主题而非按编号阅读:
- 执行与沙箱:000001 → 000002,理解"worker 即沙箱"的终态设计与向后兼容过渡;
- piece 分发与可复现:000005 → 000006,理解锁定版本如何保持字节级可复现、piece 如何以链接惰性分发;
- 数据与事件路径:000003 → 000008 → 000010 → 000011,理解引擎回调、文件写入、Webhook ACK 与 Webhook 文件流各自的数据路径取舍;
- 安全与治理:000009、000013,理解审批链接防预取、席位上限的单一事实来源。
对于其他项目团队,这套体系的要点可以概括为三条可迁移的实践:编号一次分配永不复用(链接永久稳定、顺序即历史)、每条记录强制包含 Decision / Context / Why / Consequences 四段(把备选方案与落选理由写进 Why,让争论只发生一次)、允许在同一编号下追加修订说明(决策的自我纠错同样留痕,而非悄悄改写历史)。索引页本身刻意保持简短,因为它定义的是约定,而约定的证据——几十条结构严谨的决策记录——就散落在 brain/knowledge/decisions 目录中,随时可以逐条查阅。
【免费下载链接】activepiecesAI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents项目地址: https://gitcode.com/GitHub_Trending/ac/activepieces
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考