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


##########
hudi-utilities/src/main/java/org/apache/hudi/utilities/streamer/ErrorTableUtils.java:
##########
@@ -79,25 +85,61 @@ public static 
HoodieErrorTableConfig.ErrorWriteFailureStrategy getErrorWriteFail
     return 
HoodieErrorTableConfig.ErrorWriteFailureStrategy.valueOf(writeFailureStrategy);
   }
 
-  /**
-   * validates for constraints on ErrorRecordColumn when ErrorTable enabled 
configs are set.
-   * @param dataset
-   */
-  public static void validate(Dataset<Row> dataset) {
-    if (!isErrorTableCorruptRecordColumnPresent(dataset)) {
-      throw new HoodieValidationException(String.format("Invalid condition, 
columnName=%s "
-              + "is not present in transformer " + "output schema", 
ERROR_TABLE_CURRUPT_RECORD_COL_NAME));
-    }
-  }
-
   public static Dataset<Row> 
addNullValueErrorTableCorruptRecordColumn(Dataset<Row> dataset) {
     if (!isErrorTableCorruptRecordColumnPresent(dataset)) {
       dataset = dataset.withColumn(ERROR_TABLE_CURRUPT_RECORD_COL_NAME, 
lit(null));
     }
     return dataset;
   }
 
