You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(version): refuse a snapshot read below the retained history (#617)
## Summary
`SuperVersions::get_version_for_snapshot` panicked with `should always
find a SuperVersion` when a read asked for a snapshot at or below the
oldest retained version's seqno. Compaction maintenance (with the
caller's GC watermark) and `clear` prune the version history past such
snapshots, so every read API (`get`, `multi_get`, range / prefix
iterators, seekable and batch scans, columnar scan, range estimates)
could take the tree down on a valid argument.
The read is now refused with a typed error instead of served from a
newer version (which would silently return data the snapshot never saw),
and the boundary is durable:
- `Error::SnapshotBelowRetention { requested, oldest_retained }`: point
reads return it directly; iterators yield it as their first and only
item (also via `peek_key` on the seekable iterator, whose seeks become
no-ops). Standard and KV-separated trees alike, including an empty
`multi_get` batch.
- `AbstractTree::oldest_retained_seqno()`: the read boundary, so a
caller can validate a long-lived snapshot before reading. A snapshot is
servable iff it is `0` or strictly above that seqno.
- Snapshot `0` stays served from the oldest retained version: nothing is
visible at `0` from any version, so the choice is immaterial and probing
an empty tree keeps working after pruning.
- **The boundary survives a reopen.** `Version::retention_floor`
(persisted as a manifest section plus an appended edit-log field, both
optional so older manifests recover as `0`) records the highest snapshot
an install made unservable: a GC compaction with watermark `w` sets it
to `w - 1`, capped at the compaction's own install seqno (so a
`SeqNo::MAX` watermark cannot refuse every later snapshot); a `clear` /
`drop_range` / FIFO eviction / a compaction whose user filter removed or
rewrote rows sets it to its own install seqno; additive installs (flush,
ingest, trivial move) and an empty drop leave it alone. The install
passes a `RetentionEffect` to `upgrade_version`, so the floor rides in
the same version edit as the data loss it records. A reopened history is
seeded at the floor, version seqnos are clamped non-decreasing, and the
front is checked explicitly, so a counter reset below the floor cannot
slip a version under it.
- `AbstractTree::retention_floor()`: the persisted boundary (what a
reopen enforces), distinct from the live `oldest_retained_seqno()`, and
the value a deployment records for a later repair.
- `Config::repair_retention_floor` (default `0`): a rebuilt manifest
cannot derive the floor (a GC compaction zeroes the settled rows'
seqnos) and must not guess it (the external-WAL reconciliation reads
intermediate snapshots back after a repair), so the deployment supplies
the `retention_floor()` it last recorded.
- `Tree::create_{iter,range,prefix,seekable_range_bounds}` (doc-hidden)
return `Result` and raise the error before any I/O; the happy path gains
no per-item branch, and the lock-free `snapshot_for_read` fast path is
unchanged.
- Docs: `INVARIANTS.md` (retention boundary, its durability, and the
storage cost of a retention window), `manifest-recovery.md` (retention
floor after a repair), `external-wal.md` (repair floor), rustdoc on the
new API.
## Testing
- `tests/snapshot_below_retention.rs` (31 tests): every read surface on
both tree types, the `clear` path, snapshot `0`, the exact boundary
(`oldest` fails, `oldest + 1` succeeds), the unpruned case; the boundary
after a reopen following a GC compaction, `clear`, `drop_range`, FIFO
eviction, a leveled merge and a filtering compaction at watermark 0, a
`SeqNo::MAX` watermark capped at the install, an empty `drop_range`
leaving it alone, additive installs leaving it at `0`, manifest rotation
(floor read from the snapshot section), a reopen chain with a rising
floor, a reset counter, the persisted-vs-live boundary, checkpoints, and
repair with and without the configured floor.
- Unit tests: edit codec round-trips (floor with / without blob
frontiers, pre-floor payloads), `diff` emits the floor only when
changed, recovery applies it, the history seeds at the floor and checks
the front before searching.
- Gates: `cargo fmt --check`, `cargo clippy --workspace --all-targets`
(default and `--all-features`) with `-D warnings`, `cargo doc --no-deps`
(default and `--all-features`) 0 warnings, no-std check 0 errors, `cargo
nextest run --workspace --all-features` 3288/3288, `cargo test --doc
--all-features` 80/80.
Closes#616
0 commit comments