linliu-code opened a new pull request, #694:
URL: https://github.com/apache/hudi-rs/pull/694

   ## Description
   
   **Stacked on #691, review that one first.** Until it merges the diff here 
shows its commits too; the delta is `fd712c9..HEAD`, six files.
   
   Hudi's metadata table is sorted by record key, and Hudi's readers seek into 
it: a reader that supports a key predicate is asked for an exact key set or a 
key prefix, and a full scan is the fallback for readers that do not. Full scan 
is allowed for the `files` partition only; column statistics, bloom filters and 
the indexes are point lookups by design, so this is not an optimisation for the 
metadata table, it is how the metadata table is meant to be read. Version two 
had row-filter pushdown for parquet, which skips rows while still reading every 
block, and nothing that seeks.
   
   The seek turns out to need no new I/O machinery. The ranged HFile reader 
added in #691 already fetches an arbitrary subset of data blocks in one call, 
keyed by index entry, so the seek is choosing which entries to pass it. Two 
functions select them from the block index, which is a sorted map from a 
block's first key to its entry, and the existing batched read fetches exactly 
those ranges.
   
   Correctness rests on the selection over-including rather than missing. A 
block's index key is a lower bound on its first real key, since the writer may 
shorten it, so taking the last entry at or below a wanted key can return a 
block that turns out not to hold it. That costs one block read. The synchronous 
path already names the same case when it lands before a block's first real key. 
The caller filters the records the over-included blocks bring back.
   
   One case needs the block before the selected one as well. Row keys are not 
required to be unique, and when a key's copies straddle a block boundary the 
writer's midpoint falls back to the right hand cell, so the separator equals 
the key itself; selecting only the entry at or below it would take the later 
block and drop the copies in the earlier one. So an exact hit on a separator 
takes the preceding block too, which is one extra block read against silently 
losing rows. No fixture here straddles, so that rule has a test of its own 
rather than relying on one noticing.
   
   The predicate rides on the base file read options beside the parquet row 
filter, and the same rule applies: the HFile reader honours it, other formats 
ignore it and return every row.
   
   A byte and call counter is added to the ranged fetcher, because the claim 
being made is about what is read rather than about what is returned, and there 
was no way to observe it. Bytes is the number to judge a narrowed read on, and 
it is exact on a backend that reads each range as asked, which the local 
filesystem store does. It is not exact everywhere, and the reason matters more 
than the counter: the default ranged read coalesces ranges closer together than 
a megabyte into a single request and slices the result, so on the cloud 
backends the counter reports what was asked for and the transfer can be larger. 
**Selecting blocks therefore only reduces transfer on those backends when the 
selected blocks sit more than that threshold apart.** None of the fixtures here 
are large enough for that to bite, which is exactly why it is written down 
rather than measured. Calls counts calls into the object store API and is named 
for what it can observe, since coalescing and any fan-out beneath 
 it are invisible from the fetcher: a full scan of a thousand adjacent blocks 
reports one call and a scattered three block seek also reports one.
   
   Building a probe key needed one fix in passing. A key's ordering compares 
its content, which sits after a two byte length prefix, so constructing one 
from raw key bytes reads the first two characters as a length and compares the 
rest as content. A constructor that prepends the prefix is added. The same 
construction exists in the synchronous seek, where it is masked by that path 
walking forward or backward from wherever it lands; that is left alone here and 
tracked separately.
   
   ## How are the changes test-covered
   
   - [ ] N/A
   - [x] Automated tests (unit and/or integration tests)
   - [ ] Manual tests
     - [ ] Details are described below
   
   Measured, not asserted by row count. On the deep index fixture a three key 
seek reads 418 bytes where the full scan reads 211,524, on the shortened index 
key fixture 2,873 against 110,363, and on the least favourable 49,089 against 
295,734. Every wanted key is still returned in all three.
   
   The property the whole design rests on gets the exhaustive case, over every 
fixture rather than one: roughly seventy nine thousand keys across nine files 
spanning one, two and three index levels, each key selected for individually, 
each having to land in a block that holds it. A three key sample cannot rule 
out an off by one bound, and the multi level index walk is a second population 
path for the block index and the one most likely to break the invariant, so 
covering only a single level fixture would have covered the easier half.
   
   Ten tests in all, and every rule has a mutation that fails the test naming 
it: selection returning every block, the range walked the wrong way, the probe 
key built without its length prefix, the prefix selection forgetting the block 
holding its lower bound, the boundary rule removed, selection widened by a 
block per key, the reader seeking without filtering, the predicate dropped 
between the options and the decode, and the count only projection answering 
from the trailer again.
   
   One mutation is **not** caught, and is filed rather than left implied: 
making the reader ignore the predicate entirely still passes. No fixture here 
can catch it. Every multi-block HFile in the repository is a raw HBase file 
with no Avro schema, which the base file reader refuses outright, and every 
HFile carrying an Avro schema is a metadata file with exactly one data block. 
With one block, seeking and filtering are indistinguishable in rows and in 
bytes however the test is written. So the seek is proven at the decoder and the 
wiring is proven at the reader, and their join needs a fixture that does not 
exist yet.
   
   Run locally on both legs: default features and `--no-default-features`, 
clippy clean on both with warnings denied, fmt clean, and `cargo check 
--workspace --all-targets --all-features` clean including the Python and C++ 
bindings. No CI run has happened: fork pull requests sit at `action_required` 
until a committer approves them, so all of the above is local only.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to