Keep transaction-log retention on a durable sequence floor - #799
Keep transaction-log retention on a durable sequence floor#799kriszyp wants to merge 13 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the transaction log store to ensure that only closed stores can lose their directory or flush watermark, preventing issues where active stores are deleted during ordinary retention purges. Key changes include updating close logic, rewriting timestamp-based position lookups, writing the flush state via a local stream, and handling watermark resumption on startup. Additionally, the transaction log reader is updated to skip missing segments. Feedback on the reader implementation highlights two potential bugs where empty log segments (containing only the header) could cause the iterator or reader to prematurely terminate; using while loops instead of if conditions is recommended to continuously advance past empty segments.
📊 Benchmark Resultsget-sync.bench.tsgetSync() > random keys - small key size (100 records)
getSync() > sequential keys - small key size (100 records)
ranges.bench.tsgetRange() > small range (100 records, 50 range)
realistic-load.bench.tsRealistic write load with workers > write variable records with transaction log
transaction-log.bench.tsTransaction log > read 100 iterators while write log with 100 byte records
Transaction log > read one entry from random position from log with 1000 100 byte records
worker-put-sync.bench.tsputSync() > random keys - small key size (100 records, 10 workers)
worker-transaction-log.bench.tsTransaction log with workers > write log with 100 byte records
Results from commit 6deb387 |
Directory iteration order is unspecified, so registerLogFile() promoted any segment that briefly held the highest sequence: it marker-enabled and opened that file, and nothing ever demoted it. Every superseded segment kept an append-boundary marker it will never use and an open handle for the life of the store. On Windows the handle is opened without FILE_SHARE_DELETE, so it also made those segments undeletable from outside the process — which is what failed the new already-missing-prefix purge test on all five Windows runtimes (EBUSY on unlink). Discovery now registers without opening; load() marker-enables and opens the file that is still current once the whole directory has been scanned. A current segment that cannot be opened degrades as before (append starts at a fresh sequence) rather than failing the load, while an append-boundary violation stays fatal. Co-Authored-By: Claude Opus <noreply@anthropic.com>
The marker assertion alone can pass against the old code when directory enumeration yields the highest sequence first. Add an open-descriptor check via procfs (Linux) so the assertion does not depend on iteration order, and widen the fixture to six segments. Also drops two comments that narrated PR history rather than the invariant, and corrects the AGENTS.md wording: only the active segment may have a marker created, but a retired segment keeps the one it already earned. Co-Authored-By: Claude Opus <noreply@anthropic.com>
Ordinary and startup transaction-log retention now delete only an eligible contiguous oldest prefix below the persisted flush floor. The sequence file named by
txn.stateand every newer file remain, sotxn.state, its cached file handle, the writer, and the existing contiguous reader stay in the same sequence space.This deliberately does not change transaction-log reader behavior or add JavaScript/native cache-generation APIs.
destroy: trueremains the existing separate destructive path; Harper does not call it in production.An idle store may retain one segment past the age cutoff until a later write rotates and flushes it. Disk retained past the cutoff is bounded by
transactionLogMaxSize, except when a single transaction exceeds that target.Windows CI on this branch failed the new
should detach an already-missing prefix entry without wedging retentiontest withEBUSYon all five runtimes, because the segment the test unlinks was still open. The cause is startup discovery, not retention:registerLogFile()promoted whatever segment currently held the highest sequence — opening it and enabling its append-boundary marker — and nothing demoted it when a higher one turned up. Directory iteration order is unspecified, so on a sorted-enumeration filesystem every superseded segment kept an unused marker and an open handle for the life of the store; on Windows that handle is taken withoutFILE_SHARE_DELETE, so those segments were also undeletable from outside the process. Discovery now registers without opening, andload()marker-enables and opens the one file that is still current after the whole directory has been scanned. An unopenable current segment degrades as before — appends resume at a fresh sequence — while an append-boundary violation stays fatal.Refs HarperFast/harper#846 — Transaction-log retention has no config-driven time/size control (and the storageReclamation hardening isn't on v5.1).
For the human reviewer
directory_iteratorhappens to yield the highest sequence first, the old code produced the same state. On a sorted-enumeration filesystem — including the NTFS runners where this failed — it is deterministic. Both reviewers flagged this; the limitation is documented at the test.purgeableFilesstill counts files thatdoPurge's new contiguous-prefix rule will not delete; a non-ENOENT stat failurebreaks retention permanently with only aDEBUG_LOG; the flushed-position extent guard is now unreachable on the!allpath; and startup retention still deletes files during a read-only open. Worth a decision before this merges.Verification
origin/main: an aged current segment is deleted.git diff --checkpassed.1.txnlog.boundarypresent) and passes after.pnpm checkclean.Complexity: moderate
Review-Coverage: authored=claude; ran=codex,gemini; declined=cursor-grok,cursor-composer,domain; rounds=2 @ c955d31
Human-Review-Need: 3 @ c955d31