Skip to content

Fix object-log write buffer leaks in ObjectAllocator read-only and recovery flushes - #2046

Merged
Ted Hart (TedHartMS) merged 4 commits into
mainfrom
tedhar/readbuff-dispose
Aug 9, 2026
Merged

Ted Hart (TedHartMS) merged 4 commits into
mainfrom
tedhar/readbuff-dispose

Conversation

@TedHartMS

Copy link
Copy Markdown
Contributor

Problem

ObjectAllocator flushes rent pooled object-log write buffers through a
CircularDiskWriteBuffer, which are returned to the pool only via Dispose(). Two flush
paths never disposed theirs, so the buffers leaked:

  • Read-only flush (AsyncFlushPagesForReadOnly) — one buffer set per flush range.
  • Snapshot-region recovery copy (AsyncFlushPagesForRecovery) — one per recovered page.

The read-only path also rented the buffer even for inline-only pages that have nothing to
write to the object log.

Fix

  1. Dispose the shared buffers after the read-only flush loop.
  2. Dispose each recovery page's buffer after its WriteAsync.
  3. Skip renting the buffer for inline-only read-only pages (objectIdMap.Count == 0),
    routing them through WriteInlinePageAsync. Restricted to ReadOnly; Recovery and
    Snapshot can't use this shortcut.
  4. Harden CircularDiskWriteBuffer.Dispose() memory ordering: both paths now dispose while
    writes may be in flight, so add a barrier so Dispose() and FlushToDeviceCallback
    can't race and both skip the return-to-pool.

The Snapshot checkpoint path already disposes via RecoveryInfo.Dispose() and is unchanged.

Testing

  • Tsavorite.test.recordops: 58/58 passed.
  • Tsavorite.test.recovery: 31/31 passed (object recovery, disk-delete, large-object,
    snapshot-eviction).
  • Tsavorite.core + test projects build clean.

…covery flushes

ObjectAllocator flushes rent pooled object-log write buffers via
CircularDiskWriteBuffer, which are returned to the pool only on Dispose(). The
read-only flush and snapshot-region recovery copy paths never disposed theirs,
leaking a buffer set per flush range / per recovered page.

- Dispose the shared buffers after the read-only flush loop.
- Dispose each recovery page's buffer after its WriteAsync.
- Skip renting the buffer for inline-only read-only pages (objectIdMap empty),
  routing them through WriteInlinePageAsync.
- Harden CircularDiskWriteBuffer.Dispose() memory ordering, since both paths now
  dispose while device writes may still be in flight.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: f45128a9-8d17-4a2e-b84d-6da1afd3cc73

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes pooled object-log write-buffer leaks in Tsavorite’s ObjectAllocator flush paths by ensuring CircularDiskWriteBuffer instances are disposed (returning rented buffers to the pool), skipping object-log buffer usage for inline-only read-only pages, and hardening Dispose()/callback memory ordering to avoid a dispose-vs-callback race.

Changes:

  • Add a full memory fence in CircularDiskWriteBuffer.Dispose() to prevent a race where neither Dispose() nor the IO callback returns pooled buffers.
  • Dispose shared flush buffers after issuing all read-only page writes, and dispose per-page flush buffers in recovery flushes.
  • Avoid renting object-log write buffers for read-only pages with objectIdMap.Count == 0, routing them through the inline-only page write path.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
libs/storage/Tsavorite/cs/src/core/Allocator/ObjectSerialization/CircularDiskWriteBuffer.cs Strengthens Dispose() ordering so buffer returns can’t be skipped when disposing with writes in flight.
libs/storage/Tsavorite/cs/src/core/Allocator/ObjectAllocatorImpl.cs Disposes shared flush buffers after read-only flush issuance and skips object-log buffer usage for inline-only read-only pages.
libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorBase.cs Disposes per-page flush buffers in recovery flushes (snapshot-object copy path).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread libs/storage/Tsavorite/cs/src/core/Allocator/ObjectAllocatorImpl.cs Outdated
Comment thread libs/storage/Tsavorite/cs/src/core/Allocator/AllocatorBase.cs Outdated
Wrap the read-only flush loop and the per-page recovery WriteAsync in
try/finally so the rented CircularDiskWriteBuffer is returned to the pool even
if a page write throws.

Co-authored-by: Copilot App <[email protected]>
Copilot-Session: f45128a9-8d17-4a2e-b84d-6da1afd3cc73
@TedHartMS
Ted Hart (TedHartMS) merged commit fcdb5fc into main Aug 9, 2026
431 of 433 checks passed
@TedHartMS
Ted Hart (TedHartMS) deleted the tedhar/readbuff-dispose branch August 9, 2026 02:50
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.

3 participants