mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
621 lines
34 KiB
Markdown
621 lines
34 KiB
Markdown
# ADR-0002:Legacy Crontab 兼容、影子写入与切换策略
|
||
|
||
- 状态:Proposed
|
||
- 日期:2026-07-18
|
||
- 决策者:QingLong Maintainers
|
||
- 关联 RFC:[QL-RFC-0001](../QINGLONG_3_0_ARCHITECTURE_RFC.md)
|
||
- 前置决策:[ADR-0001](./ADR-0001-run-state-and-transaction-boundaries.md)
|
||
- Amended by:[ADR-0445](./ADR-0445-schedule-service-origin-shadow-run-coverage.md)、[ADR-0446](./ADR-0446-system-crond-stable-shadow-admission.md)、[ADR-0447](./ADR-0447-boot-shadow-and-non-origin-boundaries.md)、[ADR-0448](./ADR-0448-bounded-legacy-shadow-startup-reconciliation.md)、[ADR-0449](./ADR-0449-versioned-legacy-shadow-startup-difference-report-and-metrics.md)
|
||
|
||
## 1. 决策摘要
|
||
|
||
QingLong 3.0 采用按执行来源渐进切换的三态兼容模式:
|
||
|
||
off -> shadow -> primary
|
||
|
||
- off:只执行 2.x 路径,不创建 Run。
|
||
- shadow:2.x 的 Crontab、RunningInstance、Shell 回调和日志仍是用户可见事实源;系统旁路创建 Run、RunAttempt 和 RunEvent,用于验证,不参与调度与结果判定。
|
||
- primary:Run 聚合成为事实源,2.x 字段变为兼容投影;所有外部副作用由新 Runtime 发起,Legacy 路径不得再次执行同一任务。
|
||
|
||
切换按已被入口事实证明的 manual、scheduled、boot 与内部任务 execution origin 独立进行,不允许一次性全局切换。`once`、`grpc` 等保留值只有在未来具备独立
|
||
trigger/admission identity 后才形成切换单元,不能由 schedule 或 transport 名称推导。任何单次执行在被接受时即固定 owner 为 legacy 或 runtime,运行中不得切换执行引擎。
|
||
|
||
Shadow 写入必须 fail-open:新模型写入失败只产生有界日志、指标和对账记录,不改变 2.x 的执行结果。Primary 路径在产生 spawn、派发或其他外部副作用前必须 fail-closed;只有能够证明外部副作用尚未发生时,才允许受控回退到 Legacy。
|
||
|
||
新 Runtime 通过 run_id、attempt_id、短期 callback token 和 dedupe key 关联状态。Cron ID、PID 和 log path 只用于 Legacy 兼容与诊断,不能作为 3.0 状态关联主键。
|
||
|
||
## 2. 背景
|
||
|
||
### 2.1 当前执行链事实
|
||
|
||
当前实现存在多条执行入口,但最终大量依赖 Shell 回调更新状态:
|
||
|
||
1. PUT /crons/run 调用 CronService.run,先把多个 Crontab 标为 queued,再异步调用 runSingle。
|
||
2. runSingle 经进程内并发限制后 spawn task 命令,并把 Crontab 更新为 running、记录 PID 和 log path。
|
||
3. system crontab 与 Node scheduler 都执行带 ID 的 Shell 命令;Node scheduler 外层的 runCron 本身不负责最终状态。
|
||
4. task.sh/share.sh 在开始和结束时调用 /open/crons/status。
|
||
5. CronService.status 在 running 回调时创建 RunningInstance,在 idle 回调时按 cron ID、PID 和状态更新实例,并更新 Crontab 的最近状态。
|
||
6. CronService.stop 按 Crontab PID 和命令查杀进程,随后把活跃实例标记为 stopped,并把 Crontab 恢复为 idle。
|
||
7. 为降低旧执行的迟到回调覆盖新执行,现有代码在 log path 不匹配时跳过部分 Crontab 字段更新,但仍没有稳定的运行级关联键。
|
||
|
||
关键代码位置:
|
||
|
||
back/api/cron.ts
|
||
back/services/cron.ts
|
||
back/services/schedule.ts
|
||
back/shared/runCron.ts
|
||
back/schedule/addCron.ts
|
||
shell/api.sh
|
||
shell/share.sh
|
||
|
||
### 2.2 现有模型的能力边界
|
||
|
||
Crontab.status 只表达任务最近状态,无法准确表示同一任务的多个并发实例。RunningInstance 提供实例记录,但仍存在以下限制:
|
||
|
||
- 记录在 Shell running 回调到达后才创建,spawn 与记录之间有空窗。
|
||
- 重复 running 回调可能创建重复实例。
|
||
- Cron ID、PID 和 log path 无法形成跨重启、跨节点、跨 Attempt 的稳定身份。
|
||
- queued 没有独立持久化实例,服务重启时难以恢复。
|
||
- stop 与自然退出并发时,迟到回调可能再次修改 Legacy 状态。
|
||
- Crontab 修改后无法说明历史执行使用的是哪个任务版本。
|
||
|
||
因此不能让 Legacy 状态和 Run 状态长期双向互相驱动,否则会形成循环覆盖和不可解释冲突。
|
||
|
||
## 3. 目标
|
||
|
||
1. 在不改变 2.x API、CLI、Shell 和常用脚本行为的前提下验证新 Run 模型。
|
||
2. 每个执行只由一个 owner 产生外部副作用,避免双跑。
|
||
3. 允许按执行来源小步切换和快速停止扩大影响面。
|
||
4. 对重复、乱序、迟到和缺少关联 ID 的回调给出确定行为。
|
||
5. 允许 Shadow 与 Legacy 状态自动对账,并量化差异。
|
||
6. Primary 模式下继续为 2.x UI/API 提供可接受的兼容投影。
|
||
7. 保持 edge 模式的写入、内存和后台任务开销有界。
|
||
|
||
## 4. 非目标
|
||
|
||
- 本 ADR 不决定 Executor 接口和进程隔离细节。
|
||
- 本 ADR 不决定 SQLite/PostgreSQL Repository 的具体驱动。
|
||
- 本 ADR 不把历史 RunningInstance 自动伪造成完整、可信的 RunEvent 历史。
|
||
- 本 ADR 不在 Shadow 阶段替换现有 UI、日志 API 或停止 API。
|
||
- 本 ADR 不承诺把 Crontab 的单值状态无损映射为并发 Run 状态。
|
||
- 本 ADR 不允许双执行后再通过结果去重来弥补 owner 不明确。
|
||
|
||
## 5. 术语
|
||
|
||
### 5.1 Execution origin
|
||
|
||
触发执行的兼容来源:
|
||
|
||
manual
|
||
scheduled_system
|
||
scheduled_node
|
||
once
|
||
boot
|
||
grpc
|
||
subscription
|
||
system
|
||
script
|
||
|
||
Runtime 实现可以归并内部枚举,但必须保留足够信息区分人工、定时和内部任务。
|
||
|
||
### 5.2 Execution owner
|
||
|
||
单次执行的唯一拥有者:
|
||
|
||
- legacy:现有 CronService、scheduler 或 Shell 链负责执行。
|
||
- runtime:RunService、RunQueue 和 Executor 负责执行。
|
||
|
||
owner 在接受触发时写入执行上下文,并贯穿日志、指标和回调。Feature Flag 后续变化不影响已接受的执行。
|
||
|
||
### 5.3 Compatibility projection
|
||
|
||
从 Run/Attempt 派生到 Crontab、RunningInstance 和 2.x response 的有损视图。Projection 不是 3.0 事实源。
|
||
|
||
## 6. 模式与事实源
|
||
|
||
| 模式 | 外部副作用 owner | 用户可见事实源 | Run 数据用途 | 新模型失败行为 |
|
||
| --- | --- | --- | --- | --- |
|
||
| off | Legacy | Legacy | 不创建 | 不适用 |
|
||
| shadow | Legacy | Legacy | 旁路验证与对账 | fail-open |
|
||
| primary | Runtime | Run/Attempt/Event | 调度、状态、审计 | 副作用前 fail-closed |
|
||
|
||
同一 origin 在任一时刻只能处于一个模式。配置加载失败或出现未知值时回退到 off,不能默认进入 primary。
|
||
|
||
## 7. Feature Flag 模型
|
||
|
||
建议配置结构:
|
||
|
||
type CompatibilityMode = 'off' | 'shadow' | 'primary';
|
||
|
||
interface RuntimeRolloutConfig {
|
||
defaultMode: CompatibilityMode;
|
||
origins: Partial<Record<ExecutionOrigin, CompatibilityMode>>;
|
||
allowLegacyFallbackBeforeStart: boolean;
|
||
}
|
||
|
||
约束:
|
||
|
||
1. 具体 origin 配置覆盖 defaultMode。
|
||
2. 3.0 Alpha 初始默认全部为 off。
|
||
3. primary 只有在 migration、状态机、Executor contract 和对账门禁通过后才可配置。
|
||
4. 配置变更必须写审计日志,包含 actor、旧值、新值和时间。
|
||
5. edge 不启动远程配置 watcher;配置刷新使用已有配置机制或显式 reload。
|
||
6. 禁止使用“数据库写失败就自动把全局模式改为 Legacy”这种隐式降级。
|
||
|
||
### 7.1 `next` Alpha Shadow 开关
|
||
|
||
当前孵化实现只开放观察型 Shadow,不通过该环境变量提供 primary:
|
||
|
||
QL3_SHADOW_ORIGINS=manual,scheduled_node,scheduled_system,boot,subscription,system,script
|
||
|
||
- 未设置或设置为空时全部为 off。
|
||
- ADR-0447 后当前接受 `manual`、`scheduled_node`、`scheduled_system`、`boot`、`subscription`、`system` 与 `script`;未知 origin 被忽略并记录
|
||
有界配置告警。`once` 与 `grpc` 仍是保留领域值:现有 `@once` 只是 schedule 标记,gRPC 只是传输入口,两者都不能单凭该字段推导 execution origin。
|
||
- 配置在进程内首次使用时读取;edge 不启动 watcher,变更后需要通过既有进程重启或未来的显式 reload 生效。
|
||
- 兼容观察器在实际 HTTP/gRPC worker 中按需加载;关闭时不构造 Shadow 事实或任务摘要、不增加 ChildProcess 监听器、不初始化 Repository、不创建后台任务,也不引入额外数据库写入。
|
||
- `manual/scheduled_node/boot/subscription/system/script` 只监听 Legacy 已创建的同一个 ChildProcess。`scheduled_system` 不持有 Node ChildProcess,
|
||
只接受 system crond 显式标记后由 Shell start/finish 共用的稳定 execution ID;finish-only 回调可以幂等补齐 accepted→terminal 聚合。Shadow
|
||
代码不得调用 Executor 或第二次 spawn;`subscription/system/script` 仅在 `ScheduleService` 已选中执行且 `onBefore` 成功后 accepted。
|
||
- 任意初始化、接受或后续写入失败都退化为 no-op,只记录不含命令、环境变量和 Secret 的稳定错误类型与有界计数。
|
||
- `bootTask` 只把启用的 `@boot` 条目以固定 `boot` origin 交给 `runSingle`;普通 HTTP/gRPC `run` 仍使用 `manual`。`@once` 不会因 schedule
|
||
字符串被改记为 `once`,gRPC 请求也不会因 transport 被改记为 `grpc`。
|
||
|
||
该环境变量是 Alpha 兼容桥,不替代最终可审计的 `RuntimeRolloutConfig`。进入 primary 前必须改用具备配置校验、审计和 owner 固化语义的正式配置面。
|
||
|
||
### 7.2 `next` manual Primary manifest 门禁
|
||
|
||
孵化实现已提供严格的 `data/config/qinglong3-rollout.json` loader,并在 HTTP worker 接入默认关闭、按需加载的 manual Primary bootstrap:
|
||
|
||
- 文件缺失、不可读、超过 64 KiB、JSON 非法、字段未知、审批过期或 gate 缺失时返回 off,不构造或安装 router。
|
||
- `defaultMode` 固定为 off,当前只允许 `origins.manual`;Primary 不得通过环境变量、通配 origin 或隐式默认值开启。
|
||
- enabled manifest 必须记录 revision、approvedBy、approvedAtMs、expiresAtMs 和 rollbackPlanRef;审批窗口最长 30 天。
|
||
- `durableCancellation`、`startupReconciliation`、`atomicLegacyProjection`、`rollbackDrill`、`edgeBudget` 必须全部为 passed。
|
||
- ADR-0453 后 enabled manifest 还必须绑定可由 loader 独立重算的 manual capture/terminal/resource Primary gate bundle;ADR-0454 的一次性目标实例仪式进一步绑定
|
||
exact Profile/admission 计划、原始文件摘要、短期审批、selection receipt 与 rollback intent/completion。配置选择只表示 `primary_selected`,不能冒充当前 worker
|
||
已经完成 `selected → reconciled → activated`。
|
||
- 审计只包含 source path/hash、revision、时间和稳定判定。接受审计先于 router 安装;安装后审计失败立即调用 disposer,恢复原 owner。
|
||
- edge 不启动 watcher;当前 bootstrap 只在 HTTP worker 启动时读取一次,未来显式 reload 必须复用同一校验和审计边界。
|
||
|
||
bootstrap 接入不代表 Primary 已默认开放。文件缺失、disabled、rejected 或 `manual` 非 primary 时保持 Legacy,且不加载完整 Runtime stack、不创建 router 或 timer。只有 accepted 且全部 gate 通过的 manifest 才按 startup reconciliation、cancel lifecycle、router 的顺序激活;任一步失败都会撤销 router 并停止 lifecycle。ADR-0454 已提供部署配置写入、只读选择状态与操作回滚仪式,但首次真实目标实例执行、运行态 durable activation receipt、固定 edge 基准和 ADR-0007 的完整实机恢复仍需继续评审。
|
||
|
||
## 8. Legacy 到 Run 的身份映射
|
||
|
||
### 8.1 Task 身份
|
||
|
||
在 TaskDefinition 完成迁移前:
|
||
|
||
- legacy_cron_id 保存 Crontab 数字 ID。
|
||
- task_id 使用稳定的兼容 namespace,例如 legacy-cron:<id>。
|
||
- task_revision 由影响执行的字段生成稳定摘要,至少包括 command、schedule、task_before、task_after、work_dir、log_name、环境引用版本和 Package/Subscription 来源。
|
||
- Run 创建后保存 revision 或不可变 snapshot 引用;Crontab 后续修改不得改变历史 Run。
|
||
|
||
不得把 Run 外键设置为随 Crontab 删除级联删除。任务删除后,历史 Run 仍须可查询并展示保存的名称和摘要。
|
||
|
||
### 8.2 一次触发对应一个 Run
|
||
|
||
- 批量手动执行多个 Cron 时,每个 Cron 创建独立 Run。
|
||
- 同一个 Cron 的多实例执行各自创建独立 Run。
|
||
- API 批量请求可以使用 request_id 关联,但不创建共享生命周期的“批量 Run”。
|
||
- 自动重试在同一 Run 下创建新 RunAttempt;用户再次点击运行创建新 Run。
|
||
|
||
### 8.3 Trigger 信息
|
||
|
||
在 Trigger 表落地前,Run 仍需记录:
|
||
|
||
trigger_type
|
||
triggered_by
|
||
scheduled_for_ms nullable
|
||
request_id nullable
|
||
legacy_schedule nullable
|
||
|
||
这些字段或受限 payload 不能包含 Secret 和完整环境变量值。
|
||
|
||
## 9. Shadow 写入规则
|
||
|
||
### 9.1 触发时
|
||
|
||
Legacy owner 接受执行后,Shadow Adapter 尝试:
|
||
|
||
1. 创建 Run,初始状态为 created。
|
||
2. 追加 run.created。
|
||
3. 当 Legacy 进入队列时转换为 queued 并追加 run.queued。
|
||
4. 创建 attempt 1,用于关联后续 spawn 与回调。
|
||
|
||
Shadow Adapter 不得:
|
||
|
||
- 改变 Legacy 是否执行。
|
||
- 等待无界重试后才返回 Legacy 请求。
|
||
- 获取会改变 Legacy 并发顺序的长时间锁。
|
||
- 把 Shadow Run ID 暴露成 2.x API 的成功必要条件。
|
||
|
||
### 9.2 启动和结束映射
|
||
|
||
| Legacy 事实 | Shadow 事实 |
|
||
| --- | --- |
|
||
| Crontab queued | Run queued |
|
||
| spawn 已获得 PID | Attempt starting;记录本地 executor handle |
|
||
| Shell running callback | Attempt running;Run running |
|
||
| Shell idle,exit code 0 | Attempt succeeded;Run succeeded |
|
||
| Shell idle,exit code非 0 | Attempt failed;Run failed |
|
||
| 用户 stop 已接受 | Attempt/Run cancelled,并记录 kill 结果 |
|
||
| 进程消失且无结束回调 | Attempt lost;由协调器决定 Run 结果 |
|
||
|
||
Shadow 转换仍必须遵守 ADR-0001。无法合法映射时追加 compat.transition_mismatch,不能强行覆盖终态。
|
||
|
||
当前 Alpha 切片对已审 Node worker origin 直接观察同一 ChildProcess 的 spawn、error 和 exit 事件,因此不依赖 Shell callback 才能形成基本终态。下述两级关联已补充 Shell callback、stop/cancel 和乱序/迟到回调;ADR-0448 又补充了监听前一次性启动恢复,ADR-0449 将其投影为 origin-bounded、版本化的差异报告与固定字段 metric batch。ADR-0450 再提供显式、只读、Profile-bounded 的闭合窗口终态审计;ADR-0451 已在 128 MiB router stress 与 256 MiB Edge release cgroup 中证明有界查询、SQLite 零增长和进程重启后的 Shadow-off 回滚。ADR-0453 不再从 2.x RunningInstance 猜测反向分母,而是在默认 Legacy bridge admission 建立 process-epoch 守恒 token,并把 capture/startup、terminal 与 resource/rollback 三类低敏 source report 打包;rollout v2 loader 会独立重算 bundle 后才允许 manual Primary。
|
||
|
||
### 9.4 `next` Alpha callback 与 stop 关联
|
||
|
||
当前实现已为 `manual` 与 `scheduled_node` 增加两级、失败开放的兼容关联:
|
||
|
||
1. 同一 worker 内使用最多 256 条 active execution 的内存注册表,保持 stop/callback 与 ChildProcess exit 的入队顺序;终态、启动失败和取消后立即移除,不启动清理定时器。
|
||
2. callback 或 stop 落到其他 worker 时,按 `legacy_cron_id + enabled origins + legacy owner + active status` 查询持久化候选;单次最多读取 64 个,超限只处理有界集合并记录 truncated,不能无界扫描。
|
||
3. 单实例匹配依次使用 opaque log artifact、PID 和唯一候选兜底。日志与 PID 各自唯一但指向不同 Run 时视为冲突;零候选、多候选和冲突都只记录 unmatched/ambiguous,不猜测更新。
|
||
4. log path 只在进程内转换为不超过 36 字符的稳定摘要;原始路径、命令和环境变量不进入关联告警。
|
||
5. stop all 取消该 Cron 的全部有界 active Shadow Run;stop instance 只在强字段或唯一候选能够确定一个 Run 时取消。
|
||
6. 取消事实在 Legacy kill 前投递;同 worker 的后续 exit 排在取消之后。跨 worker 使用持久化定位器尽力关联,任何查询或写入失败都不能阻断 kill 或改变 2.x API 响应。
|
||
7. 乱序 finished 可以从 queued/claimed 补齐 dispatching、starting、running 和终态;重复终态 callback、取消后的迟到成功 callback 不覆盖终态,也不追加重复完成事件。
|
||
|
||
ADR-0453 已补齐 manual origin 的正式 Shadow→Primary 判定契约:process-local admission/capture/failure/pending 守恒、clean-shutdown 一次性 exporter、capture/startup + terminal + resource/rollback 自包含 bundle,以及 rollout v2 loader 的独立重算。它仍不会自动启用 Primary;目标实例必须产生真实 manual canary bundle,其他 origin 也必须独立评审。后续能力不得让 edge 增加常驻 watcher 或无界内存队列。
|
||
|
||
### 9.3 Shadow 写失败
|
||
|
||
Shadow 写失败时:
|
||
|
||
1. Legacy 执行继续。
|
||
2. 写结构化错误日志和有界计数指标。
|
||
3. 若已经存在 Run,尽力追加 compat.shadow_write_failed;数据库不可用时不做无界内存缓存。
|
||
4. 对账器可以在数据库恢复后标记缺失或不完整记录,但不得伪造未知的精确时间和事件顺序。
|
||
5. Shadow 完整率低于门禁时禁止进入 primary。
|
||
|
||
## 10. Primary 执行规则
|
||
|
||
### 10.1 触发接受
|
||
|
||
Runtime owner 必须先完成以下持久化事务,再进入队列或 spawn:
|
||
|
||
1. 创建 Run 和 run.created。
|
||
2. 将 Run 转换为 queued 并追加 run.queued,或以等价的单事务命令完成。
|
||
3. 写入唯一 idempotency key 或 trigger delivery key。
|
||
4. 确认事务提交成功。
|
||
|
||
事务失败时不得 spawn。API 返回稳定错误,定时触发记录 delivery failure 并按 Trigger 策略重试。
|
||
|
||
### 10.2 外部副作用边界
|
||
|
||
以下任一事件发生后都视为外部副作用可能已发生:
|
||
|
||
- 调用 Executor.start。
|
||
- spawn 返回结果未知或超时。
|
||
- 已向 Worker 发送可接受的派发请求。
|
||
- 已把执行消息提交到外部队列。
|
||
|
||
进入该边界后禁止自动回退到 Legacy,因为无法证明不会双跑。系统必须把 Attempt 标记为 failed 或 lost,并交由协调器处理。
|
||
|
||
### 10.3 有限 Legacy 回退
|
||
|
||
只有同时满足以下条件,才允许 allowLegacyFallbackBeforeStart:
|
||
|
||
- 错误发生在外部副作用边界前。
|
||
- 该执行尚未创建 claimed/starting Attempt。
|
||
- idempotency key 明确属于本次触发。
|
||
- 回退决策写入审计日志;若 Run 数据库可用,追加 compat.legacy_fallback。
|
||
- Legacy 接受同一 request/delivery key,避免重复回退。
|
||
|
||
默认值为 false。Beta 前是否保留该选项由运行数据决定。
|
||
|
||
## 11. 回调关联与幂等性
|
||
|
||
### 11.1 新回调信封
|
||
|
||
Primary Runtime 启动 Shell 时注入:
|
||
|
||
QL_RUN_ID
|
||
QL_ATTEMPT_ID
|
||
QL_CALLBACK_TOKEN
|
||
QL_CALLBACK_SEQUENCE
|
||
|
||
推荐回调信封:
|
||
|
||
interface AttemptCallback {
|
||
runId: string;
|
||
attemptId: string;
|
||
token: string;
|
||
sequence: number;
|
||
event: 'started' | 'heartbeat' | 'finished';
|
||
pid?: number;
|
||
logRef?: string;
|
||
exitCode?: number;
|
||
occurredAtMs: number;
|
||
}
|
||
|
||
每个回调生成稳定 dedupe key,例如 <attempt_id>:<sequence>:<event>。重复回调返回已提交结果;小于已提交序列的迟到回调只记录受限诊断,不修改终态。
|
||
|
||
### 11.2 Legacy 回调
|
||
|
||
不包含 Run/Attempt ID 的旧回调:
|
||
|
||
- 在 off/shadow 模式继续更新 Legacy 状态。
|
||
- Shadow Adapter 可以按 cron ID、PID 和 log path 尝试关联,但只有唯一候选时才更新 Shadow Run。
|
||
- 存在零个或多个候选时记录 compat.unmatched_callback,不能猜测。
|
||
- 在 primary 模式,新 Runtime 发起的执行必须携带新信封;缺少信封视为协议错误。
|
||
- 非 Runtime 发起的旧任务仍按其固定 owner 走 Legacy,不得写入任意 Primary Run。
|
||
|
||
### 11.3 Token
|
||
|
||
Callback token:
|
||
|
||
- 仅授权更新一个 Attempt。
|
||
- 使用高熵随机值,数据库保存哈希或可轮换验证材料。
|
||
- 有明确失效时间,Attempt 终态后只允许幂等重放已接受事件。
|
||
- 不写入普通日志、RunEvent payload 或进程列表可见的命令参数;优先通过受限环境或本地凭据文件传递。
|
||
|
||
## 12. Compatibility Projection
|
||
|
||
### 12.1 单向投影
|
||
|
||
数据方向固定:
|
||
|
||
shadow: Legacy -> Run shadow facts
|
||
primary: Run facts -> Legacy projection
|
||
|
||
禁止同一 execution 同时启用两个方向,禁止用 Crontab watcher 反向覆盖 Primary Run。
|
||
|
||
### 12.2 Crontab 投影
|
||
|
||
Primary 模式下,2.x 字段按以下规则投影:
|
||
|
||
| Legacy 字段 | 投影规则 |
|
||
| --- | --- |
|
||
| status | 有 active Run 时按 running 优先于 queued;无 active Run 时为 idle |
|
||
| pid | 最近启动且仍 active 的本地 Attempt PID;远程执行为空 |
|
||
| log_path | 最近用户可见 Run 的兼容日志路径 |
|
||
| last_execution_time | 最近 Run started_at,保持 2.x 时间单位契约 |
|
||
| last_running_time | 最近终态 Run 的 duration 投影 |
|
||
|
||
Crontab 无法表达 succeeded、failed、cancelled 和并发实例,因此不能用于 3.0 结果查询或恢复。
|
||
|
||
### 12.3 RunningInstance 投影
|
||
|
||
- 每个本地或可表示的 Attempt 投影一个 RunningInstance。
|
||
- 保存稳定的 run_id 和 attempt_id 扩展列后,更新必须按这些 ID 完成。
|
||
- 旧 API 返回结构保持兼容,新增字段必须是可选字段。
|
||
- 远程 Worker 没有可用 PID 时 PID 为空,停止由 Executor handle 完成。
|
||
|
||
当前 `0003-running-instance-run-reference` migration 已增加 nullable `run_id`、`attempt_id`,建立 `(run_id, started_at)` 查询索引与 `attempt_id` 唯一索引。旧行保持 null,旧 API 无需提供新字段。孵化实现已经增加 Primary 专用的组合 Repository:它跟踪同一事务内变更的 Run/Attempt,在提交前按 `attempt_id` 幂等投影 RunningInstance,并按“running 优先于 queued、无 active Run 时 idle”聚合 Crontab。任一投影参与者失败时,Run、Attempt、Event 与 Legacy projection 一起回滚;Shadow Repository 不注册该参与者。
|
||
|
||
### 12.4 事务边界
|
||
|
||
当 Run、Event 和 Legacy projection 位于同一控制面数据库时,状态转换和必要的兼容投影应在同一事务完成。投影失败不得提交一个对 2.x API 不可解释的 Primary 状态。
|
||
|
||
若未来 projection 跨存储,必须使用 transactional outbox 和幂等消费者;不能在事务提交后仅做一次 best-effort 更新。
|
||
|
||
### 12.5 `next` manual Primary 孵化装配
|
||
|
||
当前 manual 入口已经具备一个默认关闭的 owner seam:未安装 `ManualPrimaryExecutionRouter` 时继续执行原 Legacy 路径;显式安装且 rollout policy 将 manual 判定为 primary 后,`runSingle` 只调用 Runtime,不再创建 Legacy ChildProcess。真实本机装配由 Primary 专用组合 Repository、LocalProcessExecutor、受限日志适配器和 ManualPrimaryRuntime 组成;严格 manifest loader 与可回滚 assembly 也已存在,但生产 boot 不调用安装入口,因此仍不可由部署配置启用。
|
||
|
||
约束:
|
||
|
||
- boot 虽复用 `runSingle`,不参与 manual owner seam。
|
||
- Runtime 一旦被选为 owner,准备日志、创建 Run 或 Executor 启动失败均 fail-closed,禁止回落到 Legacy 再次 spawn。
|
||
- 新触发 owner 与 in-flight owner 分离;关闭新触发不应卸载仍持有 active handle 的 Runtime。
|
||
- stop all/stop instance 优先按进程内 Run/Attempt owner 调用 Executor。数据库中存在 `attempt_id`、但当前 worker 无法证明 ownership 时拒绝 PID-only stop,等待跨 worker cancel 或 Reconciler 能力。
|
||
- 该装配不等于 Primary rollout Gate 已通过;正式启用仍需要可审计配置、跨 worker cancel、重启后 supervisor、固定 edge 预算和回滚演练。
|
||
|
||
## 13. Stop 与迟到退出
|
||
|
||
### 13.1 Shadow 模式
|
||
|
||
Legacy stop 行为不变。Shadow Adapter 观察 stop 结果:
|
||
|
||
- 能唯一关联时,将对应 Attempt/Run 转为 cancelled。
|
||
- Shell 后续 exit callback 只能追加 late callback 诊断,不能把 cancelled 改为 succeeded/failed。
|
||
- kill 失败与状态更新失败分别记录,不把“数据库已 stopped”等同于“进程一定已退出”。
|
||
|
||
### 13.2 Primary 模式
|
||
|
||
1. Cancel command 在同一事务读取 Run/Attempt,使用 Run version/CAS 写入首次 `cancel_requested_at_ms`、受限 reason 和 `run.cancel_requested`;此时不提前把实际进程标记为已退出。
|
||
2. 事务提交成功后才允许 Executor 执行 cancel/kill;提交失败不得发送 signal。
|
||
3. 重复取消不追加第二个请求事件,但可以幂等重试 stop;stop 失败保留 durable request,由 Reconciler 继续检查。
|
||
4. Attempt 终态先提交时,取消返回 already-terminal 且不发送 signal;取消请求先提交时,迟到 exit code 0 或其他非 cancel 完成结果收敛为 cancelled。
|
||
5. 实际完成后 Attempt/Run 转为 cancelled,并由同事务 Projection 更新 RunningInstance;kill 结果和后续核验事件不得包含原始命令、环境或 Secret。
|
||
|
||
2.x /crons/stop 仍可返回兼容响应,但内部语义是异步 cancel,不承诺响应时进程已退出。
|
||
|
||
当前 manual 孵化实现已覆盖独立 durable cancel command、stop-before-signal、首次请求幂等、cancel/complete 获胜裁决、待取消 Run 的有界恢复查询、最多 64 条一页的 cross-worker source、独立 dispatch lease/fencing Repository、指数退避、低敏结果 Event、单周期有界 supervisor、Linux durable handle 身份复验与 TERM/KILL controller、进程内 active handle stop 路由、Attempt 终态投影与 stop/prepare 竞争时的 abort。HTTP worker 已接入轻量、默认不激活的 manifest bootstrap:只有 accepted 且 manual=primary 才惰性加载真实 stack,先完整有界 Reconcile,再启动 cancel lifecycle 和安装 router;失败自动撤销,shutdown 先停止 router/lifecycle。ADR-0007 的 completion/log supervisor 仍未完成;找不到可证明的 owner/handle 时不会用 PID 猜测补偿。
|
||
|
||
## 14. 修改、删除与历史
|
||
|
||
### 14.1 运行中修改 Crontab
|
||
|
||
Run 使用创建时的 task revision/snapshot。更新 Crontab 只影响后续 Run,不修改正在排队、运行或已结束的 Run。
|
||
|
||
### 14.2 删除 Crontab
|
||
|
||
- 删除不级联删除 Run、Attempt、Event、日志或审计记录。
|
||
- active Run 默认继续使用 snapshot;若产品选择“删除即取消”,必须作为显式、可审计命令实现。
|
||
- v2 API 删除响应不因历史 Run 保留而改变。
|
||
|
||
### 14.3 历史导入
|
||
|
||
3.0 首次升级不自动把所有 RunningInstance 伪造成完整 Run。可以提供独立、可重跑的 import 工具,生成标记为 legacy_import 的只读记录,并明确:
|
||
|
||
- 原始时间精度和状态可能不完整。
|
||
- 不生成没有证据的中间事件。
|
||
- import 记录不参与调度、重试和资源计费。
|
||
|
||
## 15. 启动与恢复
|
||
|
||
### 15.1 off/shadow
|
||
|
||
Legacy 启动归一行为保持。HTTP worker 在归一之后、Primary activation 与 listen 之前运行一次 Shadow Reconciler:
|
||
|
||
- 使用 `(created_at_ms, run_id)` keyset,只扫描 enabled origin 的 queued/dispatching/running legacy-owned Run;单页最多 64。
|
||
- 每个 Cron 最多读取 8 条 RunningInstance,只有唯一 PID/log/实例终态证据才补齐 succeeded/failed/cancelled;冲突与截断保持 ambiguous。
|
||
- Node worker-owned dispatching/running 在 owner 重启且无终态证据时标记 lost;queued/claimed 收敛为 abandoned cancellation。
|
||
- scheduled_system 无终态证据时保持 pending,等待稳定 execution ID callback,不把 HTTP worker 生命周期误当成 system crond 生命周期。
|
||
- edge 每次启动最多 `8 × 1 page`,standalone 最多 `32 × 4 pages`,不启动 timer/watcher;cluster-control/worker 拒绝本机 SQLite 装配。
|
||
|
||
### 15.2 primary
|
||
|
||
启动时不得像 2.x 一样把所有 Run 批量重置为 idle。系统按 ADR-0001 协调 dispatching/running/lost Attempt,并通过 Executor handle、Worker lease 和 callback 序列恢复。
|
||
|
||
## 16. 对账与可观测性
|
||
|
||
### 16.1 对账维度
|
||
|
||
Shadow 阶段至少记录:
|
||
|
||
shadow_run_create_total
|
||
shadow_run_create_failed_total
|
||
shadow_callback_unmatched_total
|
||
shadow_transition_mismatch_total
|
||
shadow_terminal_mismatch_total
|
||
shadow_transition_lag_ms
|
||
shadow_active_run_delta
|
||
legacy_fallback_total
|
||
|
||
标签必须有界,只允许 origin、profile、executor type 和错误分类;禁止使用 cron ID、run ID、command 或用户名作为指标 label。
|
||
|
||
### 16.2 终态对账
|
||
|
||
终态对账比较:
|
||
|
||
- Legacy RunningInstance status/exit code。
|
||
- Shadow Run/Attempt terminal status/exit code。
|
||
- start/finish 时间是否在允许误差内。
|
||
- 日志引用是否存在。
|
||
- 是否发生重复执行或缺失 Run。
|
||
|
||
差异记录保存受限摘要和关联 ID,不复制完整命令、环境变量或日志内容。
|
||
|
||
## 17. Rollout 顺序
|
||
|
||
1. Schema only
|
||
2. manual shadow
|
||
3. scheduled_node shadow
|
||
4. scheduled_system shadow
|
||
5. boot shadow
|
||
6. future source-proven trigger shadow(只有独立 once/grpc trigger 存在时)
|
||
7. manual primary for opt-in users
|
||
8. manual primary default
|
||
9. scheduled_node primary
|
||
10. scheduled_system primary
|
||
11. remove Legacy as execution owner
|
||
|
||
每一步必须独立通过门禁,不能因为 manual 路径稳定就直接切换 scheduled/system crontab。
|
||
|
||
## 18. 进入 Primary 的门禁
|
||
|
||
一个 origin 进入 primary 前必须满足:
|
||
|
||
1. Shadow 完整率达到维护者接受的阈值,并公布测量窗口。
|
||
2. 没有未解释的重复执行。
|
||
3. terminal status 和 exit code 对账达到阈值。
|
||
4. stop/exit、重复 callback、乱序 callback 和重启场景测试通过。
|
||
5. 关闭 Feature Flag 后新触发立即回到 Legacy,已有执行保持 owner 不变。
|
||
6. edge 基准的额外 RSS、写放大和启动时间处于预算内。
|
||
7. 新 migration 从支持的 2.x 数据库升级、备份恢复和重复执行通过。
|
||
8. 2.x API、CLI、Shell 和 UI 契约测试通过。
|
||
9. 日志与指标不泄漏 callback token、Secret 或完整命令敏感参数。
|
||
|
||
## 19. 回滚策略
|
||
|
||
### 19.1 Shadow 回滚
|
||
|
||
将对应 origin 设置为 off 即可。已有 Shadow Run 标记为验证数据,不需要驱动 Legacy;不得删除以掩盖差异。
|
||
|
||
### 19.2 Primary 回滚
|
||
|
||
- 配置变更只影响新触发。
|
||
- 已由 Runtime 接受的 Run 继续由 Runtime/Executor 完成或取消。
|
||
- 禁止把 in-flight Run 重新提交给 Legacy。
|
||
- Legacy 投影保持到所有 Primary Run 终态。
|
||
- 若 Runtime 完全不可用,管理员只能执行显式、带风险提示的恢复操作;系统不得自动双跑。
|
||
- 数据库 migration 默认 forward-only;应用回滚必须能忽略新增表和可空扩展列。
|
||
|
||
## 20. Schema 对后续 PR 的要求
|
||
|
||
PR-1 至少需要支持:
|
||
|
||
- Run 的 legacy_cron_id、trigger_type、task_revision、status、version、时间字段和 idempotency key。
|
||
- RunAttempt 的 attempt number、status、executor identity、PID/handle、时间与 exit code。
|
||
- RunEvent 的 sequence、dedupe key、actor、attempt reference、受限 payload 和时间。
|
||
- 必要唯一索引和查询索引。
|
||
- Legacy 表新增 run_id/attempt_id 时必须可空,不破坏旧版本读取。
|
||
- 所有新增表和列通过显式、可重跑、带 checksum 的 migration 创建。
|
||
|
||
PR-1 只建 schema 和 Repository contract,不修改 CronService、Shell 或调度执行路径。
|
||
|
||
## 21. 被拒绝的方案
|
||
|
||
### 21.1 一次性替换所有运行路径
|
||
|
||
手动、system crontab、Node scheduler、once、boot、gRPC 和内部 ScheduleService 的生命周期不同,一次切换无法隔离风险,也无法快速回退。
|
||
|
||
### 21.2 永久双向同步
|
||
|
||
Crontab 单值状态无法无损表达并发 Run。双向同步会形成循环、迟到覆盖和不可解释的冲突。
|
||
|
||
### 21.3 以 PID 作为 Attempt ID
|
||
|
||
PID 会复用、仅在单机有意义、spawn 前不存在,也不能跨 Worker 或容器稳定关联。
|
||
|
||
### 21.4 Shadow 写失败阻断 Legacy
|
||
|
||
Shadow 的目的就是观察,不应改变生产执行结果;阻断会让验证机制成为新故障源。
|
||
|
||
### 21.5 Primary 失败时无条件回退 Legacy
|
||
|
||
在 spawn/派发结果不明确时回退会导致重复执行,任务可能包含通知、支付、删除等不可逆副作用。
|
||
|
||
### 21.6 全量历史自动回填
|
||
|
||
Legacy 数据没有完整事件顺序和稳定关联,自动构造会制造虚假的审计精度并增加 edge 升级成本。
|
||
|
||
## 22. 影响
|
||
|
||
### 正面影响
|
||
|
||
- 可以用真实 2.x 流量验证新状态机,而不立即切换生产事实源。
|
||
- 执行 owner 明确,降低迁移期间双跑风险。
|
||
- Run ID 和 Attempt ID 消除 PID/log path 猜测。
|
||
- 回滚只影响新触发,in-flight 行为可解释。
|
||
- 兼容投影为 UI/API 渐进迁移提供窗口。
|
||
|
||
### 负面影响
|
||
|
||
- Shadow 阶段增加数据库写入和对账复杂度。
|
||
- 一段时间内需要维护 Legacy 与 Runtime 两套读取模型。
|
||
- 旧 Shell 回调无法提供强关联,只能有限对账。
|
||
- Primary 兼容投影仍是有损的,2.x UI 看不到完整并发与终态语义。
|
||
|
||
## 23. 验证场景
|
||
|
||
实现必须覆盖:
|
||
|
||
1. manual off 不创建 Run,2.x 行为不变。
|
||
2. manual shadow 新模型写失败时 Legacy 仍只执行一次。
|
||
3. 批量 manual 为每个 Cron 创建一个 Run。
|
||
4. 同一 Cron 并发两次时两个 Run/Attempt 不串回调。
|
||
5. 重复 running/finished callback 不重复创建 Attempt/Event。
|
||
6. finished 先于 running 到达时按协议拒绝或记录 mismatch,不回退终态。
|
||
7. stop 与 exit code 0 并发时 cancelled 不被覆盖。
|
||
8. spawn 返回未知时不自动 Legacy fallback。
|
||
9. Feature Flag 从 primary 改为 off 后,旧 Run 继续由 Runtime 完成,新触发走 Legacy。
|
||
10. Crontab 在 Run 中途修改或删除时,历史 Run snapshot 不变。
|
||
11. 重启后不把 Primary Run 批量重置为 idle。
|
||
12. edge 上 Shadow 写入与对账保持有界。
|
||
13. v2 status/log/instances API 在兼容窗口内保持契约。
|
||
14. callback token 不出现在普通日志和 RunEvent payload。
|
||
|
||
## 24. 接受标准
|
||
|
||
- 明确接受 off/shadow/primary 三态模型。
|
||
- 明确接受按 origin 而非全局一次切换。
|
||
- 明确接受单次 execution owner 固定且禁止中途切换。
|
||
- 明确接受 Shadow fail-open、Primary 副作用前 fail-closed。
|
||
- 明确接受 Run/Attempt ID 为强关联,PID/log path 仅作兼容诊断。
|
||
- 明确接受 Primary 到 Legacy 的单向有损投影。
|
||
- 明确接受 in-flight Primary Run 不因回滚而重新提交 Legacy。
|