github-actions[bot] commented on code in PR #41285:
URL: https://github.com/apache/beam/pull/41285#discussion_r4560934089


##########
sdks/java/harness/src/main/java/org/apache/beam/fn/harness/control/ProcessBundleHandler.java:
##########
@@ -567,6 +568,10 @@ public BeamFnApi.InstructionResponse.Builder 
processBundle(InstructionRequest re
             finishFunction.run();
           }
 
+          // Surface downstream exceptions that user code may have caught 
while processing
+          // fused outputs. This must happen before outbound data is finalized.
+          
bundleProcessor.getpCollectionConsumerRegistry().failBundleIfDownstreamFailed();

Review Comment:
   **🟠 High**: `failBundleIfDownstreamFailed()` is called *after* all 
`@FinishBundle` functions have executed (lines 566-569). In `FAIL` mode, if a 
downstream `DoFn` threw an exception during `@ProcessElement` that was 
swallowed by an upstream `DoFn`, its `@FinishBundle` method will still be 
invoked. This violates the `DoFn` lifecycle contract (finish bundle should not 
be called on a failed transform instance), can cause downstream transforms to 
flush corrupted/partial state to external sinks, or can throw a secondary 
exception that masks `firstDownstreamException`.
   
   Consider moving 
`bundleProcessor.getpCollectionConsumerRegistry().failBundleIfDownstreamFailed();`
 to immediately *before* the `finishFunctionRegistry` loop (or skipping finish 
functions when a downstream failure has already been recorded in `FAIL` mode).



##########
sdks/java/harness/src/test/java/org/apache/beam/fn/harness/control/ProcessBundleHandlerTest.java:
##########
@@ -935,6 +939,277 @@ public void testPTransformStartExceptionsArePropagated() {
     
assertThat(handler.bundleProcessorCache.getCachedBundleProcessors().get("1L"), 
empty());
   }
 
