docs: Phase 1 review round — wake wording honesty, job-handle validity predicate, outbox/streams depth OQs
- C2: 'at-least-once wake delivery' collapsed to 'best-effort hints' (ADR-006 §1 + core-contract) — coalescing and the pg no-replay hole contradict per-commit wake promises; the guarantee table's row was already correct. - W2: uniform job-handle validity predicate pinned (ADR-010 §2, core-contract, queues.md; verification-backlog row): processing state + unexpired deadline; D-12's missing-check not inherited. - W1: outbox run_once worker semantics pinned in core-contract (pull op, ack/retry-on-curve, no heartbeat in delivery — honker parity, dual-execution window documented). - W3: named-locks tx-seam posture stated (no lock_tx; acquisition is auto-commit; TTL discipline governs). - C1 -> OQ-12: streams depth (key semantics w/ honker ground, event shape, ordering row, retention); method names pinned, depth open. - New find -> OQ-13: the v1 TxHandle surface cannot express the transactional outbox enqueue (derived backing-queue name is reserved-prefix-rejected; honker's raw-Transaction seam unavailable); option set + decision rule sketched. - README resolution order refreshed (OQ-06 resolved); ScheduleOpts + stream consumption-trigger lines added to core-contract.
This commit is contained in:
1 parent
8e68b44194
commit
d401908f13
9 files changed
+250
-35
No files matched your search
@@ -108,9 +108,12 @@ never sees it.
|
||||
delivers opaque `Wake { channel }` signals
|
||||
([ADR-006](decisions/006-wake-and-delivery-contract.md)), never
|
||||
payloads or ids ([ADR-008](decisions/008-contract-v1-pinning.md) §3).
|
||||
Wakes are at-least-once, possibly coalesced (SQLite) or per-notify
|
||||
(Postgres), possibly repeated after reconnect. **Consumers must be
|
||||
idempotent on wake.**
|
||||
Wakes are **best-effort hints, not per-commit delivery promises**:
|
||||
possibly coalesced (SQLite) or per-notify (Postgres), possibly
|
||||
repeated after reconnect, possibly absent entirely (SQLite burst
|
||||
coalescing; the pg no-replay hole) — recovery is the consumer's
|
||||
re-read plus the engine's reconnect surface, never a guaranteed
|
||||
deliver. **Consumers must be idempotent on wake.**
|
||||
- Failure surfaces as channel events, never silence: watcher death
|
||||
closes the receiver (`recv() -> None`, SQLite); the synthetic
|
||||
reconnect-wake (a reserved channel
|
||||
@@ -130,7 +133,11 @@ transaction-aware (`save_offset_tx` gives exactly-once-within-a-
|
||||
business-tx shape).
|
||||
|
||||
- `publish` / `publish_tx` / `publish_with_key` — append to the
|
||||
stream's durable log.
|
||||
stream's durable log. *(Depth annotation: the key's semantics, the
|
||||
`StreamEvent` shape, the ordering row, and log retention are
|
||||
OQ-12's — method names pinned by
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §1, depth deferred
|
||||
there; see [open-questions.md](open-questions.md).)*
|
||||
- `read_since` / `read_from_consumer(offset)` — cursor-based reads;
|
||||
`get_offset(consumer)` — checkpoint inspection.
|
||||
- `save_offset` / `save_offset_tx` — consumer checkpoint, explicit;
|
||||
@@ -142,6 +149,13 @@ business-tx shape).
|
||||
consumption: attach, read to current tail, resume after restart
|
||||
from the stored offset; explicit `save_offset` on the receiver
|
||||
(shape in [ADR-008](decisions/008-contract-v1-pinning.md) §8).
|
||||
Consumption is wake-driven with table re-read — the same
|
||||
mechanism split as queues (durable row, LISTEN/watcher wake,
|
||||
[ADR-006](decisions/006-wake-and-delivery-contract.md)); the
|
||||
receiver's engine-side trigger is engine-internal, but events
|
||||
never require polling to become visible (the queues row's pg
|
||||
re-poll safety net is an admission of LISTEN loss, not the
|
||||
posture).
|
||||
|
||||
### queues
|
||||
|
||||
@@ -165,7 +179,14 @@ obligations:
|
||||
attempt**; `retry(err, None)` computes the queue's equal-jitter
|
||||
exponential delay (range pinned by
|
||||
[ADR-010](decisions/010-queue-semantics-depth.md) §3), `retry(err,
|
||||
Some(d))` overrides; `fail` = immediate dead-letter. Dead letters:
|
||||
Some(d))` overrides; `fail` = immediate dead-letter. Handle-op
|
||||
validity (uniform predicate): an op succeeds only while the row is
|
||||
`processing` and the caller's claim deadline is unexpired — deadline
|
||||
lapse refuses all handle ops (heartbeats, acks included) and a
|
||||
reclaim does too; the dual-execution window's worker may still
|
||||
complete its work but its ack will not land (at-least-once, as
|
||||
documented); false, not error
|
||||
([ADR-010](decisions/010-queue-semantics-depth.md) §2). Dead letters:
|
||||
move-to-dead storage, `get_job` sees dead rows (with
|
||||
`last_error`/`died_at`), retention via `dead_letter_retention_s`
|
||||
(default forever), no redrive API
|
||||
@@ -181,7 +202,13 @@ obligations:
|
||||
|
||||
### named locks
|
||||
|
||||
TTL-bounded coordination locks, transactional-friendly:
|
||||
TTL-bounded coordination locks. Lock rows live in the same database
|
||||
as the caller's business data — coordination and data co-locate —
|
||||
but lock ops are **not** part of the tx seam: there is no `lock_tx`;
|
||||
a lock held across a business transaction is held by explicit
|
||||
acquisition before/inside it, not transaction-scoped (acquisition is
|
||||
a separate auto-commit op; rollback of the business tx does not
|
||||
release it — the TTL discipline governs).
|
||||
|
||||
- `try_lock(name, owner, ttl)` — acquire or fail (`Option<Lock>` —
|
||||
no-work is a value, not an error); `renew` and `release` on the
|
||||
@@ -201,7 +228,19 @@ TTL-bounded coordination locks, transactional-friendly:
|
||||
A helper over queues, not a separate mechanism: enqueue inside the
|
||||
business transaction + the delivery/consumption worker entry points.
|
||||
Same evidence base and guarantee as queues
|
||||
([ADR-002](decisions/002-feature-scope.md)).
|
||||
([ADR-002](decisions/002-feature-scope.md)). Worker semantics
|
||||
(honker's `run_once` shape, inherited as the pinned posture):
|
||||
`run_once(worker_id, delivery)` is a *pull* op — claim one job, run
|
||||
the delivery closure, `ack` on `Ok`, `retry(err, None)` (the queue's
|
||||
curve) on `Err`; the consumer calls it in its own loop. **No
|
||||
heartbeat inside delivery** (honker parity, inherited deliberately):
|
||||
delivery slower than the backing queue's stamped visibility timeout
|
||||
(default 60 s for outbox-backed queues) can have its claim expire
|
||||
mid-delivery and be redelivered — the dual-execution window;
|
||||
idempotent delivery is the consumer's obligation, same as queues.
|
||||
The transactional *enqueue* side's shape is OQ-13's (the v1 surface
|
||||
has a real hole there — see
|
||||
[open-questions.md](open-questions.md)).
|
||||
|
||||
### scheduler
|
||||
|
||||
@@ -213,9 +252,12 @@ the reserved name `__alkstore_scheduler`). Schedules never fire
|
||||
without a runner (no ambient timers). v1 spec grammar:
|
||||
`@every <n><unit>` only (`s|m|h|d`); cron strings are rejected
|
||||
(`InvalidSpec`) pending a consumer-inventory row naming wall-clock
|
||||
cron. Boundary guarantee:
|
||||
[ADR-009](decisions/009-scheduler-collapse.md) §4 (at-least-once per
|
||||
elapsed boundary, bounded catch-up with skip-forward past the cap).
|
||||
cron. Each boundary fire enqueues ordinary work stamped with
|
||||
`ScheduleOpts { priority, max_attempts, expires }`
|
||||
([ADR-009](decisions/009-scheduler-collapse.md) §3). Boundary
|
||||
guarantee: [ADR-009](decisions/009-scheduler-collapse.md) §4
|
||||
(at-least-once per elapsed boundary, bounded catch-up with
|
||||
skip-forward past the cap).
|
||||
|
||||
## Cross-cutting contracts
|
||||
|
||||
@@ -228,7 +270,7 @@ of record for notify/streams/queues; the locks row is
|
||||
row is [ADR-009](decisions/009-scheduler-collapse.md) §4's. This spec
|
||||
inherits them and adds the consumer obligations:
|
||||
|
||||
- wake idempotence (notify's at-least-once, coalescing behavior);
|
||||
- wake idempotence (notify's best-effort, coalescing behavior);
|
||||
- explicit offset saves (streams);
|
||||
- visibility-timeout budgeting — heartbeat inside the deadline for
|
||||
long work; the dual-execution window and reclaim-eats-attempt rules
|
||||
@@ -335,6 +377,13 @@ before the engine specs are called `stable`:
|
||||
per [ADR-012](decisions/012-forked-substrate-design.md) §2's
|
||||
contract-blind boundary; the contract suite must pin both engines'
|
||||
arithmetic to identical outputs.
|
||||
- **Job-handle validity predicate on both engines** — the uniform
|
||||
processing-state + unexpired-deadline rule
|
||||
([ADR-010](decisions/010-queue-semantics-depth.md) §2): the
|
||||
late-heartbeat boundary (refused exactly when the deadline has
|
||||
lapsed), the post-lapse-ack-refusal (at-least-once), and the
|
||||
ack-vs-reclaim race (one wins atomically); both engines' SQL pin
|
||||
identical outcomes in the contract suite.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
@@ -359,6 +408,10 @@ questions affecting this document:
|
||||
- **OQ-10**: contract versioning discipline across engine crates
|
||||
([open](open-questions.md))
|
||||
- **OQ-08**: capability-surface shape ([open](open-questions.md))
|
||||
- **OQ-12**: streams depth — key semantics, `StreamEvent` shape,
|
||||
ordering row, retention ([open](open-questions.md))
|
||||
- **OQ-13**: transactional outbox enqueue shape
|
||||
([open](open-questions.md))
|
||||
|
||||
Resolved on this document's surface: **OQ-09** (scheduler collapse —
|
||||
[ADR-009](decisions/009-scheduler-collapse.md)) and **OQ-05** (queue
|
||||
|
||||
Reference in new issue
Block a user