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:
1 parent
44637eea5b
commit
1269246faf
11 files changed
+883
-5
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user