ADR-019/020: second review round — handle surfaces, enqueue semantics, payload bridge + mechanical fixes
ADR-019: mechanism-handle traits pinned (Queue/StreamHandle/Outbox/ Lock/JobHandle), Job/Schedule struct shapes, worker_id claimant identity, core StopToken — closes the 'pin at implementation' residue before ADR-017's amend-in-place window terminates at first release. ADR-020: delay-wins-over-run_at precedence (honker parity), relative expires, scheduler-fired stamps from derived queue defaults, serde_json byte-level payload encoding. Mechanical: overview/engine-postgres ADR tables completed through 020, core-contract Errors section re-framed to class-1, ADR-014 sketch annotated with publish_with_key_tx, ADR-015 status dated, reserved- namespace kinds list normalized to six kinds, redrive gate wording aligned, broken link fixed, StopToken deferral superseded in ADR-009.
This commit is contained in:
1 parent
c49befd195
commit
8323a7853e
13 files changed
+661
-54
No files matched your search
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-06
|
||||
last_updated: 2026-10-06 (ADR-019/020 — handle surfaces, enqueue semantics, payload bridge)
|
||||
---
|
||||
|
||||
# Core contract
|
||||
@@ -87,13 +87,17 @@ present key must be non-empty → `InvalidName`) and is
|
||||
point ([ADR-015](decisions/015-streams-depth.md) §2).
|
||||
|
||||
Mechanism handles come off the store (or, for transactional variants,
|
||||
off the handle):
|
||||
off the handle). **Payload encoding** ([ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
|
||||
§4): the trait crosses `serde_json::Value`; engines serialize it with
|
||||
serde_json and store exactly those bytes — `payload_as<T>` decodes
|
||||
them, `Codec` on failure; round-trip (publish → read → decode →
|
||||
`Value` equality) is what the contract suite pins, on both engines.
|
||||
|
||||
```text
|
||||
store.notify(channel, payload) handle.notify_tx(channel, payload)
|
||||
store.stream(name) -> Stream handle.publish_tx / publish_with_key_tx
|
||||
store.stream(name) -> StreamHandle handle.publish_tx / publish_with_key_tx
|
||||
handle.save_offset_tx
|
||||
(the Stream handle also carries
|
||||
(the StreamHandle also carries
|
||||
trim_to — stream-side, outside the
|
||||
tx seam)
|
||||
store.queue(name, opts) -> Queue handle.enqueue_tx
|
||||
@@ -102,20 +106,34 @@ store.try_lock(name, owner, ttl) -> Option<Lock>
|
||||
store.listen(channel) -> Box<dyn WakeReceiver>
|
||||
store.schedule(name, spec, queue, payload, opts) -> Result<Schedule>
|
||||
store.unschedule(name) -> bool
|
||||
store.run_schedules(stop) -> Result<()>
|
||||
store.run_schedules(stop: StopToken) -> Result<()>
|
||||
```
|
||||
|
||||
The mechanism handles' full trait surfaces (`Queue { enqueue,
|
||||
claim_one, claim_batch, ack_batch, cancel, get_job, sweep_expired }`,
|
||||
`StreamHandle { publish, publish_with_key, read_since,
|
||||
read_from_consumer, save_offset, get_offset, trim_to, subscribe }`,
|
||||
`Outbox { enqueue, run_once }`, `Lock { renew, release }`, plus the
|
||||
boxed `JobHandle` a claim returns) are pinned by
|
||||
[ADR-019](decisions/019-mechanism-handle-surfaces.md); the
|
||||
`StopToken` parameter is a core-crate type (same ADR, §4).
|
||||
|
||||
Payloads cross the trait as core value types (non-generic trait
|
||||
methods — object safety of the boxed handles,
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §2); `payload_as<T>`
|
||||
decodes on concrete returned values (`Job`, `StreamEvent`). The
|
||||
`with_tx` closure wrapper ships over the handle shape (the POC-verified
|
||||
composition direction, [ADR-007](decisions/007-transactional-seam.md)).
|
||||
Mechanism handles (`Stream`, `Queue`, `Outbox`, `Lock`) are core-owned
|
||||
trait objects like the tx handle — engine types never appear in
|
||||
consumer signatures; their trait methods pin at implementation,
|
||||
mirroring the `TxHandle` pattern ([ADR-008](decisions/008-contract-v1-pinning.md)
|
||||
§2's rationale applies identically).
|
||||
Mechanism handles (`Queue`, `StreamHandle`, `Outbox`, `Lock`) are
|
||||
core-owned trait objects like the tx handle — engine types never appear
|
||||
in consumer signatures. Their trait surfaces are pinned by
|
||||
[ADR-019](decisions/019-mechanism-handle-surfaces.md) (placement
|
||||
mirroring the honker-rs starting artifact: claim/maintenance ops on the
|
||||
`Queue` handle, publish/read/trim on the `StreamHandle`, the claim ops
|
||||
on the boxed `JobHandle`, `run_once` on the `Outbox`); trait-method
|
||||
additions on these traits post-release ride
|
||||
[ADR-017](decisions/017-contract-versioning.md) class 2 like any
|
||||
other trait surface.
|
||||
|
||||
## Mechanism contracts
|
||||
|
||||
@@ -200,11 +218,18 @@ business-tx shape). Depth pinned by
|
||||
cadence — honker's per-binding ambiguity is not inherited). The
|
||||
subscription handle exposes no `save_every` / auto-save-on-drop
|
||||
([ADR-008](decisions/008-contract-v1-pinning.md) §8). Saves are
|
||||
monotone; a saved offset below the trim horizon
|
||||
monotone — a save below the stored checkpoint is a silent no-op —
|
||||
so the direct form and the receiver's no-arg form
|
||||
(`EventReceiver::save_offset(&mut self)`, which saves the
|
||||
last-yielded event's offset through the same op,
|
||||
[ADR-019](decisions/019-mechanism-handle-surfaces.md) §6)
|
||||
interleave freely and cannot regress one another. A saved offset
|
||||
below the trim horizon
|
||||
([ADR-015](decisions/015-streams-depth.md) §5) stays a valid
|
||||
position marker.
|
||||
- `trim_to(horizon)` — delete events with `offset <= horizon`
|
||||
([ADR-015](decisions/015-streams-depth.md) §5). The stream-side
|
||||
- `trim_to(horizon) -> count` — delete events with `offset <= horizon`
|
||||
([ADR-015](decisions/015-streams-depth.md) §5; the return is the
|
||||
deleted-row count, [ADR-019](decisions/019-mechanism-handle-surfaces.md)). The stream-side
|
||||
bounded-growth op, consumer-invoked, no engine-default
|
||||
retention and no ambient sweeper (the ADR-010 §6 posture; the
|
||||
replay-forever default is the mechanism's purpose — growth is
|
||||
@@ -232,14 +257,30 @@ Durable at-least-once work
|
||||
obligations:
|
||||
|
||||
- `enqueue` / `enqueue_tx` — commit-atomic; `EnqueueOpts { delay,
|
||||
run_at, priority, max_attempts, expires }`; queue-level
|
||||
run_at, priority, max_attempts, expires }`; **option semantics
|
||||
([ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md))**:
|
||||
ready time resolves `delay` → `now + delay` (wins) over `run_at` →
|
||||
literal absolute over neither → now; `expires` is relative seconds
|
||||
from enqueue resolved to an absolute row `expires_at` (`None` =
|
||||
never); the resolved values — not the raw fields — are what
|
||||
`get_job` shows. Queue-level
|
||||
`QueueOpts { visibility_timeout_s, max_attempts, backoff_base_s,
|
||||
dead_letter_retention_s }` ([ADR-010](decisions/010-queue-semantics-depth.md)
|
||||
§3/§4).
|
||||
§3/§3a) stamp onto each job row at enqueue, resolved over the
|
||||
handle's opts (auto-commit/tx paths) or the engine's derived
|
||||
defaults (outbox backing queues, scheduler boundary fires — the
|
||||
no-handle-open shapes, ADR-014/ADR-020 §3).
|
||||
- `claim_one` / `claim_batch` — exactly-once handout under concurrency
|
||||
(POC-pinned on both engines); claim ordering: priority DESC, then
|
||||
ready-time, then enqueue order (FIFO under equal priority).
|
||||
- Job handle: `ack / retry / fail / heartbeat`. `ack` deletes the row;
|
||||
(POC-pinned on both engines); take a caller-supplied `worker_id`
|
||||
(claimant identity — consumer-local, no reserved rule, non-empty;
|
||||
[ADR-019](decisions/019-mechanism-handle-surfaces.md) §2); claim
|
||||
ordering: priority DESC, then
|
||||
ready-time, then enqueue order (FIFO under equal priority). Claims
|
||||
return boxed `JobHandle`s — ops + row value (same ADR, §3).
|
||||
- Job handle: `ack / retry / fail / heartbeat` on the boxed
|
||||
`JobHandle` a claim returns ([ADR-019](decisions/019-mechanism-handle-surfaces.md)
|
||||
§3 — `job(&self) -> &Job` beside the ops; one-shot `self: Box<Self>`
|
||||
ops like the tx commit). `ack` deletes the row;
|
||||
`heartbeat(extend)` is *renewal* — an absolute reset of the claim
|
||||
deadline (extend is the new full deadline from now, **not additive**
|
||||
to elapsed time); late heartbeat refused; **a reclaim consumes an
|
||||
@@ -296,8 +337,11 @@ 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)). 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
|
||||
(honker's `run_once` shape, inherited as the pinned posture; the
|
||||
delivery callable's shape pinned by
|
||||
[ADR-019](decisions/019-mechanism-handle-surfaces.md) §5):
|
||||
`run_once(worker_id, delivery) -> bool` 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):
|
||||
@@ -312,8 +356,9 @@ payload)` on the `TxHandle` trait
|
||||
name (validated like `store.outbox(name)` — empty → `InvalidName`,
|
||||
reserved-prefixed → `ReservedName`), derives the backing queue name
|
||||
engine-side, and lands the job row in the caller's transaction with
|
||||
`EnqueueOpts` stamped per [ADR-010](decisions/010-queue-semantics-
|
||||
depth.md) §3a over the backing queue's derived `QueueOpts`. Commit
|
||||
`EnqueueOpts` stamped per
|
||||
[ADR-010](decisions/010-queue-semantics-depth.md) §3a over the backing
|
||||
queue's derived `QueueOpts`. Commit
|
||||
makes the job visible to `run_once` exactly when the business write
|
||||
commits; rollback drops both (the no-ghosts property,
|
||||
[ADR-007](decisions/007-transactional-seam.md)). The derived backing
|
||||
@@ -326,15 +371,21 @@ itself the guarantee that the derivation cannot be collided with.
|
||||
|
||||
Collapsed into queues per [ADR-009](decisions/009-scheduler-collapse.md):
|
||||
`schedule(name, spec, queue, payload, opts)` (upsert by name),
|
||||
`unschedule(name)`, and the opt-in `run_schedules(stop)` runner
|
||||
(leader-elected where the engine has peers — the leadership lock is
|
||||
the reserved name `__alkstore_scheduler`). Schedules never fire
|
||||
`unschedule(name)`, and the opt-in `run_schedules(stop: StopToken)`
|
||||
runner (`StopToken` is a core-crate cloneable cancel handle —
|
||||
[ADR-019](decisions/019-mechanism-handle-surfaces.md) §4; the
|
||||
leader-elected leadership lock is 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. Each boundary fire enqueues ordinary work stamped with
|
||||
`ScheduleOpts { priority, max_attempts, expires }`
|
||||
([ADR-009](decisions/009-scheduler-collapse.md) §3). Boundary
|
||||
([ADR-009](decisions/009-scheduler-collapse.md) §3) — resolved over
|
||||
the target queue's *derived engine defaults* (visibility/backoff/
|
||||
retention; no handle is open), per
|
||||
[ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.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).
|
||||
@@ -372,11 +423,12 @@ inherits them and adds the consumer obligations:
|
||||
`Error`, with v1 variants `PayloadTooLarge` (universal; produced
|
||||
pg-side, contract-wide matchable), `ReservedName`, `InvalidName`,
|
||||
`Closed`, `Codec`, and the opaque `Database` fallback (engine detail
|
||||
preserved via the source chain). Post-v1 additions:
|
||||
`InvalidSpec` (schedule spec grammar) and `LeadershipLost`
|
||||
(`run_schedules` return on leadership loss) — both from
|
||||
[ADR-009](decisions/009-scheduler-collapse.md) §6, the only
|
||||
taxonomy deltas so far. Pinning rule: a variant exists only when
|
||||
preserved via the source chain). `InvalidSpec` (schedule spec grammar)
|
||||
and `LeadershipLost` (`run_schedules` return on leadership loss) —
|
||||
both from [ADR-009](decisions/009-scheduler-collapse.md) §6 — are
|
||||
part of contract v1's initial text (pre-release additions per
|
||||
[ADR-017](decisions/017-contract-versioning.md) class 1; the
|
||||
"post-v1 additions" framing an earlier draft used is retired). Pinning rule: a variant exists only when
|
||||
callers can act differently on it. Queue claim "no work" and job/lock
|
||||
boolean results are values, not errors; dead-letter is observable
|
||||
state (`get_job`), not an error.
|
||||
@@ -396,11 +448,13 @@ a consumer-inventory row naming a runtime-adapt need.
|
||||
|
||||
### Naming / reserved namespace
|
||||
|
||||
Consumer-visible names (channels, streams, queues, locks, and
|
||||
schedule names) share engine-visible namespaces on Postgres (LISTEN
|
||||
channel names are server-global per database). Pinned by
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §4 (schedule names
|
||||
added by [ADR-009](decisions/009-scheduler-collapse.md) §1):
|
||||
Consumer-visible names (channels, streams, queues, outboxes, locks,
|
||||
and schedule names) share engine-visible namespaces on Postgres (LISTEN
|
||||
channel names are server-global per database). The canonical kinds
|
||||
list is exactly these six (notify/stream/queue/outbox/lock/schedule;
|
||||
pinned by [ADR-008](decisions/008-contract-v1-pinning.md) §4, outbox
|
||||
added by [ADR-014](decisions/014-outbox-tx-enqueue.md), schedule names
|
||||
by [ADR-009](decisions/009-scheduler-collapse.md) §1):
|
||||
|
||||
- Reserved prefix **`__alkstore_`**, engine-independent, applies
|
||||
across all name kinds; the one v1-reserved string is
|
||||
@@ -503,6 +557,19 @@ before the engine specs are called `stable`:
|
||||
first remaining row, saved offsets below the horizon stay valid,
|
||||
trim wakes nothing, and a concurrent subscriber never loses its
|
||||
place.
|
||||
- **Save-offset monotone composition** ([ADR-019](decisions/019-mechanism-handle-surfaces.md)
|
||||
§6) — direct-form and receiver-form saves interleave without
|
||||
regression on both engines; a lower-offset save is a silent no-op;
|
||||
receiver saves land the last-yielded event's offset exactly.
|
||||
- **Payload encode/decode round-trip on both engines**
|
||||
([ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md) §4) —
|
||||
same `Value` published/enqueued on both engines stores byte-identical
|
||||
rows and decodes back to `Value` equality (`Codec` only on
|
||||
non-serializable target types).
|
||||
- **Enqueue-opts resolution equivalence** ([ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
|
||||
§1/§2/§3) — delay-over-run_at precedence, relative-expires
|
||||
resolution, and the scheduler-fired jobs' derived-default stamps
|
||||
(`get_job`-visible via the row) are identical on both engines.
|
||||
|
||||
## Design Decisions
|
||||
|
||||
@@ -521,6 +588,8 @@ before the engine specs are called `stable`:
|
||||
| [015](decisions/015-streams-depth.md) | Streams depth (amends 006/008) | key = carried metadata, global-FIFO ordering row, `StreamEvent` shape, `publish_with_key_tx`, `trim_to` |
|
||||
| [016](decisions/016-deployment-honesty.md) | Deployment honesty (decides 008's parked question) | no runtime capability surface — compile-time engine identity + documented matrix; `PayloadTooLarge` occurrence asymmetry pinned |
|
||||
| [017](decisions/017-contract-versioning.md) | Contract versioning (governs this surface's changes) | core crate's semver *is* the contract version; four change classes; amend-in-place ends at first release; pairing = manifest pin + version-stamped contract suite + docs |
|
||||
| [019](decisions/019-mechanism-handle-surfaces.md) | Mechanism handles (amends 008 §1/§2/§8) | handle traits pinned (`Queue`/`StreamHandle`/`Outbox`/`Lock`/`JobHandle`); `worker_id` claimant identity; `Job`/`Schedule` struct shapes; core-owned `StopToken`; save-offset composition |
|
||||
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue options + bridges (amends 008 §1/§2, 009 §3) | `delay` wins over `run_at`; `expires` = relative seconds; scheduler stamps from derived queue defaults; serde_json byte-level payload encoding |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -544,3 +613,9 @@ depth — [ADR-015](decisions/015-streams-depth.md)), **OQ-08**
|
||||
[ADR-016](decisions/016-deployment-honesty.md)), and **OQ-10**
|
||||
(the versioning discipline this document's surface is governed by —
|
||||
[ADR-017](decisions/017-contract-versioning.md)), 2026-10-05/06.
|
||||
The second review round (2026-10-06) found no new open questions —
|
||||
its findings resolved directly as
|
||||
[ADR-019](decisions/019-mechanism-handle-surfaces.md) (handle
|
||||
surfaces, value shapes, stop token) and
|
||||
[ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
|
||||
(enqueue-option semantics, scheduler stamp source, payload encoding).
|
||||
Reference in new issue
Block a user