Skip to content

HBASE-27691 Prevent filters from seeing synthetic scan start cells - #8485

Open
noslowerdna wants to merge 3 commits into
apache:masterfrom
noslowerdna:HBASE-27691
Open

HBASE-27691 Prevent filters from seeing synthetic scan start cells#8485
noslowerdna wants to merge 3 commits into
apache:masterfrom
noslowerdna:HBASE-27691

Conversation

@noslowerdna

@noslowerdna noslowerdna commented Jul 20, 2026

Copy link
Copy Markdown

HBASE-27691

What changes were proposed in this pull request?

This patch disables StoreScanner's initial lazy seek for filtered non-Get scans. Doing so prevents synthetic lazy-seek Cells from being exposed to server-side Filters and Comparators, while retaining lazy seeking for unfiltered Scans and Gets.

Why are the changes needed?

A region start boundary is not guaranteed to be a valid application row key. Passing its synthetic Cell to a RowFilter comparator violates the Filter contract that filterRowKey receives the first actual Cell of a row and can result in unexpected exceptions when Filter / Comparator code attempts to parse a seemingly truncated or otherwise malformed row key.

In our case that manifested like this:

Caused by: java.lang.RuntimeException: java.io.EOFException
	at com.package.MyComparator.compareTo(MyComparator.java:133)
	at org.apache.hadoop.hbase.PrivateCellUtil.compareRow(PrivateCellUtil.java:1240)
	at org.apache.hadoop.hbase.filter.CompareFilter.compareRow(CompareFilter.java:148)
	at org.apache.hadoop.hbase.filter.RowFilter.filterRowKey(RowFilter.java:90)
	at org.apache.hadoop.hbase.filter.FilterListWithOR.filterRowKey(FilterListWithOR.java:345)
	at org.apache.hadoop.hbase.filter.FilterList.filterRowKey(FilterList.java:152)
	at org.apache.hadoop.hbase.filter.FilterListWithAND.filterRowKey(FilterListWithAND.java:227)
	at org.apache.hadoop.hbase.filter.FilterList.filterRowKey(FilterList.java:152)
	at org.apache.hadoop.hbase.filter.FilterWrapper.filterRowKey(FilterWrapper.java:108)
	at org.apache.hadoop.hbase.regionserver.HRegion$RegionScannerImpl.filterRowKey(HRegion.java:7545)
	at org.apache.hadoop.hbase.regionserver.HRegion$RegionScannerImpl.nextInternal(HRegion.java:7361)
	at org.apache.hadoop.hbase.regionserver.HRegion$RegionScannerImpl.nextRaw(HRegion.java:7153)
	at org.apache.hadoop.hbase.regionserver.RSRpcServices.scan(RSRpcServices.java:3330)
	...

This change reinstates the protection intended by HBASE-6562, using its later proposed (unmerged) eager initial seek patch.

Are there any concerns?

First let's provide some background context.

How does lazy seeking work, and why is its design problematic?

Lazy seeking uses synthetic Cells as part of the scanner's control flow. They are essentially placeholders indicating "start looking from here." A problem with this design is that before a real Cell has been read from a StoreFile, this placeholder is made visible to a filter with no supplemental context to be able to differentiate it as such. A filter has no definitive means to distinguish a synthetic Cell from a real one, so it may attempt to unsafely process the row key (decoding it, comparing it, using the value to decide to end the scan, etc). Filter or comparator code therefore can be required to have overly broad exception handling.

What is the proposal for fixing this design problem?

To elaborate on what was stated above, this patch ensures a filtered non-Get scan reads a real first Cell before invoking the filter. The correction is limited to this specific case where correctness requires that filters see a real Cell. Scans without explicit columns already do an eager initial seek. Unfiltered scans, Gets, and subsequent post-initialization repositioning still retain the existing lazy seek behavior.

What are the risks of making this correction?

