From f09ffabc9cf5c582df0b33dab556c843c08cbdb8 Mon Sep 17 00:00:00 2001 From: Andrew Olson Date: Mon, 20 Jul 2026 14:01:50 -0500 Subject: [PATCH] HBASE-27691 Prevent filters from seeing synthetic scan start cells --- .../hbase/regionserver/StoreScanner.java | 7 +- .../hbase/regionserver/TestScanner.java | 149 ++++++++++++++++++ 2 files changed, 154 insertions(+), 2 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 86752f27a0f6..e686899e7677 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 StoreScanner(HStore store, ScanInfo scanInfo, Scan scan, NavigableSet columns) + throws IOException { + super(store, store.getScanInfo(), scan, columns, Long.MAX_VALUE); + } + + @Override + protected void seekScanners(List 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 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 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 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. */