Files
alkstore/tasks/pg-decide-stamp-arithmetic-side.md
T

147 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.