A known risk of making this correction is potentially unnecessary work done at the beginning of the scan. A StoreScanner is the scanner for one column family in one region scan. On opening it, HBase may have multiple underlying scanners, one for each StoreFile plus one for the Region Server MemStore. Previously with an explicit-column scan, HBase could tell each underlying scanner "don't seek yet - wait until I know you are needed." The patch instead says that filtered non-Get scans must "seek now, so the first Cell I present to the filter is real." If a column family has many StoreFiles, each selected StoreFile scanner may do an HFile index lookup and potentially read blocks to position itself. The MemStore scanner is also positioned. The cost is proportional to the number of underlying scanners opened, not the number of rows returned. That overhead can materially matter for short filtered scans over stores with many files if the scan would otherwise have avoided touching some of them.

Is there a better way to fix this?

I have been unable to determine a more appropriate way to address this and am open to ideas. Deeper API design changes could involve enhancing the Filter abstract class or Cell interface and do not appear advisable for such a narrow problem surface area, as that would introduce major risks.

How was this patch tested?

A ROWCOL Bloom regression test verifies that a Comparator sees only the persisted row, not a synthetic region-boundary Cell. Focused tests also cover each lazy-seek decision branch.

mvn -ntp -pl hbase-server -am \
  -Dtest=TestScanner \
  -Dsurefire.failIfNoSpecifiedTests=false \
  test

@noslowerdna

Copy link
Copy Markdown
Author

@apurtell @virajjasani @Apache9 Would appreciate a review when you have a moment.

@noslowerdna

Copy link
Copy Markdown
Author

@junegunn Would appreciate a review when you have a moment. Thanks!

@junegunn

junegunn commented Jul 31, 2026

Copy link
Copy Markdown
Member

I could reproduce the problem locally:

java_import org.apache.hadoop.hbase.CompareOperator
java_import org.apache.hadoop.hbase.filter.BinaryComparator
java_import org.apache.hadoop.hbase.filter.RowFilter
java_import org.apache.hadoop.hbase.filter.WhileMatchFilter

create 't', 'd'
put 't', 'row1', 'd:foo', 'bar'
put 't', 'row2', 'd:foo', 'bar'
put 't', 'row3', 'd:foo', 'bar'
flush 't'

# 3 rows
scan 't', FILTER => WhileMatchFilter.new(RowFilter.new(CompareOperator::NOT_EQUAL,
                                                       BinaryComparator.new(''.to_java_bytes)))

# no rows
scan 't', FILTER => WhileMatchFilter.new(RowFilter.new(CompareOperator::NOT_EQUAL,
                                                       BinaryComparator.new(''.to_java_bytes))),
          COLUMNS => ['d:foo']

The history behind this issue is quite involved, so I don't feel confident making a judgment call on my own. From what I understand, this patch was first suggested by Lars Hofhansl in early 2013:

He raised a concern about the performance impact of eager seeks, but the question was never answered. Adding hasFilterRowKey was briefly discussed as a way to limit the cases where the lazy seek optimization is lost, but the community never reached a consensus.

So the question still remains. Do you have a view on the performance impact?

The eager seek is once per scanner open rather than per row, and scans without explicit columns already take that path, so it may well be fine. But it applies to every filtered explicit-column non-Get scan, so we should understand the cost before making it the default.

To be clear, I am not suggesting we leave the bug unfixed. Correctness should take priority over performance. I would just like to understand the cost before we commit.

@noslowerdna

noslowerdna commented Aug 3, 2026

Copy link
Copy Markdown
Author

@junegunn Thanks for the thoughtful review. I have added another unit test based on how you reproduced the issue with the WhileMatchFilter. I also updated the description of this PR to express the potential cost concern. I do not know how to best simulate a reasonable worst-case scenario to gather concrete statistics however. Any advice?

For awareness I do have another branch with a new configuration property implemented to be able to revert this behavior change in cases where the current behavior is preferable (if unset, the default is to fix this bug).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants