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 (ADR-018; OQ-11 resolved — question set closed)
|
||||
last_updated: 2026-10-06 (ADR-019/020 — second review round resolved)
|
||||
---
|
||||
|
||||
# alkstore — Architecture
|
||||
@@ -53,6 +53,8 @@ pending architecture review and OQ resolution.
|
||||
| [016](decisions/016-deployment-honesty.md) | Deployment honesty — no runtime capability surface; compile-time engine identity + documented matrix | Accepted |
|
||||
| [017](decisions/017-contract-versioning.md) | Contract versioning — core crate's semver is the contract version; change classes, pairing carriers, lockstep duties | Accepted |
|
||||
| [018](decisions/018-provenance-register-and-cherry-picks.md) | Substrate provenance register and cherry-pick procedure — `PROVENANCE.md` in-tree, per-delta category-tagged entries, five-step adoption discipline | Accepted |
|
||||
| [019](decisions/019-mechanism-handle-surfaces.md) | Mechanism-handle surfaces — handle traits (`Queue`/`StreamHandle`/`Outbox`/`Lock`/`JobHandle`), `Job`/`Schedule` shapes, `worker_id` identity, core `StopToken` | Accepted |
|
||||
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue-option semantics — delay/run_at precedence, relative `expires`, scheduler stamp source, serde_json payload encoding | Accepted |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -81,8 +83,10 @@ depth — [ADR-015](decisions/015-streams-depth.md)), ~~OQ-10~~
|
||||
(fork follow-through — provenance register + cherry-pick procedure,
|
||||
[ADR-018](decisions/018-provenance-register-and-cherry-picks.md)).
|
||||
|
||||
No deferred OQs. With the question set closed, Phase 1 moves to
|
||||
architecture review closure and the implementation-phase gates.
|
||||
No deferred OQs. With the question set closed (and the 2026-10-06
|
||||
second review round resolving its findings directly as ADR-019/ADR-020
|
||||
rather than as new OQs), Phase 1 moves to architecture review closure
|
||||
and the implementation-phase gates.
|
||||
|
||||
## Document Lifecycle
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -81,9 +81,11 @@ store.run_schedules(stop) -> Result<()> // runs until `stop`
|
||||
leadership ends the runner — the consumer's recipe is to respawn it
|
||||
(documented); the lock loss always precedes any stolen fire, so
|
||||
respawn never double-fires a boundary.
|
||||
- **Stop and return semantics**: `stop` is a cancellation token
|
||||
(a cloneable handle whose flip ends the sleep early — the exact
|
||||
type shapes at implementation, engine-crate docs). A leadership
|
||||
- **Stop and return semantics**: `stop` is a cancellation token — a
|
||||
core-crate cloneable `StopToken` (pinned by
|
||||
[ADR-019](019-mechanism-handle-surfaces.md) §4, 2026-10-06; the
|
||||
original "exact type shapes at implementation" deferral is
|
||||
superseded). A leadership
|
||||
loss returns `Err(LeadershipLost)` — a distinct, matchable outcome
|
||||
from the clean `Ok(())` a stop produces; the respawn recipe matches
|
||||
on it. (One taxonomy note: `LeadershipLost` is a *return-shape*
|
||||
|
||||
@@ -54,6 +54,9 @@ One method, added to the `TxHandle` trait:
|
||||
trait TxHandle {
|
||||
enqueue_tx(queue, opts, payload) -> job_id
|
||||
publish_tx(stream, payload) -> offset
|
||||
publish_with_key_tx(stream, key, payload) -> offset // ADR-015,
|
||||
// same day — not in this
|
||||
// ADR's original sketch
|
||||
notify_tx(channel, payload)
|
||||
save_offset_tx(stream, consumer, offset)
|
||||
outbox_enqueue_tx(outbox, opts, payload) -> job_id // the addition
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
Accepted (2026-10-05, Phase 1 —
|
||||
[OQ-12](../open-questions.md)'s resolution)
|
||||
|
||||
## Context
|
||||
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
# ADR-019: Mechanism-handle surfaces — handle traits, value shapes, claimant identity, stop token
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-10-06, Phase 1 — second architecture review round,
|
||||
pre-decomposition; amends [ADR-008](008-contract-v1-pinning.md) §1/§2/
|
||||
§8 in place, pre-implementation ([ADR-017](017-contract-versioning.md)
|
||||
class 1 — no release exists; the amend-in-place frame is still open))
|
||||
|
||||
## Context
|
||||
|
||||
Contract v1 pinned the `TxHandle`, `WakeReceiver`, and `EventReceiver`
|
||||
trait shapes exactly, but the mechanism handles were left
|
||||
deliberately open:
|
||||
|
||||
- [core-contract.md](../core-contract.md) (pre-review): "Mechanism
|
||||
handles (`Stream`, `Queue`, `Outbox`, `Lock`) are core-owned trait
|
||||
objects … their trait methods pin at implementation, mirroring the
|
||||
`TxHandle` pattern."
|
||||
- [ADR-009](009-scheduler-collapse.md) §1 deferred the `run_schedules`
|
||||
stop parameter's type the same way ("the exact type shapes at
|
||||
implementation, engine-crate docs").
|
||||
- `Job` and `Schedule` — both `#[non_exhaustive]` consumer-read types
|
||||
under [ADR-017](017-contract-versioning.md) §3 — had prose-only
|
||||
field lists, split across ADR-010 §1/§3a and ADR-009 §1.
|
||||
- The two `save_offset` forms (the pinned
|
||||
`save_offset(stream, consumer, offset)` store/tx form and
|
||||
[ADR-008](008-contract-v1-pinning.md) §8's receiver-side
|
||||
`save_offset(&mut self)`) had no stated composition.
|
||||
|
||||
The second architecture review flagged that "pin at implementation"
|
||||
no longer holds: [ADR-017](017-contract-versioning.md) makes trait
|
||||
methods the lockstep-priced additive class and their shapes permanent
|
||||
contract surface, and that ADR's class-1 window (amend-in-place)
|
||||
**terminates at the core crate's first release** — and these shapes
|
||||
ship in that first release. So the pinning must happen in an ADR now,
|
||||
or the implementation agent would be authoring versioned contract
|
||||
surface with no decision record behind it.
|
||||
|
||||
The decisions below are derived, not invented: honker-rs
|
||||
(`/workspace/honker` `packages/honker-rs/src/lib.rs` @ f4e53c6) is the
|
||||
starting artifact every placement so far has mirrored, and its handle
|
||||
placement is the inherited shape — `[ADR-002](002-feature-scope.md)`'s
|
||||
"fidelity where kept" posture applied to the *contract* side the way
|
||||
[ADR-012](012-forked-substrate-design.md) §3 applied it to the
|
||||
substrate's.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. Handle placement mirrors honker-rs — mechanism ops live on the mechanism handles
|
||||
|
||||
Naming, construction, and placement (the return types are boxed
|
||||
trait objects — core-owned traits, engine types never in consumer
|
||||
signatures, the [ADR-008](008-contract-v1-pinning.md) §2 posture):
|
||||
|
||||
```text
|
||||
store.queue(name, opts) -> Box<dyn Queue>
|
||||
store.stream(name) -> Box<dyn StreamHandle>
|
||||
store.outbox(name) -> Box<dyn Outbox>
|
||||
store.try_lock(name, owner, ttl) -> Option<Box<dyn Lock>>
|
||||
```
|
||||
|
||||
*(Trait-name note: the stream handle's trait is `StreamHandle`, not
|
||||
`Stream` — `StreamEvent` already occupies the type-name's vocabulary
|
||||
and a bare `Stream` trait beside `EventReceiver` invites confusion
|
||||
with tokio's `Stream`. The honker-rs struct name maps to
|
||||
`StreamHandle`; everything else keeps honker's names.)*
|
||||
|
||||
```text
|
||||
trait Queue {
|
||||
name(&self) -> &str
|
||||
enqueue(payload, EnqueueOpts) -> job_id
|
||||
claim_one(worker_id) -> Option<Box<dyn JobHandle>>
|
||||
claim_batch(worker_id, n) -> Vec<Box<dyn JobHandle>>
|
||||
ack_batch(ids) -> count // non-claimed ids not counted
|
||||
cancel(job_id) -> bool // unconditional delete
|
||||
get_job(job_id) -> Option<Job> // sees dead rows; a value,
|
||||
// no ops — see §2
|
||||
sweep_expired() -> count // this queue's rows
|
||||
}
|
||||
|
||||
trait StreamHandle {
|
||||
name(&self) -> &str
|
||||
publish(payload) -> offset
|
||||
publish_with_key(key, payload) -> offset
|
||||
read_since(offset, limit) -> Vec<StreamEvent>
|
||||
read_from_consumer(consumer, limit) -> Vec<StreamEvent>
|
||||
save_offset(consumer, offset)
|
||||
get_offset(consumer) -> i64 // absent consumer = 0
|
||||
trim_to(horizon) -> count // deletes offset <= horizon
|
||||
subscribe(consumer) -> Box<EventReceiver> // shape: ADR-008 §8
|
||||
}
|
||||
|
||||
trait Outbox {
|
||||
name(&self) -> &str
|
||||
enqueue(payload, EnqueueOpts) -> job_id // into the derived
|
||||
// backing queue
|
||||
run_once(worker_id, delivery) -> bool // see §4
|
||||
}
|
||||
|
||||
trait Lock {
|
||||
name(&self) -> &str
|
||||
renew(ttl) -> bool // new full TTL window from
|
||||
// now; false = lost it
|
||||
release(self: Box<Self>) -> bool // consuming; true if held
|
||||
}
|
||||
```
|
||||
|
||||
- **Why mirror honker's placement**: it is the starting artifact's
|
||||
proven shape; every prior decision (claim ordering, QueueOpts
|
||||
stamping, the v1 skeleton's method *names*) already assumed
|
||||
queue-scoped claim/maintenance ops — `sweep_expired(queue)` on
|
||||
`Store` would take the name argument that the handle already
|
||||
carries; a `Queue` handle without `claim_*` would be a construction
|
||||
ceremony with no behavior. Honker's `Job`-ops-on-claimed-work shape
|
||||
and `Lock::release(self)` RAII shape are likewise inherited.
|
||||
- `get_job`, `save_offset`, `get_offset`, `read_*` remain available
|
||||
from *inside* a transaction through the `TxHandle`'s `*_tx` methods
|
||||
— the handle forms are the auto-commit convenience counterparts
|
||||
([ADR-007](007-transactional-seam.md)).
|
||||
- The **reserved-prefix / name validation rules apply to the
|
||||
constructors** (`queue`/`stream`/`outbox`/`try_lock`) exactly as
|
||||
pinned by [ADR-008](008-contract-v1-pinning.md) §4 — nothing new;
|
||||
the handles carry validated names.
|
||||
- Trait *method-set* additions on these handle traits post-release
|
||||
are [ADR-017](017-contract-versioning.md) class 2 — the same
|
||||
lockstep cost as `TxHandle` additions; stated here so the cost is
|
||||
priced before any consumer depends on it.
|
||||
|
||||
### 2. Claimant identity — `worker_id`, caller-supplied and consumer-local
|
||||
|
||||
- `claim_one`/`claim_batch`/`run_once` take a **`worker_id: &str`** —
|
||||
the claim's ownership token, stamping the row's `worker_id` column.
|
||||
It is a **consumer-local identifier in the stream-consumer-name
|
||||
class** ([ADR-008](008-contract-v1-pinning.md) §4: no reserved-
|
||||
prefix rule, no shared namespace): callers self-supply (a host id, a
|
||||
task label); the contract requires only non-emptiness
|
||||
(`InvalidName` otherwise — the v1 skeleton's only name-kind rule
|
||||
addition, same variant, no new taxonomy).
|
||||
- Cross-process semantics are the queue mechanism's, not the
|
||||
identifier's: claim exclusivity comes from the claim statement's
|
||||
atomicity ([ADR-010](010-queue-semantics-depth.md)), not from id
|
||||
uniqueness; two workers self-supplying the same string is their
|
||||
confusion to avoid (ops tooling reads it, nothing enforces it).
|
||||
|
||||
### 3. Value shapes — `Job` and `Schedule` pinned as structs
|
||||
|
||||
Both `#[non_exhaustive]` per [ADR-017](017-contract-versioning.md) §3
|
||||
(field additions ride class 2):
|
||||
|
||||
```text
|
||||
struct Job {
|
||||
id: i64,
|
||||
queue: String,
|
||||
state: JobState, // enum: Pending | Processing
|
||||
// | Dead
|
||||
payload: Vec<u8>,
|
||||
priority: i64,
|
||||
run_at: i64, // the claim-order key
|
||||
attempts: i64, // every claim counts
|
||||
max_attempts: i64, // the row's stamp
|
||||
worker_id: Option<String>, // claimant, None pre-claim
|
||||
claim_expires_at: Option<i64>, // deadline, None pre-claim
|
||||
created_at: i64, // unix seconds
|
||||
expires_at: Option<i64>, // job-level expiry, None =
|
||||
// never
|
||||
visibility_timeout_s: i64, // the §3a stamps, dead rows
|
||||
backoff_base_s: i64, // included (get_job returns
|
||||
dead_letter_retention_s: Option<i64>, // the stamps with the row)
|
||||
last_error: Option<String>, // dead-only (None on live)
|
||||
died_at: Option<i64>, // dead-only
|
||||
}
|
||||
```
|
||||
|
||||
- Single `Job` type, two carriers: **claims return
|
||||
`Box<dyn JobHandle>`** — ops + row:
|
||||
|
||||
```text
|
||||
trait JobHandle {
|
||||
job(&self) -> &Job // the row, as a value
|
||||
ack(self: Box<Self>) -> bool
|
||||
retry(self: Box<Self>, err: Option<String>, delay: Option<i64>)
|
||||
// None delay = queue curve
|
||||
fail(self: Box<Self>, err: Option<String>) -> bool
|
||||
heartbeat(self, extend: i64) -> bool // absolute reset
|
||||
}
|
||||
```
|
||||
|
||||
— and **`get_job` returns `Option<Job>`** (pure data). The *same
|
||||
uniform validity predicate* ([ADR-010](010-queue-semantics-depth.md)
|
||||
§2 — `processing` + unexpired caller deadline) governs a
|
||||
`JobHandle`'s ops; a get_job row yields no handle, so no ghost ops
|
||||
exist on it. Ops take `self: Box<Self>` mirroring `TxHandle`'s
|
||||
commit shape (consumed handle, one-shot discipline).
|
||||
`err: Option<String>`: `None` = the engine's
|
||||
documented-default string (taxonomy-consistent: these strings are
|
||||
row content, `get_job` data — not matched error variants).
|
||||
- `Schedule` (the `schedule()` read-back value):
|
||||
`Schedule { name: String, spec: String, queue: String, opts:
|
||||
ScheduleOpts }`.
|
||||
|
||||
### 4. `run_schedules(stop)` — the stop token is a core-crate type
|
||||
|
||||
```text
|
||||
store.run_schedules(stop: StopToken) -> Result<()>
|
||||
|
||||
#[derive(Clone)] struct StopToken
|
||||
impl StopToken {
|
||||
fn cancel(&self)
|
||||
fn is_cancelled(&self) -> bool
|
||||
}
|
||||
```
|
||||
|
||||
- **Pinned in core, not deferred**: the parameter type is contract
|
||||
surface ([ADR-017](017-contract-versioning.md) — a pinned method's
|
||||
signature), and a core-owned `StopToken` is exactly the
|
||||
config-split posture ([ADR-008](008-contract-v1-pinning.md) §6)
|
||||
in miniature — a contract-owned type whose *await mechanics* are
|
||||
engine-internal (tokio watch per engine; not contract).
|
||||
- Semantics ([ADR-009](009-scheduler-collapse.md) §1, unchanged):
|
||||
`cancel()` on any clone flips every clone;
|
||||
`run_schedules` ends its sleep early and returns `Ok(())` — the
|
||||
clean-stop return; leadership loss still returns
|
||||
`Err(LeadershipLost)` regardless of the token.
|
||||
|
||||
### 5. `run_once`'s delivery callable
|
||||
|
||||
`Outbox::run_once(worker_id, delivery) -> bool` (true = claimed and
|
||||
processed) — the delivery argument is the consumer-supplied async
|
||||
callable taking `Box<dyn JobHandle>`, returning the boxed future of
|
||||
`Result<()>` (object safety: no generic methods on the boxed
|
||||
handle — the [ADR-008](008-contract-v1-pinning.md) §2 boxed-future
|
||||
posture applied to the one remaining surface it applies to). Outcome
|
||||
posture is pinned as before: `Ok` ⇒ ack, `Err(e)` ⇒
|
||||
`retry(err, None)` (the queue curve). The exact callable *encoding*
|
||||
(`&mut dyn FnMut…`-form or a named one-method trait object) is
|
||||
Rust mechanics with one workable answer under object safety — the
|
||||
semantics above are the contract; the encoding follows §2's pinned
|
||||
pattern at implementation.
|
||||
|
||||
### 6. The two `save_offset` forms compose by monotonicity
|
||||
|
||||
One op, two conveniences: `StreamHandle::save_offset(consumer,
|
||||
offset)` / `save_offset_tx` write the checkpoint; the receiver's
|
||||
`save_offset(&mut self)` ([ADR-008](008-contract-v1-pinning.md) §8)
|
||||
writes **the receiver's last-yielded event's offset through the same
|
||||
op**. Saves are monotone (a save whose offset is below the stored
|
||||
checkpoint is a silent no-op — inherited: honker's
|
||||
`ON CONFLICT … WHERE excluded.offset > existing` upsert), so the
|
||||
forms interleave freely — they cannot regress or fight one another.
|
||||
No-offset-regression is the pinned discipline: replay is a *read*
|
||||
concern (`read_since(offset)` works from any offset; saved checkpoints
|
||||
do not gate reads). Returns `Result<()>` — refusal (regression) is a
|
||||
no-op, not caller-actionable (the act-differently rule,
|
||||
[ADR-008](008-contract-v1-pinning.md) §5).
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**
|
||||
|
||||
- Every handle a consumer constructs is now fully specified surface:
|
||||
decomposition off the current text can write the contract-crate
|
||||
task without inventing contract shapes.
|
||||
- The last "pin at implementation" deferral in the contract surface
|
||||
is closed, before the [ADR-017](017-contract-versioning.md) class-1
|
||||
window that makes such pinning possible ends.
|
||||
- Placement honesty: the contract mirrors the starting artifact that
|
||||
every prior queue/lock/stream decision assumed, so no
|
||||
re-interpretation debt accrues at implementation.
|
||||
|
||||
**Negative**
|
||||
|
||||
- Four more boxed-handle traits (plus `JobHandle`) for engines to
|
||||
implement — the same per-trait cost `TxHandle` carries; measured
|
||||
negligible against commit costs ([ADR-008](008-contract-v1-pinning.md)
|
||||
§2), but real surface.
|
||||
- Trait-method additions on five traits now ride ADR-017 class 2
|
||||
lockstep duties (engine adoption releases are mandatory on trait
|
||||
additions). Honest pricing, stated before consumers exist.
|
||||
- `Job`'s field list is pinned wide (all stamps, both namespaces of
|
||||
timestamps) — growth from here is class-2-minor via
|
||||
`#[non_exhaustive]`, but the initial text is larger than a minimal
|
||||
read type would be.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-008](008-contract-v1-pinning.md) §1/§2/§4/§5/§8 — the
|
||||
partition this completes, the boxed-handle/object-safety posture,
|
||||
the validation rules, the act-differently rule, the
|
||||
`EventReceiver` shape and rename table this ADR's handle shapes
|
||||
extend.
|
||||
- [ADR-007](007-transactional-seam.md) — the auto-commit-vs-`_tx`
|
||||
counterpart structure the handle forms slot into.
|
||||
- [ADR-009](009-scheduler-collapse.md) §1 — the
|
||||
`run_schedules(stop)` method whose stop type §4 pins; the return
|
||||
semantics §4 restates unchanged.
|
||||
- [ADR-010](010-queue-semantics-depth.md) §1/§2/§3a — the job
|
||||
lifecycle, the uniform validity predicate §3's `JobHandle` ops ride,
|
||||
the stamps `Job`'s fields carry.
|
||||
- [ADR-017](017-contract-versioning.md) §2 class 1/§3 — the
|
||||
amend-in-place window this ADR uses (and which its urgency derives
|
||||
from), and the `#[non_exhaustive]` classifications §3's structs
|
||||
satisfy.
|
||||
- honker-rs surface (`/workspace/honker`
|
||||
`packages/honker-rs/src/lib.rs` @ f4e53c6) — the placement record:
|
||||
`Queue`/`Outbox`/`Stream`/`Lock` methods, `JobRow`/`Job` split,
|
||||
`SaveOffset` upsert, `Lock::release(self)`, claim `worker_id`.
|
||||
- [core-contract.md](../core-contract.md) — the spec this ADR's §1–§5
|
||||
pin into place.
|
||||
@@ -0,0 +1,176 @@
|
||||
# ADR-020: Enqueue option semantics — delay/run_at, expires, scheduler stamp source, payload encoding
|
||||
|
||||
## Status
|
||||
|
||||
Accepted (2026-10-06, Phase 1 — second architecture review round,
|
||||
pre-decomposition; pre-implementation depth pinning per
|
||||
[ADR-017](017-contract-versioning.md) class 1, same frame as
|
||||
[ADR-014](014-outbox-tx-enqueue.md)/[ADR-015](015-streams-depth.md))
|
||||
|
||||
## Context
|
||||
|
||||
The second architecture review flagged four semantic holes where an
|
||||
implementer would otherwise improvise observable behavior:
|
||||
|
||||
1. **`delay` vs `run_at` precedence** — [ADR-008](008-contract-v1-pinning.md)
|
||||
§1 carries both fields in `EnqueueOpts` with only "`run_at` is the
|
||||
absolute-time counterpart of `delay`" for guidance. Claim ordering
|
||||
(`priority DESC`, ready-time `run_at` ASC — ADR-010 §1) silently
|
||||
presupposes an answer: what does an enqueue with both set mean?
|
||||
2. **`expires` polarity** — the v1 field set has one expiry field;
|
||||
relative lifetime or absolute deadline? The `expires_at` column and
|
||||
the sweep predicate depend on the answer.
|
||||
3. **Scheduler-fired jobs' stamp source** — [ADR-009](009-scheduler-collapse.md)
|
||||
§3 stamps boundary-fire enqueues "per [ADR-010](010-queue-semantics-depth.md)
|
||||
§3a", but §3a resolves stamps "over the queue handle's `QueueOpts`" —
|
||||
and the scheduler's tick enqueues internally, with no queue handle
|
||||
open. The outbox closed this gap explicitly (derived `QueueOpts`
|
||||
defaults, ADR-010 §3); the scheduler never did. Two engines could
|
||||
pick different defaults before the contract suite catches it.
|
||||
4. **The payload encoding bridge** — [ADR-008](008-contract-v1-pinning.md)
|
||||
§2 pins payloads crossing the trait as `serde_json::Value`;
|
||||
[ADR-015](015-streams-depth.md) §3 pins `StreamEvent.payload:
|
||||
Vec<u8>` ("core value bytes"); `payload_as<T>` (decode side,
|
||||
error `Codec`) is pinned. The *encode* side — what relationship the
|
||||
stored bytes bear to the `Value`, without which the round trip and
|
||||
the cross-engine byte-equality rows are undefined — is stated
|
||||
nowhere.
|
||||
|
||||
All four are decidable now from decided material + the starting
|
||||
artifact's source; none is a new design space.
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. `delay` wins over `run_at`; both resolve to the row's ready time
|
||||
|
||||
Pinned exactly as honker's enqueue already implements it
|
||||
(`honker-core/src/honker_ops.rs` `enqueue`: "delay set →
|
||||
`unixepoch() + delay` (wins over run_at)"):
|
||||
|
||||
- `EnqueueOpts { delay, run_at }` resolution precedence:
|
||||
**`delay` set → ready time = now + delay** (wins);
|
||||
**else `run_at` set → ready time = run_at literally** (absolute
|
||||
unix seconds); **else → now** (claimable immediately).
|
||||
- Both fields stay (honker parity, ADR-008 §1's "carried whole"); the
|
||||
resolution rule is the contract, and the row stores the resolved
|
||||
ready time — `get_job` shows the resolved `run_at`, never the
|
||||
enqueuer's raw fields.
|
||||
- No validation error for both-set: precedence is defined, not
|
||||
rejected (an error would be a third behavior for
|
||||
[ADR-008](008-contract-v1-pinning.md) §5's rule to price, where
|
||||
honker's defined precedence already exists and suffices).
|
||||
- `now` is the engine's single second-precision clock (the queues
|
||||
pin); the same instant is used for the delay resolution and the row
|
||||
write (one enqueue, one clock read).
|
||||
|
||||
### 2. `expires` is relative seconds from enqueue; `None` = never
|
||||
|
||||
Pinned as honker implements it (`expires_s: Option<i64>` → row
|
||||
`expires_at = unixepoch() + s`):
|
||||
|
||||
- `EnqueueOpts.expires: Option<i64>` is a **relative TTL in
|
||||
seconds from the enqueue instant**; the row's `expires_at` is
|
||||
resolved at enqueue and stored absolutely. `None` = no expiry.
|
||||
- This makes the stamping story uniform — every enqueue-time field
|
||||
resolves against the enqueue instant (delay → ready time; expires →
|
||||
expiry), and `get_job` shows the resolved absolute `expires_at`.
|
||||
- The consumer who wants absolute-form expiry computes
|
||||
`run_at`-style arithmetic on its side (`expires = Some(t - now)`);
|
||||
no second field is added (the v1 field set stays closed — an
|
||||
`expires_at` absolute twin would be an opts addition, class 3
|
||||
priced, with no row naming it and the arithmetic available).
|
||||
|
||||
### 3. Scheduler-fired jobs' stamps: the target queue's derived defaults, engine-resolved
|
||||
|
||||
The boundary-fire enqueue resolves stamps from **the named queue's
|
||||
derived QueueOpts defaults** — the engine's built-in defaults (honker
|
||||
parities: 300 s / 3 / 5 s / none), not a handle's opts and not
|
||||
`ScheduleOpts`:
|
||||
|
||||
- The tick enqueues internally; there is no queue handle in scope by
|
||||
design (queues are names, not registered objects —
|
||||
[ADR-009](009-scheduler-collapse.md) §5). The stamp source is
|
||||
therefore the **engine's queue-defaults resolution** — exactly the
|
||||
"no handle was opened" shape the outbox already resolved (derived
|
||||
defaults, [ADR-014](014-outbox-tx-enqueue.md) §1; the same posture,
|
||||
the plain-queue default set instead of the outbox's 60 s/5/5 s
|
||||
set).
|
||||
- `ScheduleOpts { priority, max_attempts, expires }` ride *as
|
||||
themselves* — the two EnqueueOpts-minus-delay/run-at fields
|
||||
([ADR-009](009-scheduler-collapse.md) §3) apply over the defaults
|
||||
(`max_attempts` overrides the default 3; `expires` resolves per §2);
|
||||
the other three stamps (visibility/backoff/retention) take the
|
||||
engine defaults. One resolution, engine-side, same on both engines
|
||||
by the contract suite's equivalence pin.
|
||||
- **`ScheduleOpts` field growth post-release is class 3** (opts
|
||||
struct, [ADR-017](017-contract-versioning.md) §3's exemption) —
|
||||
stated so the pricing is visible now.
|
||||
|
||||
### 4. The payload encoding: serde_json serialization of the trait `Value`, byte-exactly
|
||||
|
||||
- The trait's `payload` parameter is `serde_json::Value`
|
||||
([ADR-008](008-contract-v1-pinning.md) §2); the engine serializes it
|
||||
with serde_json and stores **exactly those bytes** — the stored
|
||||
bytes of a job row or stream event row are, contract-pinned, the
|
||||
serde_json serialization of the `Value` the publish/enqueue carried.
|
||||
- `Job`/`StreamEvent`'s `payload: Vec<u8>` fields and `payload_as<T>`
|
||||
decode those bytes; the error is `Codec` (pinned). Nothing re-encodes
|
||||
on the way out.
|
||||
- This makes the **cross-engine byte-equality rows well-defined**: a
|
||||
`publish` with `Value` V on engine A and engine B stores byte-identical
|
||||
rows (same serde_json canonical output for the same `Value`), which
|
||||
is what the stream-equivalence backlog row's byte comparison means.
|
||||
- Not pinned to a serde_json *feature flavor* (arbitrary-precision
|
||||
big ints, map key order etc.) beyond serde_json's default shipping
|
||||
behavior — the suite pins round-trip (publish → read → `payload_as`
|
||||
→ `Value` equality), not storage-format trivia; engines must use
|
||||
serde_json (the family-standard JSON codec both POCs used), and a
|
||||
codec change would itself be a contract change under
|
||||
[ADR-017](017-contract-versioning.md) class 3 (it changes what
|
||||
`payload_as` can decode).
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**
|
||||
|
||||
- The last four improvisation surfaces close with zero new machinery:
|
||||
two are honker's existing behavior promoted to contract text, one
|
||||
reuses the outbox's already-decided derived-defaults posture, one
|
||||
makes the existing round-trip assumptions explicit.
|
||||
- `get_job`'s resolved-fields honesty (§1/§2) makes job behavior
|
||||
inspectable without ambiguity about which form was enqueued.
|
||||
- The equivalence suite's byte-equality rows get a stated meaning.
|
||||
|
||||
**Negative**
|
||||
|
||||
- `delay`-wins-over-`run_at` is honker's precedence, carried as-is:
|
||||
a consumer supplying both gets the delay form's meaning — stated
|
||||
contract text, but a surprise if unread.
|
||||
- Relative-only `expires` puts the absolute arithmetic on the
|
||||
consumer needing it — acceptable at v1 field-set closure (no row
|
||||
names the second form).
|
||||
- serde_json's shipping behavior (map order, number handling) is
|
||||
implicitly in the stored row — pinned to default serde_json, testable
|
||||
by round-trip, and only visible via `payload_as` anyway.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR-008](008-contract-v1-pinning.md) §1 (field sets), §2 (the
|
||||
`Value` payload posture §4 encodes), §5 (§1's no-new-variant
|
||||
reasoning).
|
||||
- [ADR-009](009-scheduler-collapse.md) §3/§5 — the fire-enqueue shape
|
||||
§3 stamps; the names-not-objects posture that makes §3's source the
|
||||
engine defaults.
|
||||
- [ADR-010](010-queue-semantics-depth.md) §1 (claim ordering's
|
||||
`run_at` key), §3a (stamping — §3 completes its source rule), §8.
|
||||
- [ADR-014](014-outbox-tx-enqueue.md) §1 — the derived-defaults
|
||||
posture §3 reuses.
|
||||
- [ADR-015](015-streams-depth.md) §3 — the `Vec<u8>` payload field §4
|
||||
bridges.
|
||||
- [ADR-017](017-contract-versioning.md) §2 class 1/§2 class 3/§3 —
|
||||
the amendment frame; the opts-field pricing §3 states.
|
||||
- honker-core `enqueue` (`/workspace/honker/honker-core/src/honker_ops.rs`
|
||||
@ f4e53c6, precedence/expiration doc comment + implementation) — the
|
||||
§1/§2 behavior's source of record.
|
||||
- [core-contract.md](../core-contract.md) — the spec this ADR's
|
||||
§1–§4 pin into place.
|
||||
@@ -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)
|
||||
---
|
||||
|
||||
# Deployment
|
||||
|
||||
@@ -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)
|
||||
---
|
||||
|
||||
# Postgres engine
|
||||
@@ -112,6 +112,9 @@ the record in [ADR-003](decisions/003-sqlite-driver.md).
|
||||
| [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue | `outbox_enqueue_tx` on `TxHandle`; derivation engine-side inside the caller's tx |
|
||||
| [015](decisions/015-streams-depth.md) | Streams depth | nullable key column (carried metadata); bigserial offsets, global-FIFO reads; keyed tx publish; `trim_to` as a pool-connection delete |
|
||||
| [016](decisions/016-deployment-honesty.md) | Deployment honesty | no runtime capability surface — this engine's multi-host posture is stated by its crate identity and docs; `PayloadTooLarge` occurrence pinned contract-suite (this engine produces it, client-side pre-round-trip) |
|
||||
| [017](decisions/017-contract-versioning.md) | Contract versioning | engine pins core `alkstore = "1.y"` in its manifest; changes touching contract semantics classified under its four-class taxonomy |
|
||||
| [019](decisions/019-mechanism-handle-surfaces.md) | Handle surfaces | boxed handle traits (`Queue`/`StreamHandle`/`Outbox`/`Lock`/`JobHandle`) implemented over the pooled/claimed rows; `worker_id` stamps the claimant column |
|
||||
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue options | delay-over-`run_at` resolution in the engine's enqueue SQL; scheduler fires stamp from derived defaults; serde_json byte encoding (the equivalence suite's byte rows) |
|
||||
|
||||
## Open Questions
|
||||
|
||||
@@ -137,6 +140,8 @@ Resolved: **OQ-09** (scheduler collapse —
|
||||
[ADR-009](decisions/009-scheduler-collapse.md)), **OQ-05** (queue
|
||||
semantics depth — [ADR-010](decisions/010-queue-semantics-depth.md)),
|
||||
**OQ-12** (streams depth — [ADR-015](decisions/015-streams-depth.md)),
|
||||
and **OQ-08** (capability surface — none;
|
||||
[ADR-016](decisions/016-deployment-honesty.md)),
|
||||
**OQ-08** (capability surface — none;
|
||||
[ADR-016](decisions/016-deployment-honesty.md)), and **OQ-10**
|
||||
(contract versioning —
|
||||
[ADR-017](decisions/017-contract-versioning.md)),
|
||||
2026-10-05/06.
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-06 (OQ-11 resolved — ADR-018)
|
||||
last_updated: 2026-10-06 (ADR-019/020 — handle surfaces, enqueue semantics)
|
||||
---
|
||||
|
||||
# SQLite engine
|
||||
@@ -133,6 +133,8 @@ family is `__alkstore_*` (ADR-010 §8's naming authorization).
|
||||
| [016](decisions/016-deployment-honesty.md) | Deployment honesty | no runtime capability surface; single-host posture stated by crate identity + docs; this engine never produces `PayloadTooLarge` (pinned in the contract suite) |
|
||||
| [017](decisions/017-contract-versioning.md) | Contract versioning | engine pins core `alkstore = "1.y"` in its manifest; substrate cherry-picks are class-4 non-events; adoption duties per its §5 |
|
||||
| [018](decisions/018-provenance-register-and-cherry-picks.md) | Substrate provenance | `src/substrate/PROVENANCE.md` (identity, delta register — cherry-picks as tagged entries); cherry-picks recorded at adoption, non-versioning |
|
||||
| [019](decisions/019-mechanism-handle-surfaces.md) | Handle surfaces | boxed handle traits (`Queue`/`StreamHandle`/`Outbox`/`Lock`/`JobHandle`); `worker_id` stamps the row's claimant column; `StopToken` core-owned |
|
||||
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue options | delay-over-`run_at` in the substrate's re-derived enqueue; scheduler fires stamp from derived defaults; serde_json byte encoding |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
@@ -42,6 +42,18 @@ entries;
|
||||
five-step cherry-pick procedure recorded at adoption). **The Phase 1
|
||||
question set is closed** — no open OQs remain; Phase 1 moves to
|
||||
architecture review and the implementation-phase gates.
|
||||
**Second review round (2026-10-06, pre-decomposition):** the review
|
||||
found no *new* open questions — every finding was decidable from
|
||||
decided material, so it resolved directly as
|
||||
[ADR-019](decisions/019-mechanism-handle-surfaces.md) (handle-trait
|
||||
surfaces, `Job`/`Schedule` shapes, claimant identity, `StopToken` —
|
||||
closing the "pin at implementation" residue before ADR-017's class-1
|
||||
window ends at first release) and
|
||||
[ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
|
||||
(`delay`/`run_at` precedence, relative `expires`, scheduler-fired
|
||||
jobs' stamp source, the serde_json payload encoding bridge), plus
|
||||
mechanical fixes (ADR index staleness in overview/engine-postgres,
|
||||
stale wording, framing drift).
|
||||
|
||||
Resolved questions stay listed with their resolution; they are not
|
||||
deleted.
|
||||
@@ -137,7 +149,7 @@ narrowed to the pinning work its own record already scoped.)*
|
||||
surface; the contract is the trait it returns. (7) Locks guarantee
|
||||
row added to ADR-006's table (TTL-bounded mutual exclusion, silent
|
||||
expiry); the scheduler row is explicitly transferred to OQ-09's
|
||||
resolution. A verification backlog (core-contract.md §Verification
|
||||
resolution. A verification backlog (core-contract.md §Verification
|
||||
backlog) tracks the one-engine-pinned properties.
|
||||
- **Cross-references**: OQ-01, OQ-10 (resolved —
|
||||
[ADR-017](decisions/017-contract-versioning.md), the discipline
|
||||
|
||||
@@ -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)
|
||||
---
|
||||
|
||||
# alkstore — Overview
|
||||
@@ -80,6 +80,10 @@ Per [ADR-002](decisions/002-feature-scope.md):
|
||||
| [014](decisions/014-outbox-tx-enqueue.md) | Transactional outbox enqueue (`outbox_enqueue_tx` on `TxHandle`) | Accepted |
|
||||
| [015](decisions/015-streams-depth.md) | Streams depth (carried-metadata keys, global-FIFO ordering, `StreamEvent`, `trim_to`) | Accepted |
|
||||
| [016](decisions/016-deployment-honesty.md) | Deployment honesty (no runtime capability surface; compile-time identity + matrix) | Accepted |
|
||||
| [017](decisions/017-contract-versioning.md) | Contract versioning (core crate's semver is the contract version; change classes, pairing carriers, lockstep duties) | Accepted |
|
||||
| [018](decisions/018-provenance-register-and-cherry-picks.md) | Substrate provenance register and cherry-pick procedure (`PROVENANCE.md` in-tree, per-delta entries) | Accepted |
|
||||
| [019](decisions/019-mechanism-handle-surfaces.md) | Mechanism-handle surfaces (handle traits, `Job`/`Schedule` shapes, `worker_id`, `StopToken`) | Accepted |
|
||||
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue-option semantics (delay/run_at, expires, scheduler stamp source, payload encoding) | Accepted |
|
||||
|
||||
## Non-goals
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
status: draft
|
||||
last_updated: 2026-10-05
|
||||
last_updated: 2026-10-06 (ADR-019/020 — handle surfaces, enqueue semantics)
|
||||
---
|
||||
|
||||
# Queues, scheduler, outbox — semantics depth
|
||||
@@ -39,7 +39,11 @@ made under; the ADRs carry the WHY.
|
||||
- **Job options** (the contract v1 skeleton,
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §1, from the
|
||||
honker-rs surface): `delay, priority, max_attempts, expires, run_at`
|
||||
at enqueue; `ack / retry / fail / heartbeat` on the job handle.
|
||||
at enqueue (`delay`-wins-over-`run_at` resolution and
|
||||
relative-`expires` pinned by
|
||||
[ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md) §1/§2);
|
||||
`ack / retry / fail / heartbeat` on the boxed `JobHandle` a claim
|
||||
returns ([ADR-019](decisions/019-mechanism-handle-surfaces.md)).
|
||||
- **Cut-flag context**: result storage is
|
||||
[ADR-002](decisions/002-feature-scope.md)'s cut-flag row;
|
||||
[ADR-010](decisions/010-queue-semantics-depth.md) §7 reconsidered it
|
||||
@@ -139,7 +143,8 @@ made under; the ADRs carry the WHY.
|
||||
opt) makes `sweep_expired` enforce a per-queue dead TTL. Honker's
|
||||
no-retention posture kept as default; the mechanism is ours.
|
||||
- **No redrive API in v1**: the recipe is `get_job` + fresh `enqueue`.
|
||||
No consumer names an API shape; re-entry needs an OQ.
|
||||
No consumer names an API shape; re-entry gates on a
|
||||
consumer-inventory row first (the ADR-002 row-first discipline).
|
||||
|
||||
## Sweep / maintenance (ADR-010 §5–§6)
|
||||
|
||||
@@ -187,6 +192,8 @@ Collapsed into queues: no `Scheduler` handle, no schedule objects.
|
||||
(the no-ambient-timers posture). The runner: leadership lock →
|
||||
{renew (on loss, return before ticking — `Err(LeadershipLost)`,
|
||||
distinct from clean `Ok(())` stop) → fire due boundaries → sleep}.
|
||||
The `stop` parameter is a core-crate cloneable `StopToken`
|
||||
([ADR-019](decisions/019-mechanism-handle-surfaces.md) §4).
|
||||
Leadership lock: reserved name `__alkstore_scheduler`
|
||||
(engine-derived name under the reserved prefix,
|
||||
[ADR-008](decisions/008-contract-v1-pinning.md) §4).
|
||||
@@ -201,7 +208,12 @@ Collapsed into queues: no `Scheduler` handle, no schedule objects.
|
||||
- Each boundary fire enqueues ordinary work: `payload` into the named
|
||||
queue with `ScheduleOpts { priority, max_attempts, expires }` —
|
||||
stamped onto the enqueued job per
|
||||
[ADR-010](decisions/010-queue-semantics-depth.md) §3a; the queue
|
||||
[ADR-010](decisions/010-queue-semantics-depth.md) §3a; the three
|
||||
`ScheduleOpts` fields apply over the target queue's **derived engine
|
||||
defaults** for the stamps the fire doesn't carry (no queue handle is
|
||||
open — the same derived-defaults posture as the outbox's backing
|
||||
queue, [ADR-020](decisions/020-enqueue-opt-semantics-and-bridges.md)
|
||||
§3); the queue
|
||||
mechanism's guarantees apply, no separate delivery machinery.
|
||||
- Schedule storage is engine-internal (SQLite: the forked substrate's
|
||||
`__alkstore_scheduler_tasks` table; Postgres: the engine-owned
|
||||
@@ -270,6 +282,8 @@ Collapsed into queues: no `Scheduler` handle, no schedule objects.
|
||||
| [011](decisions/011-sqlite-substrate-fork.md) | Substrate fork | SQLite-side queue ops re-derived in owned code; `__alkstore_*` naming |
|
||||
| [012](decisions/012-forked-substrate-design.md) | Fork design | contract-blind substrate boundary; fidelity posture; port deltas |
|
||||
| [014](decisions/014-outbox-tx-enqueue.md) | Outbox tx enqueue | `outbox_enqueue_tx` on `TxHandle`; derived backing queue reached only through the outbox surface |
|
||||
| [019](decisions/019-mechanism-handle-surfaces.md) | Handle surfaces | queue ops on the `Queue` handle, `JobHandle` from claims, `worker_id` identity, `Job`/`Schedule` shapes, core `StopToken` |
|
||||
| [020](decisions/020-enqueue-opt-semantics-and-bridges.md) | Enqueue options | delay-over-run_at; relative `expires`; scheduler fires stamp from derived queue defaults |
|
||||
|
||||
## Open Questions
|
||||
|
||||
|
||||
Reference in new issue
Block a user