This is an automated email from the ASF dual-hosted git repository.
voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git
The following commit(s) were added to refs/heads/master by this push:
new 59e62dd488cd fix: Improve error message for conflict resolution
(#18119)
59e62dd488cd is described below
commit 59e62dd488cd964c3125e9cb96d0089f9148c0be
Author: Vinish Reddy <[email protected]>
AuthorDate: Wed Jul 15 12:12:54 2026 +0530
fix: Improve error message for conflict resolution (#18119)
* Improve error message for conflict resolution
* Fix build
* Address comments
---
...urrentFileWritesConflictResolutionStrategy.java | 62 +++++++-
...urrentFileWritesConflictResolutionStrategy.java | 171 +++++++++++++++++++++
.../hudi/common/model/WriteOperationType.java | 11 ++
3 files changed, 242 insertions(+), 2 deletions(-)
diff --git
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/SimpleConcurrentFileWritesConflictResolutionStrategy.java
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/SimpleConcurrentFileWritesConflictResolutionStrategy.java
index 0d6327272a19..92c6f6f66ba5 100644
---
a/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/SimpleConcurrentFileWritesConflictResolutionStrategy.java
+++
b/hudi-client/hudi-client-common/src/main/java/org/apache/hudi/client/transaction/SimpleConcurrentFileWritesConflictResolutionStrategy.java
@@ -200,8 +200,66 @@ public class
SimpleConcurrentFileWritesConflictResolutionStrategy
return thisOperation.getCommitMetadataOption();
}
// just abort the current write if conflicts are found (failed for
rollback conflicts).
- throw new HoodieWriteConflictException(new
ConcurrentModificationException("Cannot resolve conflicts for overlapping
writes between first operation = " + thisOperation
- + ", second operation = " + otherOperation));
+ throw new HoodieWriteConflictException(new
ConcurrentModificationException(buildConflictErrorMessage(thisOperation,
otherOperation)));
+ }
+
+ /**
+ * Builds a detailed error message for write conflicts based on the
operation types involved.
+ */
+ private String buildConflictErrorMessage(ConcurrentOperation thisOperation,
ConcurrentOperation otherOperation) {
+ boolean thisIsTableService =
WriteOperationType.isTableService(thisOperation.getOperationType());
+ boolean otherIsTableService =
WriteOperationType.isTableService(otherOperation.getOperationType());
+ String thisOperationDescription =
formatOperationDescription(thisOperation);
+ String otherOperationDescription =
formatOperationDescription(otherOperation);
+ // If either operation is a table service, provide specific retry guidance
+ if (thisIsTableService || otherIsTableService) {
+ ConcurrentOperation tableServiceOperation = thisIsTableService ?
thisOperation : otherOperation;
+ String tableServiceDescription = thisIsTableService ?
thisOperationDescription : otherOperationDescription;
+ String regularOperationDescription = thisIsTableService ?
otherOperationDescription : thisOperationDescription;
+ String serviceType =
getTableServiceDisplayName(tableServiceOperation.getOperationType());
+ return String.format(
+ "Cannot resolve conflicts for overlapping writes. %s is currently
running and has overlapping file groups with %s. "
+ + "Please retry the write operation after the %s completes.",
+ tableServiceDescription, regularOperationDescription,
serviceType.toLowerCase()
+ );
+ }
+ // For regular write operations conflicting with each other
+ return String.format(
+ "Cannot resolve conflicts for overlapping writes. %s has overlapping
file groups with %s.",
+ thisOperationDescription, otherOperationDescription
+ );
+ }
+
+ /**
+ * Formats a description of an operation including its type, instant, and
state.
+ */
+ private String formatOperationDescription(ConcurrentOperation operation) {
+ String operationName =
WriteOperationType.isTableService(operation.getOperationType())
+ ? "Table " + getTableServiceDisplayName(operation.getOperationType())
+ : operation.getOperationType().value() + " operation";
+
+ return String.format("%s (instant: %s, state: %s)",
+ operationName,
+ operation.getInstantTimestamp(),
+ operation.getInstantActionState());
+ }
+
+ /**
+ * Returns a user-friendly display name for table service operations.
+ */
+ private String getTableServiceDisplayName(WriteOperationType operationType) {
+ switch (operationType) {
+ case COMPACT:
+ return "Compaction";
+ case CLUSTER:
+ return "Clustering";
+ case LOG_COMPACT:
+ return "Log Compaction";
+ case INDEX:
+ return "Indexing";
+ default:
+ return operationType.value();
+ }
}
@Override
diff --git
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/TestSimpleConcurrentFileWritesConflictResolutionStrategy.java
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/TestSimpleConcurrentFileWritesConflictResolutionStrategy.java
index d68d6e50f1d1..04fecce34e51 100644
---
a/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/TestSimpleConcurrentFileWritesConflictResolutionStrategy.java
+++
b/hudi-client/hudi-client-common/src/test/java/org/apache/hudi/client/transaction/TestSimpleConcurrentFileWritesConflictResolutionStrategy.java
@@ -514,4 +514,175 @@ public class
TestSimpleConcurrentFileWritesConflictResolutionStrategy extends Ho
}
}
}
+
+ @Test
+ public void testErrorMessageForConflictWithCompaction() throws Exception {
+ initMetaClient(true, HoodieTableType.MERGE_ON_READ);
+ createCommit(WriteClientTestUtils.createNewInstantTime(), metaClient);
+ HoodieActiveTimeline timeline = metaClient.getActiveTimeline();
+ Option<HoodieInstant> lastSuccessfulInstant =
timeline.getCommitsTimeline().filterCompletedInstants().lastInstant();
+
+ // writer 1 starts
+ String currentWriterInstant = WriteClientTestUtils.createNewInstantTime();
+ createInflightCommit(currentWriterInstant, metaClient);
+
+ // compaction gets scheduled and runs
+ String compactionInstant = WriteClientTestUtils.createNewInstantTime();
+ createCompactionRequested(compactionInstant, metaClient);
+
+ Option<HoodieInstant> currentInstant =
Option.of(INSTANT_GENERATOR.createNewInstant(State.INFLIGHT,
HoodieTimeline.COMMIT_ACTION, currentWriterInstant));
+ SimpleConcurrentFileWritesConflictResolutionStrategy strategy = new
SimpleConcurrentFileWritesConflictResolutionStrategy();
+ HoodieCommitMetadata currentMetadata =
createCommitMetadata(currentWriterInstant);
+ metaClient.reloadActiveTimeline();
+
+ List<HoodieInstant> candidateInstants =
strategy.getCandidateInstants(metaClient, currentInstant.get(),
lastSuccessfulInstant)
+ .collect(Collectors.toList());
+ Assertions.assertEquals(1, candidateInstants.size());
+
+ ConcurrentOperation thatCompactionOperation = new
ConcurrentOperation(candidateInstants.get(0), metaClient);
+ ConcurrentOperation thisCommitOperation = new
ConcurrentOperation(currentInstant.get(), currentMetadata);
+ Assertions.assertTrue(strategy.hasConflict(thisCommitOperation,
thatCompactionOperation));
+
+ HoodieWriteConflictException exception =
Assertions.assertThrows(HoodieWriteConflictException.class,
+ () -> strategy.resolveConflict(null, thisCommitOperation,
thatCompactionOperation));
+
+ String errorMessage = exception.getMessage();
+ Assertions.assertTrue(errorMessage.contains("Table Compaction"),
+ "Error message should mention 'Table Compaction', but was: " +
errorMessage);
+ Assertions.assertTrue(errorMessage.contains("is currently running"),
+ "Error message should contain 'is currently running', but was: " +
errorMessage);
+ Assertions.assertTrue(errorMessage.contains("Please retry the write
operation after the compaction completes"),
+ "Error message should contain retry guidance, but was: " +
errorMessage);
+ Assertions.assertTrue(errorMessage.contains(compactionInstant),
+ "Error message should contain compaction instant time, but was: " +
errorMessage);
+ }
+
+ @Test
+ public void testErrorMessageForConflictWithClustering() throws Exception {
+ initMetaClient();
+ createCommit(WriteClientTestUtils.createNewInstantTime(), metaClient);
+ HoodieActiveTimeline timeline = metaClient.getActiveTimeline();
+ Option<HoodieInstant> lastSuccessfulInstant =
timeline.getCommitsTimeline().filterCompletedInstants().lastInstant();
+
+ // writer 1 starts
+ String currentWriterInstant = WriteClientTestUtils.createNewInstantTime();
+ createInflightCommit(currentWriterInstant, metaClient);
+
+ // clustering gets scheduled
+ String clusteringInstant = WriteClientTestUtils.createNewInstantTime();
+ createClusterRequested(clusteringInstant, metaClient);
+
+ Option<HoodieInstant> currentInstant =
Option.of(INSTANT_GENERATOR.createNewInstant(State.INFLIGHT,
HoodieTimeline.COMMIT_ACTION, currentWriterInstant));
+ SimpleConcurrentFileWritesConflictResolutionStrategy strategy = new
SimpleConcurrentFileWritesConflictResolutionStrategy();
+ HoodieCommitMetadata currentMetadata =
createCommitMetadata(currentWriterInstant);
+ metaClient.reloadActiveTimeline();
+
+ List<HoodieInstant> candidateInstants =
strategy.getCandidateInstants(metaClient, currentInstant.get(),
lastSuccessfulInstant)
+ .collect(Collectors.toList());
+ Assertions.assertEquals(1, candidateInstants.size());
+
+ ConcurrentOperation thatClusteringOperation = new
ConcurrentOperation(candidateInstants.get(0), metaClient);
+ ConcurrentOperation thisCommitOperation = new
ConcurrentOperation(currentInstant.get(), currentMetadata);
+ Assertions.assertTrue(strategy.hasConflict(thisCommitOperation,
thatClusteringOperation));
+
+ HoodieWriteConflictException exception =
Assertions.assertThrows(HoodieWriteConflictException.class,
+ () -> strategy.resolveConflict(null, thisCommitOperation,
thatClusteringOperation));
+
+ String errorMessage = exception.getMessage();
+ Assertions.assertTrue(errorMessage.contains("Table Clustering"),
+ "Error message should mention 'Table Clustering', but was: " +
errorMessage);
+ Assertions.assertTrue(errorMessage.contains("is currently running"),
+ "Error message should contain 'is currently running', but was: " +
errorMessage);
+ Assertions.assertTrue(errorMessage.contains("Please retry the write
operation after the clustering completes"),
+ "Error message should contain retry guidance, but was: " +
errorMessage);
+ Assertions.assertTrue(errorMessage.contains(clusteringInstant),
+ "Error message should contain clustering instant time, but was: " +
errorMessage);
+ }
+
+ @Test
+ public void testErrorMessageForConflictBetweenRegularWrites() throws
Exception {
+ initMetaClient();
+ createCommit(WriteClientTestUtils.createNewInstantTime(), metaClient);
+ HoodieActiveTimeline timeline = metaClient.getActiveTimeline();
+ Option<HoodieInstant> lastSuccessfulInstant =
timeline.getCommitsTimeline().filterCompletedInstants().lastInstant();
+
+ // writer 1 starts
+ String currentWriterInstant = WriteClientTestUtils.createNewInstantTime();
+ createInflightCommit(currentWriterInstant, metaClient);
+
+ // writer 2 starts and finishes
+ String writer2Instant = WriteClientTestUtils.createNewInstantTime();
+ createCommit(writer2Instant, metaClient);
+
+ Option<HoodieInstant> currentInstant =
Option.of(INSTANT_GENERATOR.createNewInstant(State.INFLIGHT,
HoodieTimeline.COMMIT_ACTION, currentWriterInstant));
+ SimpleConcurrentFileWritesConflictResolutionStrategy strategy = new
SimpleConcurrentFileWritesConflictResolutionStrategy();
+ HoodieCommitMetadata currentMetadata =
createCommitMetadata(currentWriterInstant);
+ metaClient.reloadActiveTimeline();
+
+ List<HoodieInstant> candidateInstants =
strategy.getCandidateInstants(metaClient, currentInstant.get(),
lastSuccessfulInstant)
+ .collect(Collectors.toList());
+ Assertions.assertEquals(1, candidateInstants.size());
+
+ ConcurrentOperation thatCommitOperation = new
ConcurrentOperation(candidateInstants.get(0), metaClient);
+ ConcurrentOperation thisCommitOperation = new
ConcurrentOperation(currentInstant.get(), currentMetadata);
+ Assertions.assertTrue(strategy.hasConflict(thisCommitOperation,
thatCommitOperation));
+
+ HoodieWriteConflictException exception =
Assertions.assertThrows(HoodieWriteConflictException.class,
+ () -> strategy.resolveConflict(null, thisCommitOperation,
thatCommitOperation));
+
+ String errorMessage = exception.getMessage();
+ Assertions.assertTrue(errorMessage.contains("Cannot resolve conflicts for
overlapping writes"),
+ "Error message should mention overlapping writes, but was: " +
errorMessage);
+ Assertions.assertTrue(errorMessage.contains("has overlapping file groups"),
+ "Error message should contain 'has overlapping file groups', but was:
" + errorMessage);
+ Assertions.assertTrue(errorMessage.contains(currentWriterInstant),
+ "Error message should contain current writer instant time, but was: "
+ errorMessage);
+ Assertions.assertTrue(errorMessage.contains(writer2Instant),
+ "Error message should contain other writer instant time, but was: " +
errorMessage);
+ // Should NOT contain table service specific messaging
+ Assertions.assertFalse(errorMessage.contains("Table Compaction"),
+ "Error message should not mention table services for regular writes,
but was: " + errorMessage);
+ Assertions.assertFalse(errorMessage.contains("Please retry"),
+ "Error message should not contain retry guidance for regular writes,
but was: " + errorMessage);
+ }
+
+ @Test
+ public void testErrorMessageForConflictWithCompletedClustering() throws
Exception {
+ initMetaClient();
+ createCommit(WriteClientTestUtils.createNewInstantTime(), metaClient);
+ HoodieActiveTimeline timeline = metaClient.getActiveTimeline();
+ Option<HoodieInstant> lastSuccessfulInstant =
timeline.getCommitsTimeline().filterCompletedInstants().lastInstant();
+
+ // writer 1 starts
+ String currentWriterInstant = WriteClientTestUtils.createNewInstantTime();
+ createInflightCommit(currentWriterInstant, metaClient);
+
+ // clustering completes
+ String clusteringInstant = WriteClientTestUtils.createNewInstantTime();
+ createCluster(clusteringInstant, WriteOperationType.CLUSTER, metaClient);
+
+ Option<HoodieInstant> currentInstant =
Option.of(INSTANT_GENERATOR.createNewInstant(State.INFLIGHT,
HoodieTimeline.COMMIT_ACTION, currentWriterInstant));
+ SimpleConcurrentFileWritesConflictResolutionStrategy strategy = new
SimpleConcurrentFileWritesConflictResolutionStrategy();
+ HoodieCommitMetadata currentMetadata =
createCommitMetadata(currentWriterInstant);
+ metaClient.reloadActiveTimeline();
+
+ List<HoodieInstant> candidateInstants =
strategy.getCandidateInstants(metaClient, currentInstant.get(),
lastSuccessfulInstant)
+ .collect(Collectors.toList());
+ Assertions.assertEquals(1, candidateInstants.size());
+
+ ConcurrentOperation thatClusteringOperation = new
ConcurrentOperation(candidateInstants.get(0), metaClient);
+ ConcurrentOperation thisCommitOperation = new
ConcurrentOperation(currentInstant.get(), currentMetadata);
+ Assertions.assertTrue(strategy.hasConflict(thisCommitOperation,
thatClusteringOperation));
+
+ HoodieWriteConflictException exception =
Assertions.assertThrows(HoodieWriteConflictException.class,
+ () -> strategy.resolveConflict(null, thisCommitOperation,
thatClusteringOperation));
+
+ String errorMessage = exception.getMessage();
+ Assertions.assertTrue(errorMessage.contains("Table Clustering"),
+ "Error message should mention 'Table Clustering', but was: " +
errorMessage);
+ Assertions.assertTrue(errorMessage.contains(clusteringInstant),
+ "Error message should contain clustering instant time, but was: " +
errorMessage);
+ Assertions.assertTrue(errorMessage.contains("COMPLETED"),
+ "Error message should indicate the state of the clustering operation,
but was: " + errorMessage);
+ }
}
diff --git
a/hudi-common/src/main/java/org/apache/hudi/common/model/WriteOperationType.java
b/hudi-common/src/main/java/org/apache/hudi/common/model/WriteOperationType.java
index e794aad60b60..db288339a1b0 100644
---
a/hudi-common/src/main/java/org/apache/hudi/common/model/WriteOperationType.java
+++
b/hudi-common/src/main/java/org/apache/hudi/common/model/WriteOperationType.java
@@ -188,6 +188,17 @@ public enum WriteOperationType {
return operationType == COMPACT || operationType == CLUSTER;
}
+ /**
+ * Checks if the given operation type is a table service operation.
+ * Table service operations include compaction, clustering, log compaction,
and indexing.
+ */
+ public static boolean isTableService(WriteOperationType operationType) {
+ return operationType == COMPACT
+ || operationType == CLUSTER
+ || operationType == LOG_COMPACT
+ || operationType == INDEX;
+ }
+
/**
* @return true if streaming writes to metadata table is supported for a
given {@link WriteOperationType}. false otherwise.
*/