Skip to content

feat(spark): support clustering for LSM tables - #19767

Open
cshuo wants to merge 2 commits into
apache:masterfrom
cshuo:spark-cluster-lsm
Open

feat(spark): support clustering for LSM tables#19767
cshuo wants to merge 2 commits into
apache:masterfrom
cshuo:spark-cluster-lsm

Conversation

@cshuo

@cshuo cshuo commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Describe the issue this Pull Request addresses

Closes #19440.

Spark clustering and bucket rescale previously could not safely operate on LSM-layout tables. These operations must preserve the LSM sorted-run invariant: every output file has to remain ordered by record key using UTF-8 byte ordering.

Summary and Changelog

  • Enable built-in Spark sort-and-size and single-file clustering for COW and MOR LSM tables by reading input file groups through the LSM reader and reusing the existing LSM-aware Row/RDD partitioners.
  • Support consistent-hashing bucket split and merge while preserving bucket routing, fixed file IDs, replacement metadata, and record-key ordering.
  • Extract the existing LSM loser-tree implementation into a reusable common utility and add a thin merge iterator for k-way merging sorted bucket inputs.
  • Make simple-bucket Row/RDD bulk insert and partition-bucket rescale sort records by UTF-8 record-key order without changing bucket routing.
  • Disable binary/stream-copy optimization for LSM tables and fall back to an ordered rewrite.
  • Reject custom sort columns, spatial layout optimization, and unverified custom execution strategies before a clustering instant transitions to inflight.
  • Add unit and functional coverage for UTF-8 ordering, resource cleanup, COW/MOR clustering, consistent-hashing split/merge, copy fallback, early validation, bucket routing, and replace-commit metadata.

Impact

Spark users can run supported clustering and bucket-rescale operations on LSM-layout tables while retaining the ordering invariant required by LSM readers. No configuration defaults or storage format are changed. Unsupported ordering strategies fail early, while binary-copy strategies transparently use the regular ordered rewrite path.

Risk Level

medium. The change affects Spark clustering read/write paths and bucket routing. It is covered by targeted common and Spark-client unit tests plus COW/MOR functional tests that verify sorted output, snapshot correctness, split/merge routing, file IDs, replacement metadata, and subsequent upserts.

Documentation Update

None. This enables existing clustering and bucket-rescale interfaces for LSM tables and adds no new configuration.

Contributor's checklist

  • Read through contributor's guide
  • Enough context is provided in the sections above
  • Adequate tests were added if applicable
@github-actions github-actions Bot added the size:XL PR with lines of changes > 1000 label Aug 27, 2026

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR enables Spark sort-and-size, single-file, and consistent-hashing bucket rescale clustering on LSM-layout tables while preserving the UTF-8 record-key sorted-run invariant: reads are routed through the LSM file-group reader, LSM-aware Row/RDD partitioners are reused, the loser-tree is extracted into a reusable k-way merge utility, and unsupported ordering strategies are rejected before a clustering instant transitions to inflight. No correctness issues found. A few style/readability suggestions in the inline comments. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here. A couple of naming and readability suggestions below.

cc @yihua

override def numPartitions: Int = partitioner.numPartitions

