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
@@ -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.
|
||||
Reference in new issue
Block a user