102 lines
4.6 KiB
Markdown
102 lines
4.6 KiB
Markdown
---
|
|
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 |