override def getPartition(key: Any): Int = {
val bucketRoute = key.asInstanceOf[((String, Int), HoodieUTF8String)]._1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: have you considered a case match instead of asInstanceOf here? Something like case ((route, _), _: HoodieUTF8String) => partitioner.getPartition(route) would make the expected tuple shape self-documenting and give a clear MatchError rather than a ClassCastException deep in the shuffle path if the key type ever changes.

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

@codecov-commenter

codecov-commenter commented Aug 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.93496% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 78.32%. Comparing base (886ebc7) to head (f487a60).

Files with missing lines Patch % Lines
...in/java/org/apache/hudi/common/util/LoserTree.java 96.10% 2 Missing and 1 partial ⚠️
...common/util/collection/LoserTreeMergeIterator.java 90.47% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19767      +/-   ##
============================================
+ Coverage     78.29%   78.32%   +0.03%     
- Complexity    33850    33897      +47     
============================================
  Files          2541     2543       +2     
  Lines        141674   141795     +121     
  Branches      17172    17191      +19     
============================================
+ Hits         110917   111063     +146     
+ Misses        23048    23024      -24     
+ Partials       7709     7708       -1     
Components Coverage Δ
hudi-common 83.67% <94.89%> (+0.04%) ⬆️
hudi-client 83.20% <100.00%> (+0.03%) ⬆️
hudi-flink 85.72% <ø> (+0.06%) ⬆️
hudi-spark-datasource 73.18% <ø> (-0.01%) ⬇️
hudi-utilities 74.57% <ø> (+0.05%) ⬆️
hudi-cli 15.06% <ø> (ø)
hudi-hadoop 70.72% <ø> (+0.01%) ⬆️
hudi-sync 75.56% <ø> (ø)
hudi-io 79.98% <ø> (ø)
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 65.81% <ø> (ø)
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 51.48% <70.73%> (+0.02%) ⬆️
flink-integration-tests 48.82% <0.00%> (-0.01%) ⬇️
hadoop-mr-java-client 44.02% <0.00%> (-0.07%) ⬇️
integration-tests 13.50% <0.00%> (-0.02%) ⬇️
spark-client-hadoop-common 50.43% <0.00%> (-0.08%) ⬇️
spark-java-tests 52.27% <95.93%> (+0.06%) ⬆️
spark-scala-tests 46.81% <0.81%> (-0.05%) ⬇️
utilities 36.52% <0.81%> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...e/action/cluster/ClusteringPlanActionExecutor.java 100.00% <100.00%> (ø)
.../cluster/strategy/ClusteringExecutionStrategy.java 95.31% <100.00%> (+1.19%) ⬆️
...un/strategy/MultipleSparkJobExecutionStrategy.java 96.22% <100.00%> (ø)
...gleSparkJobConsistentHashingExecutionStrategy.java 91.56% <100.00%> (+0.20%) ⬆️
...gy/SparkBinaryCopyClusteringExecutionStrategy.java 91.17% <100.00%> (+0.40%) ⬆️
...gy/SparkStreamCopyClusteringExecutionStrategy.java 100.00% <100.00%> (ø)
...common/util/collection/LoserTreeMergeIterator.java 90.47% <90.47%> (ø)
...in/java/org/apache/hudi/common/util/LoserTree.java 96.10% <96.10%> (ø)

... and 17 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
@cshuo
cshuo force-pushed the spark-cluster-lsm branch 2 times, most recently from dcfc6f5 to f211eb2 Compare August 31, 2026 01:46
@cshuo
cshuo requested a review from danny0405 August 31, 2026 01:57
@cshuo
cshuo force-pushed the spark-cluster-lsm branch from 1fec70e to 63b2590 Compare August 31, 2026 07:26
@github-actions github-actions Bot added size:L PR with lines of changes in (300, 1000] and removed size:XL PR with lines of changes > 1000 labels Aug 31, 2026
@cshuo
cshuo force-pushed the spark-cluster-lsm branch from ef33b75 to bda56fe Compare August 31, 2026 12:10
@cshuo
cshuo force-pushed the spark-cluster-lsm branch from bda56fe to f487a60 Compare September 1, 2026 01:55
@hudi-bot

hudi-bot commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

CI report:

Bot commands @hudi-bot supports the following commands:
  • @hudi-bot run azure re-run the last Azure build

@hudi-agent hudi-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ 🤖 This review was generated by an AI agent and may contain mistakes. Please verify any suggestions before applying.

Thanks for working on this! This PR enables Spark sort-and-size / single-file clustering and consistent-hashing bucket split/merge on LSM-layout tables while preserving the record-key (UTF-8) sort invariant — reading inputs through the LSM file-group reader, adding a reusable loser-tree k-way merge for the consistent-hashing merge path, and falling back from binary/stream copy. I traced the loser-tree state machine, the merge vs. split routing and ordering, resource cleanup on the read path, and the reader return-type change, and they look correct for the covered cases. No correctness issues found. A few style/readability suggestions in the inline comments. Please take a look, and this should be ready for a Hudi committer or PMC member to take it from here. A couple of naming nits where the method and variable names no longer match the return type after the refactor to HoodieRecordReader.

cc @yihua

@@ -142,10 +144,24 @@ protected FileSlice clusteringOperationToFileSlice(String basePath, ClusteringOp
return fileSlice;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: now that the return type was widened to HoodieRecordReader, the name getFileGroupReader is a bit misleading — could you rename it to something like getRecordReader to match what it actually returns?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.


// instantiate FG reader
HoodieFileGroupReader<InternalRow> fileGroupReader = getFileGroupReader(metaClient, fileSlice, tableSchemaWithMetaFields, internalSchemaOption,
HoodieRecordReader<InternalRow> fileGroupReader = getFileGroupReader(metaClient, fileSlice, tableSchemaWithMetaFields, internalSchemaOption,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 nit: the local variable fileGroupReader still carries the old name — would recordReader (or just reader) be more accurate now that its type is HoodieRecordReader?

⚠️ AI-generated; verify before applying. React 👍/👎 to flag quality.

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

Labels

size:L PR with lines of changes in (300, 1000]

4 participants