Skip to content

fix(metadata): use committed Spark write statuses after retries - #19801

Open
chrevanthreddy wants to merge 3 commits into
apache:masterfrom
chrevanthreddy:fix/rli-streaming-write-status-retry
Open

fix(metadata): use committed Spark write statuses after retries#19801
chrevanthreddy wants to merge 3 commits into
apache:masterfrom
chrevanthreddy:fix/rli-streaming-write-status-retry

Conversation

@chrevanthreddy

@chrevanthreddy chrevanthreddy commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

Spark can produce a data-table WriteStatus from a task attempt that later fails or loses to a retry/speculative attempt. The streaming metadata path previously derived RLI/SI records from that raw pre-shuffle lineage, allowing a losing attempt's stale file location to leak into committed metadata.

This is a core metadata-index correctness issue discovered while validating RFC-109, but the fix is independent of vector indexing.

Summary and Changelog

Spark streaming metadata writes now place the data-table statuses behind the existing coalescing shuffle before deriving RLI/SI metadata records.

  • Build scheduler-selected data statuses at the coalescing shuffle boundary.
  • Feed that successful-attempt lineage into the existing streaming RLI/SI mappers.
  • Preserve the existing metadata-status persistence and data/metadata union.
  • Add a regression that forces partition 0 attempt 0 to fail under local[8,2] and verifies both collected data output and metadata input contain only attempt 1.
  • Add coverage that metadata statuses remain unioned into the returned DAG.

No code was copied.

Impact

Correctness fix for Spark writers using streaming metadata writes, particularly RLI and SI under task retry/speculation. No storage-format, public API, timeline-protocol, metadata-schema, or configuration change.

Risk Level

medium — this moves the existing coalescing shuffle ahead of streaming metadata generation. Mitigation:

  • JDK 17, Spark 4.0 / Scala 2.13 targeted reactor: 8 tests passed, 0 failed.
  • Forced Spark retry regression verifies metadata consumes only scheduler-selected output.
  • Existing streaming metadata mappers and commit-completion flow are retained.
  • git diff --check passes.

Documentation Update

none

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:M PR with lines of changes in (100, 300] label Aug 31, 2026
@codecov-commenter

codecov-commenter commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.23%. Comparing base (886ebc7) to head (7b7487c).

Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19801      +/-   ##
============================================
- Coverage     78.29%   78.23%   -0.06%     
+ Complexity    33850    33826      -24     
============================================
  Files          2541     2541              
  Lines        141674   141665       -9     
  Branches      17172    17172              
============================================
- Hits         110917   110835      -82     
- Misses        23048    23111      +63     
- Partials       7709     7719      +10     
Components Coverage Δ
hudi-common 83.62% <ø> (-0.01%) ⬇️
hudi-client 82.93% <ø> (-0.24%) ⬇️
hudi-flink 85.66% <ø> (ø)
hudi-spark-datasource 73.18% <ø> (-0.01%) ⬇️
hudi-utilities 74.56% <ø> (+0.05%) ⬆️
hudi-cli 15.06% <ø> (ø)
hudi-hadoop 70.72% <ø> (+0.01%) ⬆️
hudi-sync 75.54% <ø> (-0.03%) ⬇️
hudi-io 79.98% <ø> (ø)
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 65.81% <ø> (ø)
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
flink-integration-tests 48.82% <ø> (ø)
hadoop-mr-java-client 44.09% <ø> (-0.01%) ⬇️

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

Files with missing lines Coverage Δ
...udi/client/SparkStreamingMetadataWriteHandler.java 100.00% <ø> (ø)

... and 39 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.
@hudi-bot

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! The PR moves the coalescing shuffle ahead of streaming metadata generation so RLI/SI records are derived from the scheduler-selected data-table write statuses rather than raw pre-shuffle lineage. A few things are worth resolving before this goes further: the reworked method references an undefined variable (won't compile), and routing metadata derivation through the coalesced RDD looks like it collapses derivation parallelism at the default divisor — details in the inline comments, plus a note on whether the new regression test actually exercises the bug. Please take a look at any inline comments, and this should be ready for a Hudi committer or PMC member to take it from here.

// Materialize data-table statuses behind a shuffle before deriving metadata records. Spark's
// scheduler exposes only the successful attempt for each shuffle partition, so failed or losing
// speculative attempts cannot contribute stale RLI/SI locations to the metadata write.
HoodieData<WriteStatus> successfulDataWriteStatuses = HoodieJavaRDD.of(

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 won't compile as written — the local is declared successfulDataWriteStatuses here, but it's referenced as committedDataWriteStatuses on lines 68 and 71, which is never defined. Looks like a rename that got left half-applied; could you reconcile the two names?

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

return coalescedDataWriteStatuses.union(mdtWriteStatuses);

HoodieData<WriteStatus> metadataWriteStatuses =
metadataWriter.streamWriteToMetadataPartitions(committedDataWriteStatuses, instantTime);

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.

🤖 Deriving metadata from the coalesced RDD ties RLI/SI generation to coalesceParallelism = max(1, numPartitions/coalesceDivisor). With the default divisor of 5000 that's 1 partition for nearly all writes, so the whole writeStatus.flatMap(indexMapper) derivation is funneled through a single reducer, whereas before it ran at full data-write parallelism. For large writes that looks like a serious throughput/OOM regression. @nsivabalan @yihua is the metadata derivation intended to run at coalesced parallelism? Could we instead persist the pre-coalesce statuses (so both branches consume the same committed output) and coalesce only the union branch, keeping derivation parallelism intact?

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

assertTrue(paths.stream().noneMatch(path -> path.equals("file-0-attempt-0")));
assertTrue(metadataPaths.contains("file-0-attempt-1"),
"Metadata generation must consume the successful attempt");
assertTrue(metadataPaths.stream().noneMatch(path -> path.equals("file-0-attempt-0")));

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.

🤖 Since attempt 0 throws before returning a WriteStatus, file-0-attempt-0 is never emitted by any attempt, so the noneMatch("file-0-attempt-0") checks hold trivially — this test would pass on master too and doesn't actually exercise the stale-lineage path. The bug you describe is a losing/slower speculative attempt that does emit output; could the test be shaped to reproduce that so it genuinely guards the regression?

⚠️ 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:M PR with lines of changes in (100, 300]

4 participants