docs(tech-specs): update the delete block format for Avro serde - #19730
docs(tech-specs): update the delete block format for Avro serde#19730deepakpanda93 wants to merge 2 commits into
Conversation
Addresses apache#16138 (JIRA HUDI-6616). HUDI-5760 replaced Kryo with Avro as the serde for delete log blocks, but the tech spec still described the old format. The Delete Block section claimed "The following 3 fields are serialized using the KryoSerializer". HoodieDeleteBlock now dispatches on the log block version: version 1 is a Kryo-serialized HoodieKey[] carrying only key and partition path, version 2 is a Kryo-serialized DeleteRecord[], and version 3, which is what current writers emit since HoodieLogBlock.version is 3, is an Avro binary-encoded HoodieDeleteRecordList. Older versions stay readable, so the section documents all three rather than only the current one. Adds the version 3 record fields from HoodieDeleteRecordList.avsc: recordKey and partitionPath as nullable strings, and orderingVal as a union of typed wrappers (BooleanWrapper, IntWrapper, LongWrapper, FloatWrapper, DoubleWrapper, BytesWrapper, StringWrapper, DateWrapper, DecimalWrapper, TimeMicrosWrapper, TimestampMicrosWrapper, ArrayWrapper). The typed ordering value is the substantive gain over Kryo, alongside the block no longer requiring a JVM with matching Kryo registrations to read. Cross-references the RECORD_POSITIONS header, which applies to delete blocks and lets a reader apply deletes positionally rather than by key lookup. Corrects byte widths in two block tables. Both the Delete Block length field and the Avro Block record length field were documented as 8 bytes, but HoodieDeleteBlock#getContentBytes and HoodieAvroDataBlock#serializeRecords write them with output.writeInt, so both are 4. This is not a confusion with the outer log block, which does have an 8 byte content length; these tables describe the bytes inside the block content. The deprecated HoodieAvroDataBlock#getBytes path is not what current writers use. Renames the "format version" row in both tables to "block version". The value written is HoodieLogBlock.version, the log block version, not the log file format version, which is a separate concept documented under Versioning as currently 1. The other items requested on the issue, positional headers and block identifier headers, are already documented: RECORD_POSITIONS (5), BLOCK_IDENTIFIER (6), IS_PARTIAL (7) and BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS (8) are all present in the Headers table, so only the block tables needed changing. npm run build passes with the warning set byte-identical to a baseline built at the same base commit.
hudi-agent
left a comment
There was a problem hiding this comment.
Thanks for the docs update! This change corrects the delete-block tech spec to reflect the Kryo→Avro serde migration (HUDI-5760), documents all three version-dispatched encodings, adds the version-3 record field table from HoodieDeleteRecordList.avsc, and fixes two byte-width errors (8→4). I verified the byte widths, version dispatch, schema field names, the full ordering-value wrapper list, and the current block version against the source in hudi-common — all check out, and the "format version"→"block version" relabel is itself a correctness improvement. Please route to a Hudi committer or PMC member (e.g. @yihua) for a final pass on the storage-format wording.
cc @yihua
| | `orderingVal` | nullable union of typed wrappers | Ordering value used to resolve merge order against other writes to the same key, encoded with the wrapper matching the value's own type: `BooleanWrapper`, `IntWrapper`, `LongWrapper`, `FloatWrapper`, `DoubleWrapper`, `BytesWrapper`, `StringWrapper`, `DateWrapper`, `DecimalWrapper`, `TimeMicrosWrapper`, `TimestampMicrosWrapper` or `ArrayWrapper`. | | ||
|
|
||
| A delete block may also carry the [`RECORD_POSITIONS`](#headers) header. When it does, the reader can apply the deletes | ||
| positionally against the base file named by `BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS` instead of resolving each key. |
There was a problem hiding this comment.
🤖 This positional-delete note is accurate and a nice addition. One small clarification: BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS holds an instant time, not a base-file path, so "the base file named by" reads a bit loosely — the reader resolves the base file within the file slice by matching that instant time. Rewording to something like "the base file identified by the instant time in BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS" might help readers.
There was a problem hiding this comment.
Good catch, that reads wrong. Fixed in 62a96e9.
You are right that the header holds an instant time. HoodieAppendHandle writes baseInstantTimeForPositions into it as a String:
if (baseInstantTimeForPositions.isPresent()) {
updatedHeader.put(
HeaderMetadataType.BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS,
baseInstantTimeForPositions.get());
}and the reader gets it back through HoodieLogBlock#getBaseFileInstantTimeOfPositions, whose javadoc is explicit that it returns the "base file instant time of the record positions". Nothing in the block carries a path, so "named by" was misleading about what the reader actually has to work with.
What tipped it from a wording nit to something worth fixing is that my sentence was also inconsistent with the rest of the same page. The Headers table already describes this header as the "Begin (requested) instant time of the base file that the RECORD_POSITIONS bitmap is relative to", and the log-merge section already says "the base file identified by BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS". I had introduced a third phrasing for the same thing, in a spec where consistency is most of the value.
It now reads:
A delete block may also carry the
RECORD_POSITIONSheader. When it does, the reader can apply the deletes positionally instead of resolving each key, against the base file identified by the instant time inBASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS. The reader matches that instant time against the base file of the file slice, which is also how it validates that the positions still refer to the base file the bitmap was built against.
I added that last clause because it answers the question your comment implicitly raises: why carry an instant time rather than something more direct. The Headers table already gives the reason ("Used by the reader to validate that positions still refer to the same base file"), so this connects the delete block note to it instead of leaving the reader to infer the purpose.
Build passes with the warning block still byte-identical to a baseline built at the same base commit, and the rendered page no longer contains "named by".
Thanks also for verifying the byte widths, the version dispatch, the schema field names, the full wrapper list and the current block version against hudi-common. Agreed on routing to a committer. @yihua, the two things I would most value a committer's eye on are the v1 claim that those blocks carry no ordering value at all, and whether the "block version" relabel is the terminology the project wants in the spec, given the log file format version is separately documented as 1 on the same page.
Review feedback on apache#19730. The positional-delete note read "the base file named by BASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS", which reads as though the header holds a file name or path. It holds an instant time. HoodieAppendHandle writes baseInstantTimeForPositions into the header as a String, and the reader gets it back through HoodieLogBlock#getBaseFileInstantTimeOfPositions, so the base file is resolved by matching that instant time within the file slice rather than by any path stored in the block. The wording was also inconsistent with the rest of the page: the Headers table already describes this header as the "Begin (requested) instant time of the base file", and the log-merge section already says "the base file identified by". This aligns the delete block note with both, and adds the reason the instant time is carried at all, which is to let the reader confirm the positions still refer to the base file the bitmap was built against. Build passes with the warning set still byte-identical to a baseline at the same base commit.
hudi-agent
left a comment
There was a problem hiding this comment.
Thanks for the docs update! This revision corrects the Delete Block section to reflect the Kryo→Avro serde change (HUDI-5760), documents all three versioned payload encodings, adds the version-3 HoodieDeleteRecord field table, and fixes the length byte-widths (8→4) in both the Delete Block and Avro Block tables. I verified each claim against HoodieDeleteBlock, HoodieAvroDataBlock, HoodieDeleteRecordList.avsc, and SerializationUtils, and they all match the source. No correctness or architecture concerns remain from this round; please route to a Hudi committer or PMC member for final sign-off.
cc @yihua
Describe the issue this Pull Request addresses
Addresses #16138 (JIRA HUDI-6616).
HUDI-5760 replaced Kryo with Avro as the serde for delete log blocks, but the tech spec was never updated. The Delete
Block (Id: 1) section still told readers:
That has not been true since HUDI-5760 landed.
The issue also asks (@vinothchandar's comment) for positional headers, block uuid headers and "other changes". Those have
since been documented independently:
RECORD_POSITIONS(5),BLOCK_IDENTIFIER(6),IS_PARTIAL(7),COMPACTED_BLOCK_TIMES(4) andBASE_FILE_INSTANT_TIME_OF_RECORD_POSITIONS(8) are all present in the Headers tabletoday. I checked each rather than assuming, so this PR is scoped to what actually remains: the block content tables.
Summary and Changelog
One file,
website/learn/tech-specs.md(+32/−5).learn/is an unversioned Docusaurus plugin, so there is a single copyand no versioned duplicates.
1. Delete Block: the payload encoding is versioned, not simply Kryo.
HoodieDeleteBlockdispatches on the log blockversion, and older blocks stay readable, so the section now documents all three encodings rather than only the current
one:
HoodieKey[]DeleteRecord[]HoodieDeleteRecordListVersion 3 is what current writers emit, since
HoodieLogBlock.versionis3. The v1 row matters in practice: a readerencountering a v1 block gets no ordering value at all, which the old text gave no way to anticipate.
2. The version 3 record fields, from
HoodieDeleteRecordList.avsc:recordKeyandpartitionPathas nullablestrings, and
orderingValas a union of typed wrappers (BooleanWrapper,IntWrapper,LongWrapper,FloatWrapper,DoubleWrapper,BytesWrapper,StringWrapper,DateWrapper,DecimalWrapper,TimeMicrosWrapper,TimestampMicrosWrapper,ArrayWrapper). That typed ordering value is the substantive gain over Kryo, alongside theblock no longer needing a JVM with matching Kryo registrations to read. The schema's own doc string confirms the pairing:
"A list of delete records stored in the delete block in log block version 3".
3. Byte widths corrected in two tables. Both the Delete Block
lengthfield and the Avro Blockrecord lengthfieldwere documented as 8 bytes.
HoodieDeleteBlock#getContentBytesandHoodieAvroDataBlock#serializeRecordsboth writethem with
output.writeInt, so both are 4:Worth being explicit that this is not a confusion with the outer log block, which genuinely does have an 8-byte
content lengthfield. These tables describe the bytes inside the block content. I also checked thatHoodieAvroDataBlockhas two serialization paths and that the 8-byte reading does not come from the other one: the@Deprecated getBytes(Schema)method is a legacy writer that compresses and writes the schema, and is not what currentwriters use.
4.
format versionrenamed toblock versionin both tables. The value written isHoodieLogBlock.version,currently
3, which is the log block version. The log file format version is a separate concept that theVersioning section of this same page documents as currently
1, so labelling this row "version of the log file format"conflated the two. After this change no block table row says "format version"; the remaining occurrences on the page are
unrelated prose about
hoodie.table.versionand the Versioning section.Scope note
The Avro Block correction is slightly wider than issue #16138, which is about the delete block. I included it for two
reasons: it is the same class of error verified the same way, and renaming the Delete Block's
format versionrow wouldotherwise have left the two adjacent tables describing the identical field under different names. Happy to split it out
if reviewers would rather keep this strictly to the delete block.
Verification
Every claim was read out of
release-1.2.0source rather than inferred:HoodieDeleteBlock,HoodieDeleteRecordList.avsc,HoodieLogBlock(forversion = 3),SerializationUtils(confirming v1/v2 areKryo-backed via
KryoSerializerInstance),DeleteRecord, andHoodieAvroDataBlock(both the currentserializeRecordsand the deprecatedgetBytes).npm run buildpasses with the warning block byte-identical to a baseline built from the same base commit(
5971a1ac3ba3), 13,265 lines each. The branch is rebased on that head, so the workflow's changed-file check sees onlythe one
website/file. Rendering confirmed undernpm run serve: both tables render with the corrected widths, theKryo sentence is gone, all twelve wrapper types appear, and the new
RECORD_POSITIONScross-reference resolves to#headers.One limitation worth stating: this is verified by reading the format code, not by decoding a delete block off disk at each
version. A committer who can confirm the v1/v2/v3 dispatch against real log files would strengthen it, particularly the
claim that v1 blocks carry no ordering value.
Impact
Documentation only. No code, config, or behaviour change. It does correct statements that are currently wrong about both
the delete block encoding and two field widths.
Risk Level
none
Documentation Update
This PR is the documentation update — the tech spec, https://hudi.apache.org/learn/tech-specs.
Contributor's checklist
cc @vinothchandar (who raised the original list on the issue), @yihua