Wave-3 decomposition: SQLite engine tasks (open/opts, seam+tx, mechanisms, integration, review gate) + core value-constructor pre-work; plan synced for waves-1-2 review + ADR-023 follow-through

This commit is contained in:
glm-5.3-flash committed 2026-10-08 10:57:29 +00:00
1 parent 44637eea5b
commit 1269246faf
11 files changed
+883 -5

No files matched your search

+102
View File
@@ -0,0 +1,102 @@
---
id: sqlite-engine-scheduler-outbox
name: SQLite engine — scheduler (`schedule`/`unschedule`/`run_schedules`) and outbox helper
status: pending
depends_on: [sqlite-engine-queues, sqlite-engine-locks]
scope: broad
risk: high
impact: component
level: implementation
tags: [wave-3, sqlite-engine]
---
## Description
Implement the scheduler collapse (ADR-009) and the outbox helper
(ADR-019 §1, ADR-014) — the last mechanisms, and the only ones with
their own long-running machinery:
**Scheduler:**
- `schedule(name, spec, queue, payload, opts)` — upsert by name via
the substrate's `scheduler_register`; validate name AND queue
argument (shared-namespace rules, ADR-021 §3); validate the spec
against the v1 grammar (`@every <n><unit>`, s|m|h|d) **before
storage** — `InvalidSpec`, no round trip (the substrate's
`parse_every_interval` is the parser; map its rejection to
`InvalidSpec`). Returns the `Schedule` read-back (via the core
constructor).
- `unschedule(name)` — substrate's `scheduler_unregister`; `true` =
removed, `false` = absent.
- `run_schedules(stop)` — the leader loop (ADR-009 §1), engine-side
async over the sync substrate tick:
1. Acquire the leadership lock (`__alkstore_scheduler` — the
reserved-prefix derived name; use the engine's own lock
machinery with a derived TTL, engine-internal detail).
2. Loop: renew the lock — **on loss, return `Err(LeadershipLost)`
before ticking** (never fire on a stolen lease); call
`scheduler_tick` (fires due boundaries, catch-up-capped at 64,
skip-forward — all substrate-side); sleep until
`scheduler_soonest` or `stop`.
3. A cancelled `StopToken` ends the sleep and returns the clean
`Ok(())`. The loop runs in `spawn_blocking` (it's a blocking
sleep loop) with the stop token checked between iterations —
or a tokio-select shape over a watch channel wrapping the token
(implementer's choice; the token's flip-everywhere semantics are
core's).
- The runner must be honest about the wake path: after a tick fires
jobs, queue claims proceed through the ordinary queue machinery —
the scheduler adds no delivery machinery (ADR-009 §3).
**Outbox:**
- `outbox(name)` — validated constructor; derives the backing queue
name `__alkstore_outbox:{name}` (reserved prefix — unreachable by
`enqueue_tx` by design).
- `Outbox::enqueue` — auto-commit enqueue into the backing queue,
stamping the outbox's derived 60/5/5 set (ADR-014 §1) — the one
non-plain defaults set.
- `Outbox::run_once(worker_id, delivery)` — claim one job from the
backing queue, run `delivery.deliver(job)`: `Ok` ⇒ ack;
`Err(e)` ⇒ `retry(Some(e), None)` (the queue's curve). `true` =
claimed and processed; `false` = no work. No engine-issued
heartbeat inside delivery (the boxed `JobHandle`'s own heartbeat is
the consumer's renewal path).
## Acceptance Criteria
- [ ] `schedule` upserts; spec/name/queue validation all pinned
(`InvalidSpec` pre-storage, `InvalidName`/`ReservedName` on
both name and queue argument); `Schedule` read-back correct
- [ ] `unschedule` returns true/false correctly
- [ ] `run_schedules`: fires due boundaries into the named queue
(jobs claimable via the ordinary queue surface); leadership
lock held; a second concurrent runner does not double-fire
(one-firer-per-boundary — the writer serialization floor)
- [ ] Leadership loss returns `Err(LeadershipLost)` before any tick;
clean stop returns `Ok(())`; catch-up cap + skip-forward
observable through the substrate's pinned behavior
- [ ] Scheduler-fired jobs stamp the target queue's derived defaults
(300/3/5/none) with `ScheduleOpts` applied over them (ADR-020
§3) — `get_job`-visible
- [ ] Outbox: derived backing queue unreachable by `enqueue_tx`
(reserved-prefix rejection); `outbox_enqueue_tx` (from the
seam task) commit-atomic; `enqueue` stamps 60/5/5; `run_once`
acks on `Ok`, retries on `Err`, returns false on empty
- [ ] `cargo test -p alkstore-sqlite`, clippy `-D warnings`, fmt clean
## References
- docs/architecture/decisions/009-scheduler-collapse.md
- docs/architecture/decisions/014-outbox-tx-enqueue.md
- docs/architecture/decisions/019-mechanism-handle-surfaces.md §1, §4, §5
- docs/architecture/decisions/020-enqueue-opt-semantics-and-bridges.md §3
- docs/architecture/decisions/021-tx-reads-and-value-shape-fixes.md §3
- docs/architecture/engine-sqlite.md (Mapping the contract: scheduler/outbox)
- alkstore-sqlite/src/substrate/queue_ops.rs (scheduler_* fns)
## Notes
> To be filled by implementation agent
## Summary
> To be filled on completion