147 lines
7.1 KiB
Markdown
147 lines
7.1 KiB
Markdown
---
|
||
id: pg-decide-stamp-arithmetic-side
|
||
name: Decide which side guards pg's server-side stamp arithmetic (review 004 item 5, decision)
|
||
status: completed
|
||
depends_on: []
|
||
scope: narrow
|
||
risk: low
|
||
impact: component
|
||
level: implementation
|
||
tags: [review-004-fixes, postgres-engine, decision, contract]
|
||
---
|
||
|
||
## 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
|
||
|
||
- [x] The decision of record: which side owns the guard, with the
|
||
ADR-023 §2 totality argument and the sibling-engine uniformity
|
||
check written down
|
||
- [x] The window state verified and recorded; a dated annotation
|
||
landed if class-1, or user sign-off if post-publish
|
||
- [x] The guard form is precise (clamp domain, sites, the
|
||
extreme-stamped row's observable semantics) — enough for
|
||
`pg-fix-bigint-stamp-overflow` to implement without re-deciding
|
||
- [x] 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. |