-  private static boolean isErrorTableCorruptRecordColumnPresent(Dataset<Row> 
dataset) {
+  public static boolean isErrorTableCorruptRecordColumnPresent(Dataset<Row> 
dataset) {
     return Arrays.stream(dataset.columns()).anyMatch(col -> 
col.equals(ERROR_TABLE_CURRUPT_RECORD_COL_NAME));
   }
+
+  /**
+   * Restores stashed {@code _corrupt_record} values onto a transformed 
dataset via positional
+   * RDD zip. Works when the transformer only projects columns (row count and 
partition layout
+   * unchanged). Falls back to {@link 
#addNullValueErrorTableCorruptRecordColumn} with a WARN
+   * if the zip fails (e.g. because the transformer filtered or repartitioned 
rows).
+   *
+   * <p><b>Limitation:</b> a transformer that reorders rows (e.g. {@code 
orderBy}) without
+   * changing the count will produce a successful zip with misaligned values. 
This is no worse
+   * than the null-re-injection alternative, but callers should be aware.
+   *
+   * @param sparkSession the active Spark session
+   * @param transformed  the dataset after the transformer ran (missing {@code 
_corrupt_record})
+   * @param stash        single-column dataset of pre-transform {@code 
_corrupt_record} values
+   * @return {@code transformed} with the {@code _corrupt_record} column 
restored
+   */
+  public static Dataset<Row> restoreCorruptRecordColumn(
+      SparkSession sparkSession, Dataset<Row> transformed, Dataset<Row> stash) 
{
+    try {
+      JavaRDD<Row> zipped = transformed.javaRDD()
+          .zip(stash.javaRDD())
+          .map(pair -> {
+            Row dataRow = pair._1();
+            Row stashRow = pair._2();
+            int size = dataRow.size();
+            Object[] fields = new Object[size + 1];
+            for (int i = 0; i < size; i++) {
+              fields[i] = dataRow.get(i);
+            }
+            fields[size] = stashRow.isNullAt(0) ? null : stashRow.get(0);
+            return RowFactory.create(fields);
+          });
+      StructType schema = transformed.schema()
+          .add(ERROR_TABLE_CURRUPT_RECORD_COL_NAME, DataTypes.StringType, 
true);
+      Dataset<Row> result = sparkSession.createDataFrame(zipped, schema);
+      // Force eager evaluation so zip failures (partition/element count 
mismatches)
+      // are caught here instead of surfacing downstream. Without this, the 
entire
+      // zip/map/createDataFrame chain is lazy and the catch block never 
triggers.
+      result.count();
+      return result;
+    } catch (Exception e) {
+      LOG.warn("Failed to restore {} column after transformer dropped it "
+          + "(row count or partitioning changed): {}",
+          ERROR_TABLE_CURRUPT_RECORD_COL_NAME, e.getMessage());
+      return addNullValueErrorTableCorruptRecordColumn(transformed);

Review Comment:
   🤖 If an earlier transformer marked rows as corrupt and a later one filters 
rows and drops the column (for example a SQL transformer with a WHERE and an 
explicit projection), the zip fails and this re-adds the column as null, so 
`processErrorEvents` sends those corrupt rows to the main table with only a 
WARN; before this change the pipeline failed loudly. Could we rethrow here (or 
fall back to null only when the stash has no non-null values) instead of 
quietly writing error records as good data?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-utilities/src/test/java/org/apache/hudi/utilities/functional/TestErrorTableAwareChainedTransformer.java:
##########
@@ -76,17 +78,90 @@ public void testForErrorTableConfig() {
   }
 
   @Test

Review Comment:
   🤖 nit: could you drop the internal `ENG-41958` ticket id from this comment? 
It won't mean anything to OSS readers, and the rest of the comment already 
explains the regression.
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-utilities/src/main/java/org/apache/hudi/utilities/transform/ErrorTableAwareChainedTransformer.java:
##########
@@ -51,12 +56,38 @@ public ErrorTableAwareChainedTransformer(List<Transformer> 
transformers) {
   public Dataset<Row> apply(JavaSparkContext jsc, SparkSession sparkSession, 
Dataset<Row> rowDataset,
                             TypedProperties properties) {
     Dataset<Row> dataset = rowDataset;
+    // _corrupt_record can only hold values if the source supplied it or a 
transformer kept it;
+    // while it is known to be all-null, re-adding it as null is lossless and 
needs no stash.
+    boolean corruptRecordMayHaveValues = 
ErrorTableUtils.isErrorTableCorruptRecordColumnPresent(dataset);
     dataset = 
ErrorTableUtils.addNullValueErrorTableCorruptRecordColumn(dataset);
     for (TransformerInfo transformerInfo : transformers) {
       Transformer transformer = transformerInfo.getTransformer();
+
+      // Stash _corrupt_record values before the transformer can drop them
+      Dataset<Row> corruptRecordStash = null;
+      if (corruptRecordMayHaveValues) {
+        // Eagerly checkpoint the input so the stash and the transformer read 
the same
+        // materialized rows. Caching only the stash is not enough: the 
transformer would
+        // recompute the upstream lineage independently, and if that lineage 
has
+        // non-deterministic row ordering (e.g. shuffle/repartition) the 
positional zip
+        // silently misaligns values. The checkpoint also keeps the restored 
dataset
+        // aligned when downstream actions recompute it, so it is left for 
Spark's
+        // ContextCleaner to release rather than unpersisted here.
+        dataset = dataset.localCheckpoint(true);

Review Comment:
   🤖 Because the source path already strips `_corrupt_record`, any transformer 
that just keeps the column (e.g. `SELECT * FROM <SRC>`) flips 
`corruptRecordMayHaveValues` to true even when every value is null, so each 
later transformer in the chain now runs an eager `localCheckpoint` of the whole 
batch. Those checkpoint blocks live only on executors, so losing an executor 
(spot or dynamic allocation) can fail the sync, which affects chains that 
passed fine before this PR; could the checkpoint be skipped when the column is 
still all null, or the stash and checkpoint be created only after we see the 
transformer dropped the column?
   
   <sub><i>⚠️ AI-generated; verify before applying. React 👍/👎 to flag 
quality.</i></sub>



##########
hudi-utilities/src/main/java/org/apache/hudi/utilities/streamer/ErrorTableUtils.java:
##########
@@ -79,25 +85,61 @@ public static 
HoodieErrorTableConfig.ErrorWriteFailureStrategy getErrorWriteFail
     return 
HoodieErrorTableConfig.ErrorWriteFailureStrategy.valueOf(writeFailureStrategy);
   }
 
-  /**
-   * validates for constraints on ErrorRecordColumn when ErrorTable enabled 
configs are set.
-   * @param dataset
-   */
-  public static void validate(Dataset<Row> dataset) {
-    if (!isErrorTableCorruptRecordColumnPresent(dataset)) {
-      throw new HoodieValidationException(String.format("Invalid condition, 
columnName=%s "
-              + "is not present in transformer " + "output schema", 
ERROR_TABLE_CURRUPT_RECORD_COL_NAME));
-    }
-  }
-
   public static Dataset<Row> 
addNullValueErrorTableCorruptRecordColumn(Dataset<Row> dataset) {
     if (!isErrorTableCorruptRecordColumnPresent(dataset)) {
       dataset = dataset.withColumn(ERROR_TABLE_CURRUPT_RECORD_COL_NAME, 
lit(null));
     }
     return dataset;
   }
 
-  private static boolean isErrorTableCorruptRecordColumnPresent(Dataset<Row> 
dataset) {
+  public static boolean isErrorTableCorruptRecordColumnPresent(Dataset<Row> 
dataset) {
     return Arrays.stream(dataset.columns()).anyMatch(col -> 
col.equals(ERROR_TABLE_CURRUPT_RECORD_COL_NAME));
   }
+
+  /**
+   * Restores stashed {@code _corrupt_record} values onto a transformed 
dataset via positional
+   * RDD zip. Works when the transformer only projects columns (row count and 
partition layout
+   * unchanged). Falls back to {@link 
#addNullValueErrorTableCorruptRecordColumn} with a WARN
+   * if the zip fails (e.g. because the transformer filtered or repartitioned 
rows).
+   *
+   * <p><b>Limitation:</b> a transformer that reorders rows (e.g. {@code 
orderBy}) without
+   * changing the count will produce a successful zip with misaligned values. 
This is no worse

Review Comment:
   🤖 I don't think a misaligned zip is "no worse than" the null fallback: after 
an `orderBy`/`repartition` with the same per-partition counts, good rows get 
another row's non-null `_corrupt_record` and are moved to the error table (so 
they disappear from the target table), while the truly corrupt rows are 
upserted; would it make sense to fail here, or to carry a row id through rather 
than relying on position?
   
   <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