Skip to content

Commit b86f828

Browse files
badrishcCopilot
andauthored
Fix deferred DoReadPage drain callback accessing disposed ScanIteratorBase (#1718)
BufferAndLoad registers DoReadPage as a deferred drain callback via BumpCurrentEpoch. This callback captures instance state including loadCompletionEvents. When the callback is for a read-ahead page and SafeToReclaimEpoch hasn't advanced yet, the callback remains in the drain list. If the scan completes and the iterator is disposed before the callback executes, loadCompletionEvents is set to null. When the callback later runs (during another thread's Drain), it accesses the null array, throwing NullReferenceException. This exception from a drain callback leaves the epoch in a held state (SuspendDrain's Resume/Release is not exception-safe), causing the cascading 'Trying to acquire protected epoch' assertion failure seen in ObjectIterationPushLockTest(4,4,Iterate,False). Fix: capture loadCompletionEvents into a local before accessing it in DoReadPage, and return early if the iterator has been disposed. Co-authored-by: badrishc <badrishc@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 19e7f82 commit b86f828

1 file changed

Lines changed: 7 additions & 1 deletion

File tree

libs/storage/Tsavorite/cs/src/core/Allocator/ScanIteratorBase.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,14 @@ protected bool BufferAndLoad(long currentIterationAddress, long currentPage, lon
208208

209209
void DoReadPage(int frameIndex)
210210
{
211+
// The drain callback may execute after the iterator has been disposed (loadCompletionEvents set to null),
212+
// because the callback is deferred via BumpCurrentEpoch and only runs when SafeToReclaimEpoch advances.
213+
// This can happen for read-ahead pages (frameIndex > 0) when the scan completes before the callback runs.
214+
var events = loadCompletionEvents;
215+
if (events is null)
216+
return;
211217
AsyncReadPageFromDeviceToFrame(readBuffer, readPage: frameIndex + GetPageOfAddress(currentIterationAddress, logPageSizeBits), untilAddress: endIterationAddress,
212-
context: Empty.Default, out loadCompletionEvents[nextFrame], devicePageOffset: 0, device: null, objectLogDevice: null, loadCTSs[nextFrame]);
218+
context: Empty.Default, out events[nextFrame], devicePageOffset: 0, device: null, objectLogDevice: null, loadCTSs[nextFrame]);
213219
loadedPages[nextFrame] = pageEndAddress;
214220
}
215221
}

0 commit comments

Comments
 (0)