Skip to content

fix(core): reject a second INSERT_OVERWRITE of a partition already overwritten by a concurrent writer - #19720

Open
Davis-Zhang-Onehouse wants to merge 1 commit into
apache:masterfrom
Davis-Zhang-Onehouse:insert-overwrite-partition-conflict
Open

fix(core): reject a second INSERT_OVERWRITE of a partition already overwritten by a concurrent writer#19720
Davis-Zhang-Onehouse wants to merge 1 commit into
apache:masterfrom
Davis-Zhang-Onehouse:insert-overwrite-partition-conflict

Conversation

@Davis-Zhang-Onehouse

Copy link
Copy Markdown
Contributor

Describe the issue this Pull Request addresses

Under OCC, SimpleConcurrentFileWritesConflictResolutionStrategy.hasConflict only reports a conflict when two operations share a (partition, file id) pair. Two INSERT_OVERWRITE writers that both planned against an empty partition replace nothing and write fresh file ids, so both commit and every row of the partition is present twice; clean, clustering and compaction then keep both copies. INSERT_OVERWRITE has partition semantics but OCC only records it as file ids.

Summary and Changelog

Two overwrite operations (INSERT_OVERWRITE / INSERT_OVERWRITE_TABLE) that touch a common partition now conflict once the other one has completed, regardless of file ids. Scope, deliberately narrow:

  • only when the other overwrite is already completed (first to commit wins, as elsewhere in OCC). Matching pending overwrites too would make both writers fail each other and would let an abandoned pending overwrite block every later overwrite of that partition until lazy cleaning rolls it back;
  • only overwrite against overwrite. A plain insert that lands in a partition a concurrent overwrite completed on keeps today's file-level semantics.

Known conservative case: an INSERT_OVERWRITE of partition P that planned after a concurrent INSERT_OVERWRITE_TABLE completed is now rejected as well (the reverse ordering already was, via file ids). Not in this PR: BucketIndexConcurrentFileWritesConflictResolutionStrategy overrides hasConflict entirely and does not get this rule.

Changes:

  • SimpleConcurrentFileWritesConflictResolutionStrategy.hasConflict: partition-level check for a completed overwrite of a common partition.
  • TestSimpleConcurrentFileWritesConflictResolutionStrategy: predicate-level test (same partition / pending other / different partition / non-overwrite).
  • TestConcurrentInsertOverwriteSamePartition: two SparkRDDWriteClients overwriting the same empty partition, plus a populated-partition contrast.
  • TestConcurrentInsertOverwritePartition: two Spark SQL sessions running INSERT OVERWRITE ... PARTITION concurrently.

No code copied.

Impact

A second concurrent INSERT OVERWRITE of the same partition now fails with HoodieWriteConflictException instead of silently duplicating the partition's data. No API or config change. Inherited by strategies that subclass SimpleConcurrentFileWritesConflictResolutionStrategy without overriding hasConflict.

Risk Level

Low. Tests at three levels, all red on the old predicate (see Changelog). TestHoodieClientMultiWriter passes unchanged.

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

…erwritten by a concurrent writer

Under OCC, SimpleConcurrentFileWritesConflictResolutionStrategy only sees a conflict on shared file ids.
Two INSERT_OVERWRITE writers that both planned against an empty partition replace nothing and write
disjoint file ids, so both commit and every row in the partition is present twice; clean, clustering
and compaction then keep both copies. Treat two overwrite operations of a common partition as
conflicting once one of them has completed (first to commit wins, as elsewhere in OCC). Pending
overwrites keep file-id semantics so an abandoned pending overwrite cannot block the partition.

Tests, all red on the old predicate: the predicate itself
(TestSimpleConcurrentFileWritesConflictResolutionStrategy), two SparkRDDWriteClients
(TestConcurrentInsertOverwriteSamePartition), and two Spark SQL sessions running
INSERT OVERWRITE ... PARTITION concurrently (TestConcurrentInsertOverwritePartition).
@github-actions github-actions Bot added the size:L PR with lines of changes in (300, 1000] label Aug 24, 2026
@codecov-commenter

