Skip to content

Release frozen transaction-log memory maps once JS releases them#653

Open
kriszyp wants to merge 3 commits into
kris/txn-log-madv-coldfrom
kris/txn-log-mmap-weakref
Open

Release frozen transaction-log memory maps once JS releases them#653
kriszyp wants to merge 3 commits into
kris/txn-log-madv-coldfrom
kris/txn-log-mmap-weakref

Conversation

@kriszyp

@kriszyp kriszyp commented Jun 12, 2026

Copy link
Copy Markdown
Member

Stacked on #652 (base branch kris/txn-log-madv-cold). Review #652 first; the base will retarget to main automatically when #652 merges. The diff shown here is PR #2's changes only.

Summary

Reworks transaction-log memory-map ownership so a frozen (rotated-out) log file no longer pins its mmap for the life of the TransactionLogFile:

  • The current (actively-written) file keeps a strong memoryMap (the writer extends its overlay; the timestamp index reads through it).
  • A frozen file keeps only a weak frozenMapCache; the strong reference is handed to the caller and thus to the JS external buffer, so the mapping is munmap'd when JS GCs the buffer.
  • On rotation, the file being rotated away from is downgraded (strong → weak) via rotateToNextSequence(), so a reader that mapped it while it was current no longer pins it.
  • store/handle getMemoryMap now return a strong shared_ptr (the buffer adopts ownership); GetMemoryMapOfFile uses it directly.

Purpose

Follow-up to #650/#652. PR #652 (MADV_COLD) makes read txn-log pages reclaimable under pressure; this PR lets them be freed outright when the reader is done — during replication catch-up the joining/source node reads the full log, and previously every read frozen map stayed resident until the file was closed/removed.

It also resolves #652's deferred concurrency finding: getMemoryMap now (re)assigns memoryMap under fileMutex, matching adviseCold/close/removeFile, so the shared_ptr read/write race is closed.

Where to look

  • Lock ordering (transaction_log_file_posix.cpp getMemoryMap): now holds fileMutex for its whole body. Order is dataSetsMutex/indexMutex → fileMutex, consistent with the existing store.getMemoryMap → open() path; no fileMutex holder takes the outer locks. Worth a second pair of eyes.
  • Windows getMemoryMap deliberately does not take fileMutex — Windows open() → openFile() → findPositionByTimestamp() → getMemoryMap() already holds it, so locking there would self-deadlock (POSIX openFile doesn't index). Commented inline; the pre-existing Windows reassignment race is left as-is (not the memory-pressure target).
  • Rotation downgrade (transaction_log_store.cpp rotateToNextSequence): applied at all 5 writeBatch rotation sites; the purge advance is excluded because it removes the file (map reset on removal).

Cross-model review

Codex (correctness/concurrency) + a Harper-domain pass. Both findings from Codex are addressed in this PR: the shared_ptr race (the fileMutex serialization above) and a test-baseline-stability flake (the finalization test now flushes earlier finalizers before sampling). The rotation-downgrade hole Codex flagged is fixed here (it was the originally-deferred tradeoff). The domain pass found no blockers.

Caveat: the Gemini/agy review leg hung and produced no output (same as #652) — no Gemini coverage this round; Codex + the domain pass are the cross-model coverage.

Tests: native GoogleTest ownership cases (frozen released on drop, current retained then freed on file destroy, downgrade releases on drop, frozen handout dedup) + Vitest finalization tests (a frozen buffer and a mapped-while-current-then-rotated buffer are both unmapped after release, asserted via transactionLogMapCount()). Full suite green (485 passed; 37 native).

Generated by an LLM (Claude Opus 4.7); reviewed/curated by Kris.

A TransactionLogFile held a strong reference to its memory map for the
object's whole life, so a log file's mmap stayed resident until the file
was closed, removed, or remapped — even after every JS reference to its
external buffer was gone. During replication catch-up this retains the
full set of read log maps.

Hold the strong reference only for the current (actively-written) file —
the writer extends its overlay and the timestamp index reads through it.
A frozen (rotated-out) file keeps only a weak handle, so the JS external
buffer becomes the sole owner and the mapping is unmapped when JS GCs the
buffer. On rotation, the file being rotated away from is downgraded to the
weak handle so a reader that mapped it while it was current no longer pins
it. store/handle getMemoryMap now return a strong shared_ptr (the buffer
adopts ownership). Windows keeps the strong-retain behavior (not the
memory-pressure target; its open path would self-deadlock under fileMutex).

getMemoryMap now (re)assigns the map under fileMutex, which also closes the
memoryMap shared_ptr data race deferred from the MADV_COLD change (#652):
adviseCold/close/removeFile already hold fileMutex, and the lock order
(dataSetsMutex/indexMutex -> fileMutex) is consistent and deadlock-free.

A process-global MemoryMap::liveCount (exposed as transactionLogMapCount())
lets tests assert that releasing a frozen log's buffer actually unmaps it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
gemini-code-assist[bot]

This comment was marked as resolved.

kriszyp and others added 2 commits June 12, 2026 10:20
The "unmaps a frozen log when all JS references are released" test asserts
the weak-for-frozen behavior, which is POSIX-only — Windows getMemoryMap
retains the map strongly by design, so a directly-mapped frozen log is not
released until the file is closed/removed. The sibling rotation test still
runs everywhere (downgradeMapToFrozen is platform-agnostic).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
It is written on the write path (under writeMutex) but read on the read
path (getMemoryMap/findPositionByTimestamp under dataSetsMutex) — different
locks, so the plain uint32_t accesses were a data race (flagged in review).
Make it std::atomic<uint32_t> with relaxed ordering: readers only need a
coherent value, and the existing mutexes already provide ordering against
other state. Pre-existing; surfaced by the rotateToNextSequence refactor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@kriszyp kriszyp marked this pull request as ready for review June 12, 2026 16:41
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.

1 participant