Skip to content

fix: separate raw retention boundary from oldest held sample (#212) - #219

Draft
NikolayS wants to merge 1 commit into
mainfrom
release/fix-retention-boundary-212
Draft

fix: separate raw retention boundary from oldest held sample (#212)#219
NikolayS wants to merge 1 commit into
mainfrom
release/fix-retention-boundary-212

Conversation

@NikolayS

@NikolayS NikolayS commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Fixes #212 — the README's flagship drill hard-errors on any fresh install, and
ash.status().raw_retention_start is unusable as the planning boundary it is documented to be.

Found by the pre-tag audit (#160). Draft on purpose: needs REV and your explicit approval.

The bug

ash._raw_retention_start() returns min(sample_ts) — the oldest sample currently held — but
_raise_tie_retention() treats it as a retention boundary, and status() publishes it under a
name documented for window planning. On a young install with the data 100% present:

-- README.md:193, verbatim
ERROR: this drill needs raw samples; raw retention starts at 22:34:51 but the
       requested window starts at 22:30:01. Narrow the window to start at or after 22:35:00

Feed it status().raw_retention_start and it still raises (the reader floors since to the
minute before the guard runs). Follow the error's own printed advice and you get 0 rows — the
whole partial first minute discarded. With ash.start()'s 1s default this fires 59/60 of the time.

The fix

Splits the two conflated concepts rather than fudging one:

  • ash._raw_retention_boundary() (new) — the configured ring boundary,
    rotated_at - (num_partitions - 2) * rotation_period, floored to the minute, honouring an older
    sample if one is still physically retained. Used by status() and every tie guard.
  • ash._raw_retention_start() (unchanged) — still the exact oldest held sample. Still used by
    source selection and report attribution, where "what data exists" is the right question.
  • Both sides of the tie comparison are floored consistently, so a window starting exactly at the
    boundary is accepted.
  • The remediation hint in the error is rounded down, so following it returns data.

Verification

RED against unpatched SQL — the real README:193 failure, reproduced:

ERROR: README:193 raised on a young install: pg_ash: this drill needs raw samples...

GREEN, with exact-value assertions rather than "returns rows":

  • README:193 → 1 / 212212 / raw, metrics (0.03, 0.13, 0.13, 8.00, 100.00)
  • passing status().raw_retention_start straight back in → same eight backend-seconds
  • following the printed remediation verbatim → same non-empty result
  • genuine age-out still raises: a row is inserted into slot 2, rotate() truncates it, its
    count is asserted to be exactly zero, and the old window raises SQLSTATE P0001 with the exact
    unrecoverable message. That is the control proving the guard was fixed, not disabled.

Fresh install, double re-apply, full 1.0→2.0 chain, and fresh-vs-chain schema/ACL equivalence all
pass on PG 18.3.

⚠️ Merge order and a required edit to #129

#129 must merge first, then this. Composition was tested against #129 head 2006159, not
assumed.

The code cherry-picks with no textual conflict — but one of #129's own test assertions
becomes wrong
once this lands. #129 asserts that status().raw_retention_start equals the exact
oldest retained sample; this PR redefines that field as the configured ring boundary. Whoever
merges must update that assertion to compute the boundary from rotated_at, num_partitions and
rotation_period. Verified composed and green after exactly that change:

NOTICE: raw retention excludes the oldest ring slot PASSED
NOTICE: issue #212 raw retention boundary tests PASSED
NOTICE: retention error split PASSED (boundary hint is followable, echoes passed start)

This branch deliberately remains one clean commit on 37a0e2a; the composition was validated on a
scratch branch and not included here.

Review note

raw_retention_start now means two different things on two user-facing surfaces:
status() reports the planning boundary, while report().coverage.raw_retention_start keeps
the exact oldest sample (it describes attribution availability, not planning). Each choice is
defensible on its own; sharing the name is the wart. Worth renaming one before this ships —
report()'s payload contract is frozen per 2.0 minor line, so it is easier to change now than later.

Related but distinct: #217 introduces a geometry constraint using (num_partitions - 1). That is
not an inconsistency with the - 2 used here — -1 bounds the physical age of rows in the slot
about to be truncated, -2 is the user-visible retention window. Both PRs would benefit from a
comment saying so.

🤖 Generated with Claude Code

https://claude.ai/code/session_014HzBGzjFyN8dXZHbdWmYBj

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Release blocker: README's flagship drill hard-errors on any fresh install; raw_retention_start is unusable as a planning boundary

1 participant