codecov-commenter commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 77.90%. Comparing base (2460f8b) to head (7a10426).

Files with missing lines Patch % Lines
...oncurrentFileWritesConflictResolutionStrategy.java 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##             master   #19720      +/-   ##
============================================
+ Coverage     77.88%   77.90%   +0.01%     
- Complexity    33219    33230      +11     
============================================
  Files          2533     2533              
  Lines        140218   140227       +9     
  Branches      16887    16891       +4     
============================================
+ Hits         109215   109240      +25     
+ Misses        23369    23355      -14     
+ Partials       7634     7632       -2     
Components Coverage Δ
hudi-common 83.36% <ø> (+<0.01%) ⬆️
hudi-client 82.94% <88.88%> (+0.01%) ⬆️
hudi-flink 85.77% <ø> (+0.02%) ⬆️
hudi-spark-datasource 72.34% <ø> (ø)
hudi-utilities 74.33% <ø> (+0.04%) ⬆️
hudi-cli 15.06% <ø> (ø)
hudi-hadoop 69.09% <ø> (+0.01%) ⬆️
hudi-sync 75.58% <ø> (ø)
hudi-io 79.85% <ø> (+0.09%) ⬆️
hudi-timeline-service 83.44% <ø> (ø)
hudi-cloud 64.33% <ø> (ø)
hudi-kafka-connect 53.20% <ø> (ø)
Flag Coverage Δ
common-and-other-modules 50.97% <88.88%> (+<0.01%) ⬆️
flink-integration-tests 49.13% <0.00%> (-0.01%) ⬇️
hadoop-mr-java-client 43.89% <0.00%> (+0.05%) ⬆️
integration-tests 13.61% <0.00%> (+<0.01%) ⬆️
spark-client-hadoop-common 50.75% <66.66%> (+0.19%) ⬆️
spark-java-tests 51.96% <11.11%> (-0.02%) ⬇️
spark-scala-tests 46.61% <55.55%> (+0.12%) ⬆️
utilities 36.55% <0.00%> (+<0.01%) ⬆️

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

Files with missing lines Coverage Δ
...oncurrentFileWritesConflictResolutionStrategy.java 92.22% <88.88%> (-0.38%) ⬇️

... and 12 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 the contribution! This PR closes a real OCC gap where two concurrent INSERT_OVERWRITE / INSERT_OVERWRITE_TABLE writers that both plan against an empty partition write fresh file ids and both commit, duplicating the partition; it adds a partition-level conflict check once one overwrite has completed. I traced the argument convention in TransactionUtils, the mutated-partition derivation in ConcurrentOperation.init() (which includes written write-stat partitions, so empty-partition overwrites are still detected), the asymmetric completed-only guard, and the additive placement after the file-id check — the logic holds up and the scoping (pending-skip, overwrite-vs-overwrite only) is deliberate and tested. 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 small readability nits in the new test files.

cc @yihua


HoodieTableMetaClient metaClient = HoodieTestUtils.createMetaClient(storageConf, basePath);
HoodieTimeline replaceTimeline = metaClient.getActiveTimeline().getCompletedReplaceTimeline();
assertEquals(Arrays.asList(firstInstant),

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: Collections.singletonList(firstInstant) would be slightly more idiomatic here than Arrays.asList with a single element — makes the intent (exactly one element) immediately clear.

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

s"select count(distinct rank_source_id, locode, device, normalized_query, serp_item_position) from $tableName where day = '$targetPartition'")
.head().getLong(0)

println(s"overwrites committed=${completedOverwrites.map { case (i, md) => s"$i replaced=${md.getPartitionToReplaceFileIds}" }} " +

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: could you swap println for a logger (e.g. log.info(...))? Raw println gets swallowed by test frameworks in CI and mixed with unrelated output, making it harder to trace a failure.

⚠️ 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]

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants