Files
qinglong/docs/adr/ADR-0071-bounded-local-dispatch-and-artifact-admission.md
T

9.4 KiB
Raw Blame History

ADR-0071:有界本机调度、执行计划物化与 Artifact Admission

  • 状态:Proposed
  • 日期:2026-07-20
  • 关联 RFCQL-RFC-0001 D-02、D-05、D-17、D-24、D-25、D-26、D-37、D-40、D-42、D-62、D-65、D-68、D-69、D-70
  • 关联 ADRADR-0003、ADR-0007、ADR-0014、ADR-0022、ADR-0023、ADR-0024、ADR-0025、ADR-0026、ADR-0040、ADR-0042、ADR-0044、ADR-0062、ADR-0063、ADR-0066、ADR-0069、ADR-0070

ADR-0087 现行增量:本文 dispatch authority 已迁入 @qinglong/local-execution/dispatch,只允许单向相对依赖 /executionSQLite、Secret 与 Profile 仍通过 runtime-core contract/application 注入,不因合包获得额外生产依赖。

上下文

ADR-0070 已把本机 Run 的状态推进与外部 spawn 收敛到独立 coordinator,但安全的启动内核不等于安全的生产入口。若 application 仍向业务 stack 暴露 coordinator,调用方可以自行拼装命令、全局环境变量、Secret 和日志路径,绕过 Task revision、Artifact 容量与 Profile 预算。若调度器先创建文件或进程,再发现 Secret 缺失或磁盘低水位,会在低性能路由设备上积累空 Artifact、写放大和不可解释的半启动事实。

back/runtime 调度链依赖 legacy service、Sequelize 与进程全局环境,也不能成为 3.0 本机执行入口。cluster 的 Worker Session、Run Lease、认证 transport 和共享 Artifact 又属于另一种部署权威;把它们带入 edge/standalone 会让路由器为永远不会使用的多节点能力支付依赖、内存和后台连接成本。

决策

1. 使用独立、Profile-neutral 的 local-dispatch package

新增 @qinglong/local-dispatch。它生产只依赖 @qinglong/runtime-core@qinglong/local-execution,不得导入 local-sqlite、local-process、legacy、cluster、ORM、HTTP 或 Profile importer。固定依赖方向为:

runtime-core
  <- local-process
  <- local-execution
  <- local-dispatch

local-sqlite + local-dispatch + local-execution
  <- local-application

SQLite adapter 通过 runtime-core port 提供候选、不可变定义和 mutationlocal-dispatch 不创建数据库连接、timer、watcher或进程内队列。application 私有持有 materializer 与 coordinator,只向 assembly stack 暴露一个 LocalRunDispatcher 和只含不可变定义 append 的冻结 writer facade。业务 stack、插件、Workflow 与旧 Cron 都不能取得裸 launcher、coordinator、candidate source 或 SQLite repository。

2. 执行定义必须不可变且内容寻址

本机可执行 Task revision 只允许受审 local_process 类型、绝对且有界的 argv,或显式 allowlist 中的 /bin/sh/bin/bash。执行环境由独立 localctx:sha256:<digest> recipe 描述;环境键排序后 canonicalize,公共值与 Secret 引用分离,QL3_* 保留名禁止由任务定义覆盖。revision 与 recipe 都采用 append-only、同内容幂等、不同内容冲突的写入语义。

SQLite migration 0005-local-dispatch-plan 建立两个不可变定义表与 dispatch 候选索引;0006-capability-v3 在前一步 migration history 已提交后再把 local-control-core 提升为 v3,并声明 local_dispatch_plan:1。DDL 与 capability 分步,避免 readiness capability 在其依赖 migration 尚未成为耐久历史前引用不存在的事实。

3. 候选发现复用唯一 SQLite authority

LocalSqliteRunRepository 同时实现有界 dispatch port,但仍只使用既有单连接/单 operation queue。候选必须满足:

  • runtime-owned Run 为 queued,无 cancellation intent
  • latest Attempt 为 claimedexecutor type 精确为 local_process
  • Attempt 引用的 revision 已发布;
  • 顺序固定为 priority 降序、Run 创建时间升序、Attempt 创建时间升序、Attempt ID 升序。

分页必须使用稳定 keyset cursor 和 limit + 1 截断证据,不得 offset 全表扫描。一次 dispatcher pass 只读取一页:edge 最多 4 项,standalone 最多 16 项,maxPages=1;成功激活一个 Run 后立即返回。candidate race 可跳过继续,launch failure 必须向调用方暴露,不能静默制造重试风暴。

4. 计划物化严格先于 Artifact 和 spawn

每个候选的处理顺序固定为:

  1. 重读 exact candidate authority
  2. 解析 pinned Task execution revision
  3. 解析 content-addressed context recipe
  4. 去重并解析全部 Secret reference
  5. 校验、合并并冻结有界 environment
  6. 读取文件系统容量并执行 Profile quota/reserve admission
  7. 创建私有、确定性的日志 Artifact;
  8. 构造 LocalExecutionStartCommand 并交给 ADR-0070 coordinator。

revision、recipe、Secret 或容量不可用时必须在创建 Artifact 和调用 coordinator 前返回 unavailable。Secret 明文只存在于单次 materialization 调用栈,不得写入 revision、recipe、Artifact metadata、RunEvent、错误或日志。

5. Artifact 与输出使用 Profile 硬预算

日志 Artifact root/shard 必须为 0700,文件为不可跟随 symlink 的 0600 普通文件,标识由 Attempt ID 确定性派生。分配前使用 statfs 同时裁决剩余空间、Profile 总配额与保留水位:

Profile 单日志最大字节 必须保留的文件系统可用空间
edge 4 MiB 32 MiB
standalone 64 MiB 256 MiB

launcher wrapper 使用一个 POSIX FIFO drainer 持续消费 stdout/stderr,但只把剩余 quota 写入 Artifact;超过上限后继续 drain,避免反压改变用户进程退出语义,并以不可覆盖的低敏 truncated fact 记录配额触达。不得为每个任务启动 Node sidecar、把输出行写入 SQLite、把 Secret/命令写入 truncation fact,或只依赖父进程内 pipe 作为日志持有者。

6. 本切片不宣称完整执行生命周期

dispatcher 是显式一次性调用对象,不安装 timer;未来 scheduler lifecycle 必须由 Profile application 以独立 admission、并发和停止协议拥有。当前完成的是 queued/claimed 到安全启动的入口,不包含 completion 在线消费、timeout/cancellation、shutdown drain、retry、Artifact retention/read authorization、加密 SecretStore 产品 adapter 或 API/UI 管理面。

被否决的替代方案

  1. 直接向 stack 暴露 coordinator:调用方可以绕过不可变定义、Secret 与 Artifact admission,拒绝。
  2. 复用 legacy Cron/全局环境拼装:重新引入 Sequelize、全局 mutable state 与 Secret 泄漏边界,拒绝。
  3. 先创建 Artifact,再解析 Secret:缺失 Secret 会留下无主文件并消耗闪存,拒绝。
  4. 把 Secret 明文保存到 context recipe:历史 revision、事件和备份会成为凭证仓库,拒绝。
  5. 用 offset 或持续循环清空 backlog:队列规模会直接决定单次延迟与内存,拒绝。
  6. 只在 Node pipe 中截断输出:控制面崩溃后日志存活与 quota 都不再成立,拒绝。
  7. 超过 quota 后关闭 pipe:SIGPIPE/阻塞会改变用户任务行为,拒绝。
  8. 复用 cluster Dispatcher/Worker Lease:让 edge/standalone 携带多副本依赖与无关 authority,拒绝。

影响与未完成项

已完成:

  • runtime-core 的候选、revision、context recipe、Secret provider 与 definition/store port
  • SQLite 0005/0006 reviewed migration、capability v3、typed schema/readiness lockstep 和唯一 authority dispatch adapter
  • 独立 local-dispatch importer、严格 materialization 顺序和 Profile-aware Artifact allocator
  • edge 4/standalone 16 的单页 dispatcher,单次最多启动一个 Attempt;
  • durable stdout/stderr hard quota 与不可覆盖 truncation fact
  • application 只暴露 dispatcher 与冻结 definition writer facade
  • 真实 Node 24 SQLite、文件系统、缺 Secret、低水位、输出超限和 activation race 测试。

仍未完成:

  • 具体加密本机 Secret provider、key lifecycle、管理权限与审计;
  • Task definition/revision/context recipe 的认证 API、Policy、Approval 和引用感知 retention
  • ADR-0072 已闭环 live completion receipt、cancellation、timeout 与 shutdown drain;仍缺 retry 与 Artifact retention/read stack
  • Linux x64/arm64、PID namespace、ENOSPC/断电、overlayfs 与固定低配路由器实机门禁;
  • target executable、systemd/Docker/s6 controller 与 2.x cutover 产品流程。

因此本 ADR 关闭的是“安全启动前的 scheduler admission”缺口,不表示本机 3.0 已形成完整生产生命周期,也不授权默认启用 execution target。

验证

  1. 不可变 revision/recipe 同内容重放幂等、不同内容冲突,Secret plaintext 不进入 SQLite。
  2. dispatch keyset 顺序稳定,edge/standalone 每次最多读取 4/16 项并只激活一个。
  3. missing revision/recipe/Secret 或容量低水位在 Artifact 创建和 coordinator 调用前失败。
  4. Artifact root/shard/file 权限、symlink/普通文件和 deterministic identity 均被复核。
  5. 真实 SQLite candidate 经 materializer 原子进入 ADR-0070 coordinatoractivation race 不产生第二次 spawn。
  6. 用户写出超过 quota 的 stdout/stderr 时 Artifact 精确停在上限、用户退出码不变、FIFO 被清理且 truncation fact 不含用户内容。
  7. application context 不包含 launcher、coordinator 或 repository 候选读取方法。
  8. dependency/source/lock audit 阻止 local-dispatch 导入 adapter、legacy、cluster 或 local-process,并阻止反向依赖污染 edge/standalone。