+  private static final AtomicInteger FUSED_DOWNSTREAM_INVOCATIONS = new 
AtomicInteger();
+  private static final AtomicInteger FUSED_UPSTREAM_CAUGHT_EXCEPTIONS = new 
AtomicInteger();
+
+  private static final class CatchingFusedDoFn extends DoFn<String, String> {
+    private static final TupleTag<String> MAIN_OUTPUT_TAG = new 
TupleTag<>("mainOutput");
+
+    @ProcessElement
+    public void processElement(ProcessContext context) {
+      try {
+        context.output(context.element());
+      } catch (RuntimeException exception) {
+        FUSED_UPSTREAM_CAUGHT_EXCEPTIONS.incrementAndGet();
+      }
+    }
+  }
+
+  private static final class ThrowingFusedDoFn extends DoFn<String, String> {
+    private static final TupleTag<String> MAIN_OUTPUT_TAG = new 
TupleTag<>("mainOutput");
+
+    @ProcessElement
+    public void processElement(ProcessContext context) {
+      FUSED_DOWNSTREAM_INVOCATIONS.incrementAndGet();
+      throw new IllegalStateException("fused downstream failure");
+    }
+  }
+
+  private static PTransform fusedDoFnTransform(
+      String inputPCollection,
+      String outputPCollection,
+      DoFnWithExecutionInformation doFnWithExecutionInformation) {
+    RunnerApi.FunctionSpec doFnSpec =
+        RunnerApi.FunctionSpec.newBuilder()
+            .setUrn(ParDoTranslation.CUSTOM_JAVA_DO_FN_URN)
+            .setPayload(
+                ByteString.copyFrom(
+                    
SerializableUtils.serializeToByteArray(doFnWithExecutionInformation)))
+            .build();
+    ParDoPayload payload = ParDoPayload.newBuilder().setDoFn(doFnSpec).build();
+    return PTransform.newBuilder()
+        .setSpec(
+            FunctionSpec.newBuilder()
+                .setUrn(PTransformTranslation.PAR_DO_TRANSFORM_URN)
+                .setPayload(payload.toByteString()))
+        .putInputs("input", inputPCollection)
+        .putOutputs("mainOutput", outputPCollection)
+        .build();
+  }
+
+  private ProcessBundleHandler setupHandlerForFusedExceptionTest(String 
exceptionHandling)
+      throws Exception {
+    TupleTag<String> upstreamTag = CatchingFusedDoFn.MAIN_OUTPUT_TAG;
+    TupleTag<String> downstreamTag = ThrowingFusedDoFn.MAIN_OUTPUT_TAG;
+    DoFnWithExecutionInformation upstream =
+        DoFnWithExecutionInformation.of(
+            new CatchingFusedDoFn(),
+            upstreamTag,
+            Collections.emptyMap(),
+            DoFnSchemaInformation.create());
+    DoFnWithExecutionInformation downstream =
+        DoFnWithExecutionInformation.of(
+            new ThrowingFusedDoFn(),
+            downstreamTag,

Review Comment:
   **🟢 Nit**: `FUSED_DOWNSTREAM_INVOCATIONS` and 
`FUSED_UPSTREAM_CAUGHT_EXCEPTIONS` are static fields shared across tests. While 
each test resets them at the start, resetting them in an `@Before` method or 
scoping counters per-test instance avoids potential cross-test pollution if 
tests are ever run concurrently or if new tests are added.



##########
sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/PCollectionConsumerRegistry.java:
##########
@@ -61,15 +63,29 @@
 /**
  * The {@code PCollectionConsumerRegistry} is used to maintain a collection of 
consuming
  * FnDataReceiver for each pCollectionId. Registering with this class allows 
inserting an element
- * count counter for every pCollection. A combined MultiplexingConsumer 
(Wrapped with an
- * ElementCountFnDataReceiver) is returned by calling getMultiplexingConsumer.
+ * count counter for every pCollection. A combined MultiplexingConsumer 
(wrapped with metrics
+ * tracking) is returned by calling getMultiplexingConsumer. When configured, 
this class also
+ * records downstream exceptions so that exceptions swallowed by fused user 
code can be surfaced
+ * when the bundle finishes.
  */
 @SuppressWarnings({
   "rawtypes", // TODO(https://github.com/apache/beam/issues/20447)
   "nullness" // TODO(https://github.com/apache/beam/issues/20497)
 })
 public class PCollectionConsumerRegistry {
 
+  /** Controls how exceptions from fused downstream consumers are handled at 
bundle completion. */
+  public enum FusedDoFnExceptionHandling {
+    LEGACY,
+    WARN,
+    FAIL
+  }
+
+  private final FusedDoFnExceptionHandling fusedDoFnExceptionHandling;
+  private final AtomicReference<Exception> firstDownstreamException = new 
AtomicReference<>();

Review Comment:
   **🟡 Medium**: Downstream transforms may also throw `Error` (such as 
`AssertionError` in tests or user validations), which can be caught by overly 
broad `catch (Throwable t)` blocks in upstream user code. Because 
`MetricTrackingFnDataReceiver` and `MultiplexingMetricTrackingFnDataReceiver` 
only `catch (Exception e)`, any `Error` thrown by a downstream `DoFn` bypasses 
`recordDownstreamException` (and `logAndRethrow`). Consider tracking 
`Throwable` (or catching `Throwable` and rethrowing `Error` after recording) so 
swallowed `AssertionError`s and other `Error`s are also surfaced at bundle 
completion.



##########
sdks/java/harness/src/main/java/org/apache/beam/fn/harness/data/PCollectionConsumerRegistry.java:
##########
@@ -461,9 +554,13 @@ public void accept(WindowedValue<T> input) throws 
Exception {
       for (int size = consumerAndMetadatas.size(), i = 0; i < size; ++i) {
         ConsumerAndMetadata consumerAndMetadata = consumerAndMetadatas.get(i);
         ExecutionState state = consumerAndMetadata.getExecutionState();
+        // The original failure was already logged when the consumer first 
threw. Avoid logging
+        // the same failure again for every later element rejected by 
fail-fast handling.
+        failFastIfDownstreamFailed(consumerAndMetadata.getPTransformId());

Review Comment:
   **🟠 High**: When a `PCollection` has multiple consumers (e.g., `[Consumer1, 
Consumer2]`), if `Consumer1` fails on element 1 and the upstream `DoFn` catches 
the exception and emits element 2, `failFastIfDownstreamFailed` throws 
immediately at `i = 0`. This aborts the loop before `Consumer2` ever receives 
element 1 or element 2, even in a scenario where `WARN` vs `FAIL` behavior or 
sibling consumer execution is expected before rethrowing to the caller. 
Moreover, `this.elementCountCounter.inc(...)` on line 540 has already 
incremented the PCollection counter before the fail-fast check in 
`MetricTrackingFnDataReceiver` (where `failFastIfDownstreamFailed` runs before 
`elementCountCounter.inc`), creating an inconsistency in metric reporting 
between single and multiplexing consumers.



-- 
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