@@ -81,6 +81,7 @@ class RegionScannerImpl implements RegionScanner, Shipper, RpcCallback {
8181
8282 protected final byte [] stopRow ;
8383 protected final boolean includeStopRow ;
84+ protected final boolean reversed ;
8485 protected final HRegion region ;
8586 protected final CellComparator comparator ;
8687
@@ -123,6 +124,7 @@ private static boolean hasNonce(HRegion region, long nonce) {
123124 defaultScannerContext = ScannerContext .newBuilder ().setBatchLimit (scan .getBatch ()).build ();
124125 this .stopRow = scan .getStopRow ();
125126 this .includeStopRow = scan .includeStopRow ();
127+ this .reversed = scan .isReversed ();
126128 this .operationId = scan .getId ();
127129
128130 // synchronize on scannerReadPoints so that nobody calculates
@@ -492,11 +494,18 @@ private boolean nextInternal(List<Cell> results, ScannerContext scannerContext)
492494 if (isFilterDoneInternal ()) {
493495 return scannerContext .setScannerState (NextState .NO_MORE_VALUES ).hasMoreValues ();
494496 }
497+ // HBASE-29974: ask the filter for a seek hint so we can jump directly past the rejected
498+ // row instead of iterating through its cells one-by-one via nextRow().
499+ Cell rowHint = getHintForRejectedRow (current );
495500 // Typically the count of rows scanned is incremented inside #populateResult. However,
496501 // here we are filtering a row based purely on its row key, preventing us from calling
497- // #populateResult. Thus, perform the necessary increment here to rows scanned metric
502+ // #populateResult. Thus, perform the necessary increment here to rows scanned metric.
503+ // Placed after getHintForRejectedRow so that a buggy filter throwing DNRIOE doesn't
504+ // leave the metric incremented for a row that was never actually processed.
498505 incrementCountOfRowsScannedMetric (scannerContext );
499- boolean moreRows = nextRow (scannerContext , current );
506+ boolean moreRows = (rowHint != null )
507+ ? nextRowViaHint (scannerContext , current , rowHint )
508+ : nextRow (scannerContext , current );
500509 if (!moreRows ) {
501510 return scannerContext .setScannerState (NextState .NO_MORE_VALUES ).hasMoreValues ();
502511 }
@@ -721,6 +730,76 @@ protected boolean nextRow(ScannerContext scannerContext, Cell curRowCell) throws
721730 || this .region .getCoprocessorHost ().postScannerFilterRow (this , curRowCell );
722731 }
723732
733+ /**
734+ * Fast-path alternative to {@link #nextRow} used when the filter has provided a seek hint via
735+ * {@link org.apache.hadoop.hbase.filter.Filter#getHintForRejectedRow(Cell)}. Instead of iterating
736+ * through every cell in the rejected row one-by-one, this method issues a single seek to jump
737+ * directly to the filter's suggested position ({@code requestSeek} for forward scans,
738+ * {@code backwardSeek} for reversed scans).
739+ * <p>
740+ * The filter state is reset after the seek so the next row starts with a clean filter context.
741+ * <p>
742+ * <strong>Stop-row invariant:</strong> This method does not validate that {@code hint} falls
743+ * within the scan's stop row. If the hint overshoots, the next iteration's
744+ * {@link #shouldStop(Cell)} check catches it and returns NO_MORE_VALUES. One wasted seek may
745+ * occur, but correctness is maintained.
746+ * <p>
747+ * <strong>Metrics note:</strong> The rows-scanned metric is incremented once by the caller for
748+ * the rejected row. Rows physically skipped by the seek are not individually counted — this
749+ * reflects the fact that no per-row work was done for those rows.
750+ * <p>
751+ * <strong>Coprocessor note:</strong> {@code postScannerFilterRow} is invoked once with
752+ * {@code curRowCell}, not once per skipped row. Coprocessors counting filtered rows should be
753+ * aware of this semantic when the hint path is used.
754+ * @param scannerContext scanner context used for limit tracking
755+ * @param curRowCell the first cell of the row that was rejected by {@code filterRowKey};
756+ * passed to the coprocessor hook for observability
757+ * @param hint the {@link Cell} returned by the filter; the scanner will seek to this
758+ * position
759+ * @return {@code true} if scanning should continue, {@code false} if a coprocessor requests an
760+ * early stop (mirrors the contract of {@link #nextRow})
761+ * @throws IOException if the seek or the coprocessor hook signals a failure
762+ */
763+ private boolean nextRowViaHint (ScannerContext scannerContext , Cell curRowCell , Cell hint )
764+ throws IOException {
765+ assert this .joinedContinuationRow == null : "Trying to go to next row during joinedHeap read." ;
766+
767+ int difference = comparator .compareRows (hint , curRowCell );
768+ if ((!reversed && difference > 0 ) || (reversed && difference < 0 )) {
769+ if (reversed ) {
770+ // ReversedKeyValueHeap does not support requestSeek; use backwardSeek
771+ // to position at-or-before the hint within the target row.
772+ // seekToPreviousRow would skip past the hint row entirely.
773+ this .storeHeap .backwardSeek (hint );
774+ } else {
775+ this .storeHeap .requestSeek (hint , true , true );
776+ }
777+
778+ resetFilters ();
779+
780+ return this .region .getCoprocessorHost () == null
781+ || this .region .getCoprocessorHost ().postScannerFilterRow (this , curRowCell );
782+ }
783+
784+ return nextRow (scannerContext , curRowCell );
785+ }
786+
787+ /**
788+ * Asks the current {@link org.apache.hadoop.hbase.filter.FilterWrapper} for a seek hint to use
789+ * after a row has been rejected by {@link #filterRowKey}. If the wrapped filter overrides
790+ * {@link org.apache.hadoop.hbase.filter.Filter#getHintForRejectedRow(Cell)}, this returns its
791+ * answer; otherwise returns {@code null}.
792+ * @param rowCell the first cell of the rejected row (same cell passed to {@code filterRowKey})
793+ * @return a {@link Cell} seek target, or {@code null} if the filter provides no hint
794+ * @throws IOException if the filter signals an I/O failure
795+ */
796+ private Cell getHintForRejectedRow (Cell rowCell ) throws IOException {
797+ if (filter == null ) {
798+ return null ;
799+ }
800+ return filter .getHintForRejectedRow (rowCell );
801+ }
802+
724803 protected boolean shouldStop (Cell currentRowCell ) {
725804 if (currentRowCell == null ) {
726805 return true ;
0 commit comments