FetchStage: pass prefetch intention to EloqIndexCursor#446
FetchStage: pass prefetch intention to EloqIndexCursor#446xiexiaoy merged 1 commit intoeloqdata:mainfrom
Conversation
WalkthroughAdds an opt-in prefetch hook to SortedDataInterface::Cursor, exposes IndexScan's internal cursor, enables FetchStage to call the hook for IndexScan-backed paths, implements prefetch gating in EloqIndexCursor, and includes minor local-variable/formatting tweaks. Changes
Sequence DiagramsequenceDiagram
participant Fetch as FetchStage
participant Scan as IndexScan
participant Cursor as SortedDataInterface::Cursor
participant Eloq as EloqIndexCursor
Fetch->>Scan: on first doWork(), detect IndexScan child
Scan->>Cursor: getCursor()
Fetch->>Cursor: enablePrefetchRecords()
activate Cursor
Cursor->>Eloq: enablePrefetchRecords() (override)
Eloq->>Eloq: set _enablePrefetchRecords = true
deactivate Cursor
Fetch->>Scan: proceed to child doWork()
loop cursor advancement
Scan->>Cursor: advance index cursor
alt _enablePrefetchRecords (rgba(0,128,0,0.5))
Cursor->>Eloq: prefetch record data
else not enabled (rgba(255,0,0,0.5))
Eloq->>Eloq: skip prefetch
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/mongo/db/modules/eloq/src/eloq_index.cpp (1)
326-335: Count-scan path can hit MONGO_UNREACHABLE.
Line 330 skips prefetch, but_updateRecordPtr()still assumes prefetched records for STANDARD/UNIQUE indexes and willMONGO_UNREACHABLEwhen the vector is empty. Count scans only needRecordId, so avoid the lookup when_scanForCountis true.✅ Proposed fix (guard in _updateRecordPtr)
@@ case IndexCursorType::UNIQUE: case IndexCursorType::STANDARD: { + if (_scanForCount) { + // CountScan only needs RecordId; avoid prefetched record lookup + _recordPtr = nullptr; + break; + } // We dont have prefetch records for upsert operations if (!_opCtx->isUpsert()) { // Look up prefetched record by index
8b726a2 to
1c9a56b
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/mongo/db/modules/eloq/src/eloq_index.cpp (1)
326-336: Missing_scanForCounthandling in_updateRecordPtr()will cause assertion failure.When
_scanForCountis true, prefetching is correctly skipped here. However,_updateRecordPtr()(lines 527-574) is still called from_updatePosition()at line 355. For STANDARD/UNIQUE index types when not in upsert mode,_updateRecordPtr()enters the prefetch lookup branch (line 537) but finds empty_prefetchedRecords, eventually hittingMONGO_UNREACHABLE.
_updateRecordPtr()needs to check_scanForCountsimilarly to how it checksisUpsert():Proposed fix
case IndexCursorType::UNIQUE: case IndexCursorType::STANDARD: { // We dont have prefetch records for upsert operations - if (!_opCtx->isUpsert()) { + if (!_opCtx->isUpsert() && !_scanForCount) { // Look up prefetched record by index // ... existing prefetch lookup code ... } else { - // For upsert operations, we dont prefetch records + // For upsert or count operations, we dont prefetch records _recordPtr = nullptr; } } break;
1c9a56b to
b4943d2
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/mongo/db/modules/eloq/src/eloq_index.cpp (1)
656-661: Resetting_disablePrefetchRecordsin_clearPrefetchedRecords()may negate thedisablePrefetchRecords()call.In
CountScan::doWork, the sequence is:
_cursor->setEndPosition(...)_cursor->disablePrefetchRecords()— sets_disablePrefetchRecords = true_cursor->seek(...)— calls_seekCursor()→_clearPrefetchedRecords()→ resets_disablePrefetchRecords = falseThis means the flag is reset before the prefetch logic in
_updatePosition()is reached, effectively makingdisablePrefetchRecords()a no-op.Consider moving the flag reset out of
_clearPrefetchedRecords(), or only reset it inreset()where a full cursor re-initialization is intended.🐛 Proposed fix
void _clearPrefetchedRecords() { - _disablePrefetchRecords = false; _prefetchedRecords.clear(); _prefetchedBatchStartIdx = 0; _lastRecordsBatchCnt = 0; }If you want to reset the flag only during full cursor reset, keep it in
reset():void reset(const EloqIndex* idx, OperationContext* opCtx, bool forward, IndexCursorType cursorType) { // ... existing code ... // Clear prefetched records _clearPrefetchedRecords(); + _disablePrefetchRecords = false; // Only reset on full cursor reset }
b4943d2 to
8c26ea7
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/mongo/db/exec/fetch.cpp`:
- Around line 87-92: The null-check currently uses FetchStage::_cursor but then
dereferences the index cursor local variable cursor; change the guard to check
the index cursor (SortedDataInterface::Cursor* cursor from IndexScan in the
child() block) instead of _cursor so you only call
cursor->enablePrefetchRecords() when cursor is non-null; locate the dynamic_cast
to IndexScan* (function child()) and replace the if condition that checks
_cursor with a check of cursor.
| const std::unique_ptr<PlanStage>& childStage = child(); | ||
| if (IndexScan* stage = dynamic_cast<IndexScan*>(childStage.get()); stage) { | ||
| SortedDataInterface::Cursor* cursor = stage->getCursor(); | ||
| if (_cursor) { | ||
| cursor->enablePrefetchRecords(); | ||
| } |
There was a problem hiding this comment.
Fix null-check to guard the index cursor, not the record-store cursor.
Line 90 checks _cursor (FetchStage’s record-store cursor) but then dereferences cursor. This can skip prefetch enablement and can also null-deref if the index cursor is absent (e.g., after scan end/reset). Use cursor in the check.
🛠️ Proposed fix
- if (IndexScan* stage = dynamic_cast<IndexScan*>(childStage.get()); stage) {
- SortedDataInterface::Cursor* cursor = stage->getCursor();
- if (_cursor) {
- cursor->enablePrefetchRecords();
- }
- }
+ if (IndexScan* stage = dynamic_cast<IndexScan*>(childStage.get()); stage) {
+ SortedDataInterface::Cursor* cursor = stage->getCursor();
+ if (cursor) {
+ cursor->enablePrefetchRecords();
+ }
+ }🤖 Prompt for AI Agents
In `@src/mongo/db/exec/fetch.cpp` around lines 87 - 92, The null-check currently
uses FetchStage::_cursor but then dereferences the index cursor local variable
cursor; change the guard to check the index cursor (SortedDataInterface::Cursor*
cursor from IndexScan in the child() block) instead of _cursor so you only call
cursor->enablePrefetchRecords() when cursor is non-null; locate the dynamic_cast
to IndexScan* (function child()) and replace the if condition that checks
_cursor with a check of cursor.
| WorkingSetID id; | ||
| StageState status; | ||
| if (_idRetrying == WorkingSet::INVALID_ID) { | ||
| const std::unique_ptr<PlanStage>& childStage = child(); |
There was a problem hiding this comment.
So the current workflow is
- scan return first tuple
- fetch stage mark cursor as enable prefetch
- fetch tuple read pk table for first index tuple
- sk index scan then prefetch rest of the tuples?
There was a problem hiding this comment.
So the current workflow is
- scan return first tuple
- fetch stage mark cursor as enable prefetch
- fetch tuple read pk table for first index tuple
- sk index scan then prefetch rest of the tuples?
Right.
8c26ea7 to
59bb1b4
Compare
Summary by CodeRabbit
New Features
Performance Improvements
Chores