ExportXMLWordPrintableJSON

    • Type: Task
    • Resolution: Unresolved
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Component/s: None
    • Storage Execution
    • Storage Execution 2026-10-26
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      Problem

      For a primary-driven index build of N indexes, the load phase calls done() for all N indexes before committing any, so every sorter builds its final merge iterator up front:

      multi_index_block.cpp:1271-1275

      for (auto&& index : _indexes) {
          // If the build has spilled, force all builders to spill their in-memory remainder so the
          // persisted sorter is a complete copy and it's safe to resume from the load phase.
          index.bulk->done(/*forceSpill=*/_anyIndexSpilled);
      }
      

      Building a sorter's merge iterator reads the head of every spilled range, which positions every range's cursor and pins its leaf page. The chain:

      After the done() loop, all N sorters' cursors (about N ranges each) are positioned and pinned, and they stay pinned until _releaseReplicatedSorters at the end of the phase (multi_index_block.cpp:1424-1434, index_access_method.cpp:1608-1610).

      On a large collection with a small WT cache this pins enough pages to stall clean eviction. 

      Proposed fix

      Decouple the force-spill (required before the kBulkLoad phase write for resumability) from merge-iterator construction:

      1. Force-spill all sorters without building merge iterators (add BulkBuilder::forceSpill, or split done()).
      2. Write the phase metadata as today.
      3. Per index: done() then commit() then releaseSorter(), so only one sorter's cursors are positioned at a time and its iterators/cursors/buffers are freed before the next index.

      Peak pinned cursors drops from about (numIndexes x rangesPerSorter) to rangesPerSorter.

      Why it is safe

      • _writeIndexBuildMetadataToContainer writes only IndexBuildMetadata (buildUUID, phase, collectionUUID) and no sorter state: multi_index_block.cpp:1829-1847. It does not require done().
      • Per-index IndexStateInfo is persisted by the ContainerSpillYielder::onSpill callback via getPersistedState(), which snapshots the spiller's current ranges: multi_index_block.cpp:1939-1958, sorter_template_defs.h:438-449. It works before or after a merge; it only requires the in-memory remainder to be spilled.
      • The only ordering constraint (all sorters force-spilled before the phase flip) is preserved. Add a tassert to enforce it.
      • BulkBuilder::done has one caller and _sortedIterator has no other readers.

      Caveats

      • Subtle behavior diff: today a mergeSpills-triggered onSpill during done() runs while _phase == kCollectionScan, so the persisted IndexStateInfo includes lastSpilledRecordId. Deferred, it will not. Harmless (that field is only read when resuming from kCollectionScan), but resume-info parsing tests may observe it.
      • Smaller alternative (Option 2): make MergeIterator/Stream lazy so positioning defers to the first read. It stops the pinning but does not free iterators per index, and it touches shared query-path machinery. Option 1 is preferred.

            Assignee:
            Shin Yee Tan
            Reporter:
            Shin Yee Tan
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated: