This is an automated email from the ASF dual-hosted git repository.

junegunn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hbase.git


The following commit(s) were added to refs/heads/master by this push:
     new 59fc59ca7c1 HBASE-27691 Prevent filters from seeing synthetic scan 
start cells (#8485)
59fc59ca7c1 is described below

commit 59fc59ca7c10ab0cd9fa90a2c9afc70db3febee4
Author: Andrew Olson <[email protected]>
AuthorDate: Fri Aug 7 00:35:39 2026 -0500

    HBASE-27691 Prevent filters from seeing synthetic scan start cells (#8485)
    
    Co-authored-by: Andrew Olson <[email protected]>
    Signed-off-by: Junegunn Choi <[email protected]>
---
 .../hadoop/hbase/regionserver/StoreScanner.java    |   7 +-
 .../hadoop/hbase/regionserver/TestScanner.java     | 189 +++++++++++++++++++++
 .../hbase/regionserver/TestSeekOptimizations.java  |  63 +++++--
 3 files changed, 243 insertions(+), 16 deletions(-)

diff --git 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/StoreScanner.java
 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/StoreScanner.java
index 86752f27a0f..1309c3456a8 100644
--- 
a/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/StoreScanner.java
+++ 
b/hbase-server/src/main/java/org/apache/hadoop/hbase/regionserver/StoreScanner.java
@@ -279,8 +279,11 @@ public class StoreScanner extends 
NonReversedNonLazyKeyValueScanner
       // Always check bloom filter to optimize the top row seek for delete
       // family marker.
 
-      seekScanners(scanners, matcher.getStartKey(), explicitColumnQuery && 
lazySeekEnabledGlobally,
-        parallelSeekEnabled);
+      // Filters must only see real Cells. A lazy seek can expose a synthetic 
Cell
+      // for the scan start row, so disable it for non-Get filters.
+      boolean useLazySeek =
+        explicitColumnQuery && lazySeekEnabledGlobally && !(scan.hasFilter() 
&& !scan.isGetScan());
+      seekScanners(scanners, matcher.getStartKey(), useLazySeek, 
parallelSeekEnabled);
 
       // set storeLimit
       this.storeLimit = scan.getMaxResultsPerColumnFamily();
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestScanner.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestScanner.java
index a5078c12959..b2aa81c7103 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestScanner.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestScanner.java
@@ -29,8 +29,11 @@ import static org.junit.jupiter.api.Assertions.fail;
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.NavigableSet;
 import org.apache.hadoop.hbase.Cell;
 import org.apache.hadoop.hbase.CellUtil;
+import org.apache.hadoop.hbase.CompareOperator;
+import org.apache.hadoop.hbase.ExtendedCell;
 import org.apache.hadoop.hbase.HBaseTestingUtil;
 import org.apache.hadoop.hbase.HConstants;
 import org.apache.hadoop.hbase.HTestConst;
@@ -48,9 +51,12 @@ import org.apache.hadoop.hbase.client.Scan;
 import org.apache.hadoop.hbase.client.Table;
 import org.apache.hadoop.hbase.client.TableDescriptor;
 import org.apache.hadoop.hbase.client.TableDescriptorBuilder;
+import org.apache.hadoop.hbase.filter.BinaryComparator;
+import org.apache.hadoop.hbase.filter.ByteArrayComparable;
 import org.apache.hadoop.hbase.filter.Filter;
 import org.apache.hadoop.hbase.filter.InclusiveStopFilter;
 import org.apache.hadoop.hbase.filter.PrefixFilter;
+import org.apache.hadoop.hbase.filter.RowFilter;
 import org.apache.hadoop.hbase.filter.WhileMatchFilter;
 import org.apache.hadoop.hbase.testclassification.MediumTests;
 import org.apache.hadoop.hbase.testclassification.RegionServerTests;
@@ -95,6 +101,10 @@ public class TestScanner {
 
   private static final long START_CODE = Long.MAX_VALUE;
 
+  private static final byte[] LAZY_SEEK_FAMILY = Bytes.toBytes("family");
+  private static final byte[] LAZY_SEEK_QUALIFIER = Bytes.toBytes("qualifier");
+  private static final byte[] LAZY_SEEK_ROW = Bytes.toBytes("row");
+
   private HRegion region;
 
   private byte[] firstRowBytes, secondRowBytes, thirdRowBytes;
@@ -113,6 +123,185 @@ public class TestScanner {
     col1 = Bytes.toBytes("column1");
   }
 
+  private static final class RecordingStoreScanner extends StoreScanner {
+    private boolean initialSeekWasLazy;
+
+    RecordingStoreScanner(HStore store, Scan scan, NavigableSet<byte[]> 
columns)
+      throws IOException {
+      super(store, store.getScanInfo(), scan, columns, Long.MAX_VALUE);
+    }
+
+    @Override
+    protected void seekScanners(List<? extends KeyValueScanner> scanners, 
ExtendedCell seekKey,
+      boolean isLazy, boolean isParallelSeek) throws IOException {
+      initialSeekWasLazy = isLazy;
+      super.seekScanners(scanners, seekKey, isLazy, isParallelSeek);
+    }
+  }
+
+  private static final class TrackingRowComparator extends ByteArrayComparable 
{
+    private final List<byte[]> comparedRows = new ArrayList<>();
+
+    TrackingRowComparator(byte[] value) {
+      super(value);
+    }
+
+    @Override
+    public int compareTo(byte[] value, int offset, int length) {
+      comparedRows.add(Bytes.copy(value, offset, length));
+      return Bytes.compareTo(getValue(), 0, getValue().length, value, offset, 
length);
+    }
+
+    @Override
+    public byte[] toByteArray() {
+      return getValue();
+    }
+  }
+
+  @Test
+  public void testFilterComparatorOnlySeesActualRows() throws Exception {
+    byte[] family = Bytes.toBytes("family");
+    byte[] qualifier = Bytes.toBytes("qualifier");
+    byte[] regionStartKey = new byte[] { 1 };
+    byte[] row = new byte[] { 1, 0, 1 };
+    TableDescriptor tableDescriptor =
+      
TableDescriptorBuilder.newBuilder(TableName.valueOf("testFilterComparatorOnlySeesActualRows"))
+        .setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(family)
+          .setBloomFilterType(BloomType.ROWCOL).build())
+        .build();
+    TrackingRowComparator comparator = new TrackingRowComparator(row);
+
+    StoreScanner.enableLazySeekGlobally(true);
+    try {
+      this.region = TEST_UTIL.createLocalHRegion(tableDescriptor, 
regionStartKey, null);
+      Put put = new Put(row);
+      put.addColumn(family, qualifier, Bytes.toBytes("value"));
+      region.put(put);
+      region.flush(true);
+
+      Scan scan = new Scan().withStartRow(regionStartKey);
+      scan.addColumn(family, qualifier);
+      scan.setFilter(new RowFilter(CompareOperator.EQUAL, comparator));
+      List<Cell> results = new ArrayList<>();
+      try (InternalScanner scanner = region.getScanner(scan)) {
+        assertFalse(scanner.next(results));
+      }
+
+      assertEquals(1, results.size());
+      assertTrue(CellUtil.matchingRows(results.get(0), row));
+      assertEquals(1, comparator.comparedRows.size());
+      assertTrue(Bytes.equals(row, comparator.comparedRows.get(0)));
+    } finally {
+      
StoreScanner.enableLazySeekGlobally(StoreScanner.LAZY_SEEK_ENABLED_BY_DEFAULT);
+      HBaseTestingUtil.closeRegionAndWAL(this.region);
+    }
+  }
+
+  @Test
+  public void testWhileMatchFilterOnlySeesActualRows() throws Exception {
+    byte[] family = Bytes.toBytes("family");
+    byte[] qualifier = Bytes.toBytes("qualifier");
+    TableDescriptor tableDescriptor =
+      
TableDescriptorBuilder.newBuilder(TableName.valueOf("testWhileMatchFilterOnlySeesActualRows"))
+        
.setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(family).build()).build();
+
+    StoreScanner.enableLazySeekGlobally(true);
+    try {
+      this.region = TEST_UTIL.createLocalHRegion(tableDescriptor, null, null);
+      List<String> rows = List.of("row1", "row2", "row3");
+      for (String row : rows) {
+        Put put = new Put(Bytes.toBytes(row)).addColumn(family, qualifier, 
Bytes.toBytes("value"));
+        region.put(put);
+      }
+      region.flush(true);
+
+      Scan scan = new Scan().addColumn(family, qualifier)
+        .setFilter(new WhileMatchFilter(new 
RowFilter(CompareOperator.NOT_EQUAL,
+          new BinaryComparator(HConstants.EMPTY_START_ROW))));
+      int scannedRows = 0;
+      try (InternalScanner scanner = region.getScanner(scan)) {
+        boolean hasMoreRows;
+        do {
+          List<Cell> results = new ArrayList<>();
+          hasMoreRows = scanner.next(results);
+          if (!results.isEmpty()) {
+            ++scannedRows;
+          }
+        } while (hasMoreRows);
+      }
+
+      assertEquals(rows.size(), scannedRows);
+    } finally {
+      
StoreScanner.enableLazySeekGlobally(StoreScanner.LAZY_SEEK_ENABLED_BY_DEFAULT);
+      HBaseTestingUtil.closeRegionAndWAL(this.region);
+    }
+  }
+
+  @Test
+  public void testInitialLazySeekForUnfilteredExplicitColumnScan() throws 
Exception {
+    Scan scan = new Scan().withStartRow(LAZY_SEEK_ROW);
+    scan.addColumn(LAZY_SEEK_FAMILY, LAZY_SEEK_QUALIFIER);
+    assertInitialLazySeek(scan, true, true);
+  }
+
+  @Test
+  public void testInitialLazySeekForFilteredGet() throws Exception {
+    Get get = new Get(LAZY_SEEK_ROW);
+    get.addColumn(LAZY_SEEK_FAMILY, LAZY_SEEK_QUALIFIER);
+    get.setFilter(new PrefixFilter(LAZY_SEEK_ROW));
+    assertInitialLazySeek(new Scan(get), true, true);
+  }
+
+  @Test
+  public void testInitialLazySeekForFilteredNonGetScan() throws Exception {
+    Scan scan = new Scan().withStartRow(LAZY_SEEK_ROW);
+    scan.addColumn(LAZY_SEEK_FAMILY, LAZY_SEEK_QUALIFIER);
+    scan.setFilter(new PrefixFilter(LAZY_SEEK_ROW));
+    assertInitialLazySeek(scan, true, false);
+  }
+
+  @Test
+  public void testInitialLazySeekForAllColumnScan() throws Exception {
+    assertInitialLazySeek(new Scan().withStartRow(LAZY_SEEK_ROW), true, false);
+  }
+
+  @Test
+  public void testInitialLazySeekWhenDisabledGlobally() throws Exception {
+    Scan scan = new Scan().withStartRow(LAZY_SEEK_ROW);
+    scan.addColumn(LAZY_SEEK_FAMILY, LAZY_SEEK_QUALIFIER);
+    assertInitialLazySeek(scan, false, false);
+  }
+
+  private void assertInitialLazySeek(Scan scan, boolean lazySeekEnabled, 
boolean expected)
+    throws IOException {
+    StoreScanner.enableLazySeekGlobally(lazySeekEnabled);
+    try {
+      HStore store = createLazySeekTestStore();
+      try (RecordingStoreScanner scanner =
+        new RecordingStoreScanner(store, scan, 
scan.getFamilyMap().get(LAZY_SEEK_FAMILY))) {
+        assertEquals(expected, scanner.initialSeekWasLazy);
+      }
+    } finally {
+      
StoreScanner.enableLazySeekGlobally(StoreScanner.LAZY_SEEK_ENABLED_BY_DEFAULT);
+      if (this.region != null) {
+        HBaseTestingUtil.closeRegionAndWAL(this.region);
+        this.region = null;
+      }
+    }
+  }
+
+  private HStore createLazySeekTestStore() throws IOException {
+    TableDescriptor tableDescriptor = TableDescriptorBuilder
+      .newBuilder(TableName.valueOf("testInitialLazySeek"))
+      
.setColumnFamily(ColumnFamilyDescriptorBuilder.newBuilder(LAZY_SEEK_FAMILY).build()).build();
+    this.region = TEST_UTIL.createLocalHRegion(tableDescriptor, null, null);
+    Put put = new Put(LAZY_SEEK_ROW);
+    put.addColumn(LAZY_SEEK_FAMILY, LAZY_SEEK_QUALIFIER, 
Bytes.toBytes("value"));
+    region.put(put);
+    region.flush(true);
+    return region.getStore(LAZY_SEEK_FAMILY);
+  }
+
   /**
    * Test basic stop row filter works.
    */
diff --git 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestSeekOptimizations.java
 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestSeekOptimizations.java
index 3225f4a2b7f..8fc509ff4ef 100644
--- 
a/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestSeekOptimizations.java
+++ 
b/hbase-server/src/test/java/org/apache/hadoop/hbase/regionserver/TestSeekOptimizations.java
@@ -17,6 +17,7 @@
  */
 package org.apache.hadoop.hbase.regionserver;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
@@ -43,6 +44,7 @@ import 
org.apache.hadoop.hbase.client.ColumnFamilyDescriptorBuilder;
 import org.apache.hadoop.hbase.client.Delete;
 import org.apache.hadoop.hbase.client.Put;
 import org.apache.hadoop.hbase.client.Scan;
+import org.apache.hadoop.hbase.filter.PrefixFilter;
 import org.apache.hadoop.hbase.io.compress.Compression;
 import org.apache.hadoop.hbase.testclassification.MediumTests;
 import org.apache.hadoop.hbase.testclassification.RegionServerTests;
@@ -51,6 +53,7 @@ import org.apache.hadoop.hbase.util.Bytes;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.TestInfo;
 import org.junit.jupiter.api.TestTemplate;
 import org.junit.jupiter.params.provider.Arguments;
 import org.slf4j.Logger;
@@ -120,21 +123,19 @@ public class TestSeekOptimizations {
   }
 
   @BeforeEach
-  public void setUp() {
+  public void setUp(TestInfo testInfo) throws IOException {
     RNG.setSeed(91238123L);
     expectedKVs.clear();
     TEST_UTIL.getConfiguration().setInt(BloomFilterUtil.PREFIX_LENGTH_KEY, 10);
-  }
 
-  @TestTemplate
-  public void testMultipleTimestampRanges() throws IOException {
     // enable seek counting
     StoreFileScanner.instrument();
     ColumnFamilyDescriptor columnFamilyDescriptor =
       
ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(FAMILY)).setCompressionType(comprAlgo)
         .setBloomFilterType(bloomType).setMaxVersions(3).build();
 
-    region = TEST_UTIL.createTestRegion("testMultipleTimestampRanges", 
columnFamilyDescriptor);
+    region =
+      TEST_UTIL.createTestRegion(testInfo.getTestMethod().get().getName(), 
columnFamilyDescriptor);
 
     // Delete the given timestamp and everything before.
     final long latestDelTS = USE_MANY_STORE_FILES ? 1397 : -1;
@@ -150,12 +151,15 @@ public class TestSeekOptimizations {
     }
 
     prepareExpectedKVs(latestDelTS);
+  }
 
+  @TestTemplate
+  public void testMultipleTimestampRanges() throws IOException {
     for (int[] columnArr : COLUMN_SETS) {
       for (int[] rowRange : ROW_RANGES) {
         for (int maxVersions : MAX_VERSIONS_VALUES) {
           for (boolean lazySeekEnabled : new boolean[] { false, true }) {
-            testScan(columnArr, lazySeekEnabled, rowRange[0], rowRange[1], 
maxVersions);
+            testScan(columnArr, lazySeekEnabled, rowRange[0], rowRange[1], 
maxVersions, false);
           }
         }
       }
@@ -176,8 +180,9 @@ public class TestSeekOptimizations {
         + String.format("%.2f%%", expectedSeekSavings * 100));
   }
 
-  private void testScan(final int[] columnArr, final boolean lazySeekEnabled, 
final int startRow,
-    final int endRow, int maxVersions) throws IOException {
+  private ScanResult testScan(final int[] columnArr, final boolean 
lazySeekEnabled,
+    final int startRow, final int endRow, final int maxVersions, final boolean 
filtered)
+    throws IOException {
     StoreScanner.enableLazySeekGlobally(lazySeekEnabled);
     final Scan scan = new Scan();
     final Set<String> qualSet = new HashSet<>();
@@ -186,6 +191,9 @@ public class TestSeekOptimizations {
       scan.addColumn(FAMILY_BYTES, Bytes.toBytes(qualStr));
       qualSet.add(qualStr);
     }
+    if (filtered) {
+      scan.setFilter(new PrefixFilter(Bytes.toBytes("row")));
+    }
     scan.readVersions(maxVersions);
     scan.withStartRow(rowBytes(startRow));
 
@@ -198,18 +206,23 @@ public class TestSeekOptimizations {
 
     final long initialSeekCount = StoreFileScanner.getSeekCount();
     final InternalScanner scanner = region.getScanner(scan);
+    final long scannerOpenSeekCount = StoreFileScanner.getSeekCount() - 
initialSeekCount;
     final List<Cell> results = new ArrayList<>();
     final List<Cell> actualKVs = new ArrayList<>();
 
     // Such a clumsy do-while loop appears to be the official way to use an
     // internalScanner. scanner.next() return value refers to the _next_
     // result, not to the one already returned in results.
-    boolean hasNext;
-    do {
-      hasNext = scanner.next(results);
-      actualKVs.addAll(results);
-      results.clear();
-    } while (hasNext);
+    try {
+      boolean hasNext;
+      do {
+        hasNext = scanner.next(results);
+        actualKVs.addAll(results);
+        results.clear();
+      } while (hasNext);
+    } finally {
+      scanner.close();
+    }
 
     List<Cell> filteredKVs =
       filterExpectedResults(qualSet, rowBytes(startRow), rowBytes(endRow), 
maxVersions);
@@ -234,6 +247,7 @@ public class TestSeekOptimizations {
       totalSeekDiligent += seekCount;
     }
     assertKVListsEqual(testDesc, filteredKVs, actualKVs);
+    return new ScanResult(actualKVs, scannerOpenSeekCount);
   }
 
   private List<Cell> filterExpectedResults(Set<String> qualSet, byte[] 
startRow, byte[] endRow,
@@ -448,4 +462,25 @@ public class TestSeekOptimizations {
         + HBaseTestingUtil.safeGetAsStr(actual, i) + " (length " + aLen + ")" 
+ additionalMsg);
     }
   }
+
+  @TestTemplate
+  public void testSeeksEagerlyWhenFiltered() throws IOException {
+    ScanResult filteredLazyResults = testScan(new int[] { 0 }, true, 0, 2, 1, 
true);
+    ScanResult filteredEagerResults = testScan(new int[] { 0 }, false, 0, 2, 
1, true);
+    assertKVListsEqual("Filtered explicit column scan results differ with lazy 
seeking enabled",
+      filteredEagerResults.cells, filteredLazyResults.cells);
+    assertEquals(filteredEagerResults.scannerOpenSeekCount,
+      filteredLazyResults.scannerOpenSeekCount,
+      "Filtered explicit column scans must always eagerly seek");
+  }
+
+  private static final class ScanResult {
+    private final List<Cell> cells;
+    private final long scannerOpenSeekCount;
+
+    private ScanResult(List<Cell> cells, long scannerOpenSeekCount) {
+      this.cells = cells;
+      this.scannerOpenSeekCount = scannerOpenSeekCount;
+    }
+  }
 }

Reply via email to