hudi-agent commented on code in PR #19801:
URL: https://github.com/apache/hudi/pull/19801#discussion_r3900949060


##########
hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/SparkStreamingMetadataWriteHandler.java:
##########
@@ -42,26 +42,32 @@ public HoodieData<WriteStatus> 
streamWriteToMetadataTable(HoodieTable table, Hoo
     Option<HoodieTableMetadataWriter> metadataWriterOpt = 
getMetadataWriter(instantTime, table);
     ValidationUtils.checkState(metadataWriterOpt.isPresent(),
         "Cannot instantiate metadata writer for the table of interest " + 
table.getMetaClient().getBasePath());
-    return streamWriteToMetadataTable(dataTableWriteStatuses, 
metadataWriterOpt.get(), table, instantTime, coalesceDivisorForDataTableWrites);
+    return streamWriteToMetadataTable(dataTableWriteStatuses, 
metadataWriterOpt.get(), table, instantTime,
+        coalesceDivisorForDataTableWrites);
   }
 
   private HoodieData<WriteStatus> 
streamWriteToMetadataTable(HoodieData<WriteStatus> dataTableWriteStatuses,
                                                              
HoodieTableMetadataWriter metadataWriter,
                                                              HoodieTable table,
                                                              String 
instantTime,
                                                              int 
coalesceDivisorForDataTableWrites) {
-    HoodieData<WriteStatus> mdtWriteStatuses = 
metadataWriter.streamWriteToMetadataPartitions(dataTableWriteStatuses, 
instantTime);
-    mdtWriteStatuses.persist("MEMORY_AND_DISK_SER", table.getContext(), 
HoodieData.HoodieDataCacheKey.of(table.getMetaClient().getBasePath().toString(),
 instantTime));
-    HoodieData<WriteStatus> coalescedDataWriteStatuses;
-    int coalesceParallelism = Math.max(1, 
dataTableWriteStatuses.getNumPartitions() / coalesceDivisorForDataTableWrites);
-    // lets coalesce to lesser number of spark tasks so that, when unioned 
along with metadata table write status,
-    // we only allocate very less number of tasks for data table write 
statuses.
-    // In fact, data table writes should have triggered in previous stage 
before coalesce (partition by below forces the writes
-    // to data table is triggered in previous stage and with the coalesced 
stage)
-    coalescedDataWriteStatuses = 
HoodieJavaRDD.of(HoodieJavaRDD.getJavaRDD(dataTableWriteStatuses)
-            .mapToPair((PairFunction<WriteStatus, String, WriteStatus>) 
writeStatus -> new Tuple2(writeStatus.getStat().getPath(), writeStatus))
+    int coalesceParallelism = Math.max(1,
+        dataTableWriteStatuses.getNumPartitions() / 
coalesceDivisorForDataTableWrites);
+
+    // 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(

Review Comment:
   🤖 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?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-client/hudi-spark-client/src/main/java/org/apache/hudi/client/SparkStreamingMetadataWriteHandler.java:
##########
@@ -42,26 +42,32 @@ public HoodieData<WriteStatus> 
streamWriteToMetadataTable(HoodieTable table, Hoo
     Option<HoodieTableMetadataWriter> metadataWriterOpt = 
getMetadataWriter(instantTime, table);
     ValidationUtils.checkState(metadataWriterOpt.isPresent(),
         "Cannot instantiate metadata writer for the table of interest " + 
table.getMetaClient().getBasePath());
-    return streamWriteToMetadataTable(dataTableWriteStatuses, 
metadataWriterOpt.get(), table, instantTime, coalesceDivisorForDataTableWrites);
+    return streamWriteToMetadataTable(dataTableWriteStatuses, 
metadataWriterOpt.get(), table, instantTime,
+        coalesceDivisorForDataTableWrites);
   }
 
   private HoodieData<WriteStatus> 
streamWriteToMetadataTable(HoodieData<WriteStatus> dataTableWriteStatuses,
                                                              
HoodieTableMetadataWriter metadataWriter,
                                                              HoodieTable table,
                                                              String 
instantTime,
                                                              int 
coalesceDivisorForDataTableWrites) {
-    HoodieData<WriteStatus> mdtWriteStatuses = 
metadataWriter.streamWriteToMetadataPartitions(dataTableWriteStatuses, 
instantTime);
-    mdtWriteStatuses.persist("MEMORY_AND_DISK_SER", table.getContext(), 
HoodieData.HoodieDataCacheKey.of(table.getMetaClient().getBasePath().toString(),
 instantTime));
-    HoodieData<WriteStatus> coalescedDataWriteStatuses;
-    int coalesceParallelism = Math.max(1, 
dataTableWriteStatuses.getNumPartitions() / coalesceDivisorForDataTableWrites);
-    // lets coalesce to lesser number of spark tasks so that, when unioned 
along with metadata table write status,
-    // we only allocate very less number of tasks for data table write 
statuses.
-    // In fact, data table writes should have triggered in previous stage 
before coalesce (partition by below forces the writes
-    // to data table is triggered in previous stage and with the coalesced 
stage)
-    coalescedDataWriteStatuses = 
HoodieJavaRDD.of(HoodieJavaRDD.getJavaRDD(dataTableWriteStatuses)
-            .mapToPair((PairFunction<WriteStatus, String, WriteStatus>) 
writeStatus -> new Tuple2(writeStatus.getStat().getPath(), writeStatus))
+    int coalesceParallelism = Math.max(1,
+        dataTableWriteStatuses.getNumPartitions() / 
coalesceDivisorForDataTableWrites);
+
+    // 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(
+        HoodieJavaRDD.getJavaRDD(dataTableWriteStatuses)
+            .mapToPair((PairFunction<WriteStatus, String, WriteStatus>) 
writeStatus ->
+                new Tuple2<>(writeStatus.getStat().getPath(), writeStatus))
             .partitionBy(new CoalescingPartitioner(coalesceParallelism))
             .map((Function<Tuple2<String, WriteStatus>, WriteStatus>) entry -> 
entry._2));
-    return coalescedDataWriteStatuses.union(mdtWriteStatuses);
+
+    HoodieData<WriteStatus> metadataWriteStatuses =
+        
metadataWriter.streamWriteToMetadataPartitions(committedDataWriteStatuses, 
instantTime);

Review Comment:
   🤖 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?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-client/hudi-spark-client/src/test/java/org/apache/hudi/client/TestSparkStreamingMetadataWriteHandler.java:
##########
@@ -60,27 +74,93 @@ void setUp() {
 
   private static Stream<Arguments> coalesceDivisorTestArgs() {
     return Arrays.stream(new Object[][] {
-        {100, 20, 1000, true},
-        {1, 1, 1000, true},
-        {10000, 100, 5000, true},
-        {10000, 100, 5000, true},
-        {10000, 100, 20000, true},
-        {10000, 100, 20000, true}
+        {100, 1000},
+        {1, 1},
+        {10000, 1},
+        {10000, 5000},
+        {10001, 5000},
+        {10000, 20000}
     }).map(Arguments::of);
   }
 
   @ParameterizedTest
   @MethodSource("coalesceDivisorTestArgs")
-  public void testCoalesceDividentConfig(int numDataTableWriteStatuses, int 
numMdtWriteStatus, int coalesceDividentForDataTableWrites) {
+  public void testCoalesceDividentConfig(int numDataTableWriteStatuses, int 
coalesceDividentForDataTableWrites) {
     HoodieData<WriteStatus> dataTableWriteStatus = 
mockWriteStatuses(numDataTableWriteStatuses);
-    HoodieData<WriteStatus> mdtWriteStatus = 
mockWriteStatuses(numMdtWriteStatus);
-    HoodieTableMetadataWriter mdtWriter = 
mock(HoodieTableMetadataWriter.class);
-    when(mdtWriter.streamWriteToMetadataPartitions(any(), 
any())).thenReturn(mdtWriteStatus);
+    HoodieTableMetadataWriter mdtWriter = 
metadataWriterReturningEmptyStatuses();
     SparkStreamingMetadataWriteHandler metadataWriteHandler = new 
MockSparkStreamingMetadataWriteHandler(mdtWriter);
 
     HoodieData<WriteStatus> allWriteStatuses = 
metadataWriteHandler.streamWriteToMetadataTable(mockHoodieTable, 
dataTableWriteStatus, "00001",
         coalesceDividentForDataTableWrites);
-    assertEquals(Math.max(1, numDataTableWriteStatuses / 
coalesceDividentForDataTableWrites) + numMdtWriteStatus, 
allWriteStatuses.getNumPartitions());
+    assertEquals(Math.max(1, numDataTableWriteStatuses / 
coalesceDividentForDataTableWrites),
+        allWriteStatuses.getNumPartitions());
+    verify(mdtWriter).streamWriteToMetadataPartitions(any(), any());
+  }
+
+  @Test
+  void testSparkCollectsOnlySuccessfulRetriedTaskAttempt() {
+    HoodieData<WriteStatus> retryingDataWriteStatus = 
HoodieJavaRDD.of(jsc().parallelize(Arrays.asList(0, 1), 2)
+        .map(partition -> {
+          int attempt = TaskContext.get().attemptNumber();
+          if (partition == 0 && attempt == 0) {
+            throw new IllegalStateException("intentional first-attempt 
failure");
+          }
+          return writeStatus("file-" + partition + "-attempt-" + attempt, 
false);
+        }));
+    HoodieTableMetadataWriter mdtWriter = 
mock(HoodieTableMetadataWriter.class);
+    AtomicReference<HoodieData<WriteStatus>> metadataInput = new 
AtomicReference<>();
+    when(mdtWriter.streamWriteToMetadataPartitions(any(), 
any())).thenAnswer(invocation -> {
+      metadataInput.set(invocation.getArgument(0));
+      return HoodieJavaRDD.of(jsc().emptyRDD());
+    });
+
+    List<WriteStatus> statuses = new 
MockSparkStreamingMetadataWriteHandler(mdtWriter)
+        .streamWriteToMetadataTable(mockHoodieTable, retryingDataWriteStatus, 
"00001", 1000)
+        .collectAsList();
+    List<String> paths = statuses.stream().map(status -> 
status.getStat().getPath()).collect(java.util.stream.Collectors.toList());
+
+    List<String> metadataPaths = metadataInput.get().collectAsList().stream()
+        .map(status -> status.getStat().getPath())
+        .collect(java.util.stream.Collectors.toList());
+    assertTrue(paths.contains("file-0-attempt-1"), "Collected output must come 
from the successful attempt");
+    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")));

Review Comment:
   🤖 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?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to