Skip to content

kv: add optional Snapshotter for consistent point-in-time reads (pebblekv) - #108

Merged
oc-engteam merged 4 commits into
mainfrom
feat/kv-snapshot
Jul 7, 2026
Merged

kv: add optional Snapshotter for consistent point-in-time reads (pebblekv)#108
oc-engteam merged 4 commits into
mainfrom
feat/kv-snapshot

Conversation

@oc-engteam

Copy link
Copy Markdown
Collaborator

What

Adds an optional consistent point-in-time read primitive at the kv layer:

  • core/kv/kv.go — two NEW interfaces, kv.Store UNCHANGED:
    • Snapshotter { Snapshot() (Snapshot, error) } — an optional capability a backend implements alongside Store; callers type-assert (sn, ok := store.(kv.Snapshotter)).
    • Snapshot { Get; Scan; ScanRange; Close } — a read-only view frozen at the moment it was taken.
  • core/kv/pebblekv — implements it via pebble.DB.NewSnapshot(); extracts the shared read helpers (getCopy/scanPrefix/scanRange) so PebbleDB.{Get,Scan,ScanRange} and the snapshot share one source of truth for the subtle 0xff prefix bound.

Why

Today every Get/Scan reads at "now", so a multi-read operation has no point-in-time consistency. The motivating case is search: one query fans out into many independent index scans + docid→document reads over a single shared pebble store, and concurrent writes (indexing, deletes, the keyword merger + compaction) can tear that view. pebble.DB.NewSnapshot() fixes it, and because index + docs share one *pebble.DB, a single snapshot can cover an entire query.

Scope (intentionally narrow)

This PR only exposes the primitive. It does not wire the snapshot into search/engine/invertedindex/documents — that is a separate later change. No production caller adopts Snapshotter yet.

Compatibility

Non-breaking: kv.Store is untouched, so all existing implementers (pebblekv + the in-repo test fakes + any external backend) keep satisfying it. Snapshotter/Snapshot are purely additive; consumers opt in via type assertion. No on-disk format change, no StorageVersion bump.

Design notes (verified against pebble v1.1.5)

  • Snapshot() guards IsClosed() (pebble's NewSnapshot() panics on a closed DB) and returns a literal-nil view on error.
  • pebbleSnapshot.Close is idempotent (nils its reader unconditionally on first close) — a raw pebble.Snapshot.Close panics on double-close.
  • Concurrency: Snapshot() is safe with concurrent writes/reads, and one snapshot serves concurrent Get/Scan/ScanRange (each opens its own reader/iterator).
  • Callers must Close a snapshot before closing the Store (else pebble's DB.Close errors "leaked snapshots").

Verification (go1.24.2, CI parity)

  • ./kv/... + -race green.
  • go-cov: zero CRITICAL; Snapshot + the 4 pebbleSnapshot methods at 100%, pebblekv 92.9%.
  • gofmt clean; core builds. Behavior of the refactored Get/Scan/ScanRange guarded by the existing db_test suite.

Built via the SDD flow (spec → 2 review rounds to a clean gate → task breakdown → review → workflow-driven TDD).

🤖 Generated with Claude Code
via Happy

oc-engteam and others added 4 commits July 7, 2026 18:16
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
Add PebbleDB.Snapshot() backed by *pebble.Snapshot, satisfying the
optional kv.Snapshotter capability. The pebbleSnapshot type delegates
Get/Scan/ScanRange to the same shared read helpers as PebbleDB so their
semantics stay byte-identical, guards Snapshot() on a closed DB, and has
an idempotent Close(). NewSnapshot() is added to the pebbleStore seam so
tests can substitute a fake.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tests for the Snapshotter/Snapshot point-in-time read view (SDD tasks T3+T4):
consistency vs concurrent writes, Scan/ScanRange isolation, the 0xff prefix
bound through the snapshot, Get value-copy + not-found, stop-early, a -race
concurrent-readers test (one snapshot, N readers while the DB is written),
Close-before-DB-Close ordering (positive) and the negative case (open snapshot
makes DB.Close error "leaked snapshots"), Close idempotency, Snapshot() on a
closed store returning a literal-nil view, and — via an errSnapReader fake —
the underlying-error propagation of Get/Scan/ScanRange/Close.

Test-only. go1.24.2: ./kv/... + -race green; gofmt clean; go-cov zero CRITICAL
(Snapshot + the 4 pebbleSnapshot methods 100%, pebblekv 92.9%).

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@oc-engteam
oc-engteam merged commit fd92528 into main Jul 7, 2026
8 checks passed
@oc-engteam
oc-engteam deleted the feat/kv-snapshot branch July 7, 2026 13:03
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.

2 participants