7.1 KiB
id, name, status, depends_on, scope, risk, impact, level, tags
| id | name | status | depends_on | scope | risk | impact | level | tags | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| pg-decide-stamp-arithmetic-side | Decide which side guards pg's server-side stamp arithmetic (review 004 item 5, decision) | completed | narrow | low | component | implementation |
|
Description
The decision that must precede the BIGINT-overflow fix, per
docs/reviews/004-pre-release-general-review.md §"Findings recorded,
not fixed" item 5: pg's claim, heartbeat, retry-reschedule, and
retention-sweep statements compute server-side sums over
consumer-supplied i64 values (queue.rs:386/912/1000/714,
lock.rs:91) — one row stamped with visibility_timeout_s = i64::MAX
(or retention i64::MIN) makes every claim/heartbeat UPDATE fail with
"bigint out of range", and the failure is sticky per row. The
client-side resolution is saturated (resolution.rs); the row-stamp
arithmetic is not.
Decide which side owns the guard:
- (a) Clamp/validate at the enqueue seam — consumer i64s bounded
at entry, matching
resolution.rs's client-side saturation posture and the sibling engines' stamp arithmetic (sqlite/mem saturate per review 004 inline fix 6 — the engine-uniformity argument). - (b)
LEAST/GREATEST-guard the server expressions — defense at the arithmetic site; no entry-point change, but the stored stamps are silently clamped server-side.
Contract-adjacent — this is why it's its own task. The choice changes which failures are reachable through the public surface, which touches ADR-023 §2's totality wording. Check the window state first: as of the decomposition (2026-10-11) no publish tag exists, so the class-1 window is presumed open — verify, and if open, land the decision with a dated ADR-023 §2-style annotation (the failure-set change is class-1-legal pre-publish, the review-004 inline fix 5 precedent). If the window has closed, raise with the user before pinning anything.
The decision must name the guard form precisely: the clamp domain, which sites it applies to (enqueue vs the stamp sites), and what the observable behavior of an extreme-stamped row becomes.
Acceptance Criteria
- The decision of record: which side owns the guard, with the ADR-023 §2 totality argument and the sibling-engine uniformity check written down
- The window state verified and recorded; a dated annotation landed if class-1, or user sign-off if post-publish
- The guard form is precise (clamp domain, sites, the
extreme-stamped row's observable semantics) — enough for
pg-fix-bigint-stamp-overflowto implement without re-deciding - The decision recorded where the family keeps such records (ADR annotation / task Notes + contract text if the wording needs it)
References
- docs/reviews/004-pre-release-general-review.md §"Findings recorded, not fixed" item 5 (the finding, the which-side question, the contract-adjacency)
- docs/architecture/decisions/023-fourth-review-round.md §2 (the totality wording the choice touches)
- alkstore-postgres/src/resolution.rs (the client-side saturation posture), queue.rs / lock.rs (the stamp sites)
Notes
The site set is six, not four — the lock arithmetic is two
expressions, and the review's lock.rs:91 line ref was the try_lock
insert only: lock.rs:92 (insert) and lock.rs:217 (renew) both sum
$3::bigint + $4::bigint over the per-call ttl; and the retention
sweep's subtraction is queue.rs:715 (the :714 ref was the
IS NOT NULL line above it). The guard form must cover the renew
arm too.
Per-call vs stamped arguments split the option-(a) story. Four of
the six arguments are per-call (heartbeat(extend), the
retry(err, Some(d)) override, try_lock/renew(ttl) ×2) — there
is no enqueue to clamp at. The other two ride enqueue/stamped
sources (visibility_timeout_s, dead_letter_retention_s —
QueueOpts, stamped verbatim per the review-002 disposition, which
recorded unvalidated stamping as the posture, not a defect). So
option (a) as written decomposes into two different entry rules —
one of which (validating QueueOpts) contradicts the recorded
review-002 posture, and the other (validating per-call boundary
args) contradicts ADR-023 §2's totality wording itself ("no
clamping, no rejection" — heartbeat(i64::MAX) is a value, not a
violation). Option (b) needs neither posture change: it realizes
what the pinned totality already computes.
The pinned arithmetic's form, not its totality, is what was wrong. ADR-023 §2's 2026-10-11 annotation cites "saturating_add / plain SQL integer add" as the three engines' code of record and calls it total — correct for the non-positive values that annotation classified, but pg's plain bigint add was never total in the overflow domain (Postgres errors; SQLite's overflow→REAL approximates; Rust saturates). The decision therefore needs no contract-text change — the table pins the dispositions already; the annotation corrects the form on pg.
Summary
Decision of record: (b), the server expression owns the guard;
clamp domain = the arithmetic result, saturated to the i64 domain;
no input clamped, validated, or rejected at any seam. Guard form:
every server-side stamp expression computes its sum in numeric and
clamps before the bigint cast —
LEAST(GREATEST(<sum>, -9223372036854775808), 9223372036854775807)::bigint
— the SQL realization of the same saturating arithmetic the pinned
code of record runs. Sites of record: pg queue.rs:386/912/1000/715
(claim, heartbeat, retry-reschedule, retention sweep) and
lock.rs:92/217 (try_lock insert, renew); enqueue itself stays
Rust-resolved (resolution.rs's saturating_add — never a SQL-side
sum). Extreme-stamped-row observables (all three engines uniform):
visibility_timeout_s = i64::MAX → claim deadline i64::MAX
(effectively never lapsing); dead_letter_retention_s = i64::MIN →
swept-before i64::MAX (sweeps everything, matching mem's
saturating_sub); negative-extreme extend/delay → a past
deadline voided by the uniform validity predicate on the next op;
ttl = i64::MAX → i64::MAX expiry. "bigint out of range" leaves
pg's reachable surface; the sticky-poison class dies; the duration
row's opaque rejection stays ttl <= 0 only.
Recorded as the dated 2026-10-11 annotation in
docs/architecture/decisions/023-fourth-review-round.md §2
(amending the boundary-row annotation's arithmetic parenthetical in
place — ADR-017 §2 class 1). Window state verified open at
decision time (2026-10-11): no git tags exist, no cargo publish
has occurred, and ADR-017 §2's termination of record pins the class
permanently at the first core-crate publish — so the annotation
landed pre-publish, legal.
The guard form was validated against the live Postgres 16 harness
server before pinning: clamping legs green (overflow both edges
clamps; 1700000000 + i64::MIN rides exact, matching Rust
saturating_add; now − i64::MIN clamps to i64::MAX), so
pg-fix-bigint-stamp-overflow can implement the form verbatim
without re-deciding. No code changed (a decision task); no crate
touched, so no build/test gates re-run — the doc-only diff carries
no rustdoc surface.