feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,98 @@
# ADR-0103:有界本机 Cron 调度与 Run 原子准入
- 状态:Accepted(本机 schedule state、一次性协调器、Profile cadence 与 Run 原子准入已实现;PostgreSQL/cluster 对等实现由 ADR-0104/0105 完成,物理设备长期抖动证据待完成)
- 日期:2026-07-22
- 关联 RFCQL-RFC-0001 D-08、D-23、D-61、D-62、D-85、D-86、D-91、D-93、D-94、D-97、D-100、D-101、D-102
- 关联 ADRADR-0068、ADR-0071、ADR-0087、ADR-0092、ADR-0093、ADR-0094、ADR-0098、ADR-0102
## 背景
TaskDefinition、execution revision 与 Trigger 已成为不可变、摘要绑定的正式事实,Legacy adoption 也能在一个目标事务中发布这些事实,但此前没有任何生产 Scheduler 把到期 Trigger 转换为 Run。仅有 Trigger 表不等于执行接管;若直接在内存 cron callback 中启动进程,又会丢失重启恢复、幂等和并发围栏。
该切片还必须同时服务低配路由器与本机单节点。路由器不能为调度能力常驻大依赖、无界扫描或后台 watcher;单节点则必须在进程竞争和崩溃时保持同一到期时刻最多产生一个 durable Run。
## Package 决策
不新增第 28 个 workspace package。Package 是部署、依赖或权限边界,不是每个 use case 的默认边界:
- Profile-neutral schedule contract、严格输入归一化和 cron 决策位于 `@qinglong/runtime-core/local-scheduler` 显式子入口;根入口不 re-export,未使用者不会 eager-load。
- SQLite 状态与事务 authority 位于既有 `@qinglong/local-sqlite` 的 runtime adapter。
- 一次性协调器与显式生命周期位于既有 `@qinglong/local-execution/scheduler` 子入口;它们与 dispatch 总是共同部署,但仍保持源码依赖方向。
- application 只组合上述端口,不拥有 cron 计算、SQL 或进程启动语义。
`croner@7.0.8` 固定精确版本并只在首次 cron 计算时加载。选择它是因为离线 pack 约 124 KiB 且无运行时传递依赖;曾评估的 `cron-parser` 会带入约 4.5 MiB 的 Luxon,直接突破 4 MiB Edge 制品预算。制品审计会显式拒绝启动 import closure 中出现 `croner`,防止 lazy 边界回退。
## 持久化状态
SQLite migration `0035-local-scheduler` 新增 `QingLong3LocalTriggerSchedules`,每个 Trigger head 恰有一行:
- `trigger_revision` 把状态绑定到当前 immutable Trigger revision
- `next_fire_at_ms` 是 durable cursor`NULL` 只表示需要初始化;
- `last_scheduled_at_ms` 记录最后一次实际准入的 occurrence
- `state_version` 为 compare-and-set fence
- due 与 initialize 均有独立 partial index。
新建或追加内建 `qinglong/cron@v1` Trigger 时,在同一事务计算并 upsert 下一时刻;extension Trigger 保留状态行但 `next_fire_at_ms=NULL`,在对应 scheduler provider 出现前不会进入内建 cron 候选。Legacy adoption 同样在原 publication transaction 内为每个已审 cron Trigger 写入非空 cursor,避免 100,000 行迁移后依赖低速懒初始化。
`0036-capability-v18` 把 local-control-core 推进到 contract v18,并显式声明 `local_scheduler_admission`。readiness 同时校验新表、两个索引、migration checksum、owned trigger 和精确 capability;不得通过自动建表或降低检查绕过 migration ceremony。
## 调度与 misfire 语义
协调器只提供显式 `scheduleOnce()`,核心层不创建 timer、watcher 或后台线程,因此单元测试、CLI 和未来 cluster lease scheduler 能复用同一轮次语义,而不会因 import 产生隐式常驻工作。`@qinglong/local-execution/scheduler` 另提供显式 start/stop 的 Profile lifecycleStandalone 每 1 秒、Edge 每 5 秒执行至多一页,timer `unref`、轮次不重叠、诊断失败隔离,shutdown 等待受 5/10 秒硬上限约束。
application 只在 startup recovery、既有 execution lifecycles 与 admission 全部成功后启动 Scheduler lifecycle。shutdown 先撤销 admission,再停止 Scheduler,之后才 drain execution control、stack 与 storage,保证停止期间不会继续创建新 Run。
每轮只读取一页:Edge 为 4Standalone 为 16,协议硬上限 256。所有时间、revision、摘要、cron、timezone 和页面形状先做严格验证。cron macro 和隐式主机 timezone 继续拒绝。
- 正常到期:准入当前 durable `next_fire_at_ms`,随后把 cursor 推进到观察时刻之后。
- `skip`:超过 Profile grace 后不创建 Run,只把 cursor 推进到观察时刻之后。
- `fire_once`:无论漏过多少 occurrence,只补偿最老的一个 durable occurrence,然后直接推进到观察时刻之后;不会扫描或回放 backlog。
- 新建 Trigger 的首次 occurrence 从激活时间开始计算;migration 遗留的 `NULL` 状态只初始化 cursor,不虚构历史执行。
Edge grace 为 30 秒,Standalone grace 为 5 分钟;全局最大值固定为 5 分钟。
## 原子 Run 准入
SQLite adapter 对每个 decision 使用 `BEGIN IMMEDIATE`,并在写入前重新读取 Project、Trigger head/revision、schedule version、Task revision 和 local execution revision。任一事实漂移都返回 `raced`,不产生部分 Run。
一次成功 admit 在同一事务内完成:
1. 创建固定 Task revision/digest、Trigger revision/digest 和 `scheduled_for_ms` 的 queued Run
2. 创建 `local_process` claimed Attempt
3. 追加 `run.created``run.queued` 两个事件;
4. 以 schedule revision/version/next cursor 作 CAS,更新 next/last/state version
5. commit 后才通知现有 bounded dispatcher 执行一次 dispatch。
幂等键为 `ql3:cron:v1:<triggerId>:<triggerRevision>:<scheduledForMs>`。Scheduler 不直接 spawn,不在 callback 中拼接命令,也不把 event delivery 或内存锁当成 durable claim。
## 资源与验收证据
1. runtime-core 109/109、local-sqlite 54/54、local-execution 27/27、local-application 13/13 目标测试通过;覆盖 on-time、skip、fire-once、迁移初始化、严格上限、原子 Run/Attempt/Event、竞争重放、非重叠 cadence、诊断隔离和有界 shutdown。
2. adoption 测试要求每个已发布 cron Trigger 在同一事务拥有非空 schedule cursor,并在失败路径一起回滚。
3. 六类离线 production artifact 均通过 4 MiB、512 files、16 MiB RSS 门禁。最大为 standalone-application 2,181,590 bytes/323 filesbase/adopted/application 启动闭包为 36/39/69 modules 且不 eager-load `croner`,本轮抽样最大 RSS delta 为 12,042,240 bytes。
4. ADR-0106 收敛后 importer 为 23;本切片没有新增 package、binary、watcher 或数据库连接。只有 active application 拥有一个显式、可停止、`unref` 的 Scheduler timerbase/adopted importer 仍无该 timer。
5. 23 个 QL3 importer 全量测试和 backend 669/669 回归通过;依赖/源码边界审计保持零 finding。
## 未包含
- PostgreSQL schedule claim/lease 与 cluster Run admission 已由 ADR-0105 完成,不属于本 ADR 的 SQLite 证据范围;
- interval/event/webhook/AI Trigger provider
- Project/Task 配额和 retry admission
- 固定物理路由设备的长期时钟跳变、休眠唤醒、抖动、写放大及断电证据;
- Legacy scheduler cutover 与旧 `ScheduleService.runTask` 下线。
- Scheduler 指标、积压告警与管理面健康投影。
在上述 Gate 完成前,本 ADR 只证明本机 cron occurrence 到 durable Run 的原子准入,不宣称 cluster scheduler 或 Legacy 全量接管。
## ADR-0218 后续修正
ADR-0218 已 supersede 本 ADR 的两个实现细节,但不改变 misfire、CAS 或 Run 原子准入:
- Croner adapter 从 `runtime-core` 移到现有 `local-execution` 部署 owner
- SQLite Trigger append 与 Legacy adoption 不再预计算非空 cursor,而与 PostgreSQL
一致写入 `NULL` sentinel,由首次 schedule cycle 基于原
`triggerUpdatedAtMs` 计算并执行 initialize/skip/admit。
因此上文“adoption transaction 写非空 cursor”及对应历史验收记录只描述当时实现,
不再是当前 contract。新的实现避免 base/adopted-only、Worker 与 cluster-admin 安装
Croner;物理 Profile 资源证据以 ADR-0218 后续重跑为准。