This is an automated email from the ASF dual-hosted git repository.

stankiewicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new 7b60339d86b Fix output bugs in FnApiDoFnRunner @OnTimer and 
@OnWindowExpiration (#40024)
7b60339d86b is described below

commit 7b60339d86b8de685ce66833c0f868ad9362e2aa
Author: Kenneth Knowles <[email protected]>
AuthorDate: Thu Sep 17 10:59:38 2026 -0400

    Fix output bugs in FnApiDoFnRunner @OnTimer and @OnWindowExpiration (#40024)
    
    The three argument providers each carry their own near-identical copy of
    the output-receiver plumbing, and the @OnTimer and @OnWindowExpiration
    copies had drifted from the working @ProcessElement one:
    
    - OnTimerContext.outputWindowedValue(tag, ...) had an empty body, so
      tagged windowed output from @OnTimer was silently dropped.
    - OnTimerContext's tagged receivers passed no tag to
      outputWindowedValue, so output to a side tag went to the main output.
    - The @OnTimer row receivers built from WindowedValues.builder(
      currentElement) or called withValue() on a fresh builder. Neither
      works during timer processing: currentElement is only set while
      processing an element, and withValue() reads timestamp/window/pane
      off the builder it is called on. Both now seed from currentTimer,
      matching the @OnWindowExpiration equivalents.
    - OnWindowExpirationContext read currentElement.getValueKind() while
      currentElement was null. Window expiration emits new records, so this
      is ValueKind.INSERT.
    - OnWindowExpirationContext.timeDomain() returned currentTimeDomain,
      which processOnWindowExpiration never sets. TimeDomain is not an
      allowed @OnWindowExpiration parameter, so drop the override and let
      BaseArgumentProvider reject it.
    - outputWindowedValue(tag, ...) skipped the unknown-tag check its
      siblings perform, so a bad tag produced an NPE.
    - processTimer's finally did not clear causedByDrain, unlike
      processOnWindowExpiration. Not observable today since every entry
      point sets it before invoking user code, but the asymmetry is a trap.
    
    The two inner Context classes are renamed to TimerContext and
    WindowExpirationContext so the fields holding them can drop their raw
    types; the enclosing providers are generic in K, and errorprone's
    SameNameButDifferent rejects the bare Context name.
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 .../apache/beam/fn/harness/FnApiDoFnRunner.java    |  61 +++--
 .../beam/fn/harness/FnApiDoFnRunnerTest.java       | 292 +++++++++++++++++++++
 2 files changed, 333 insertions(+), 20 deletions(-)

diff --git 
a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java
 
b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java
index a5914a25f79..d391807d798 100644
--- 
a/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java
+++ 
b/sdks/java/harness/src/main/java/org/apache/beam/fn/harness/FnApiDoFnRunner.java
@@ -1235,6 +1235,7 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
       currentTimer = null;
       currentTimeDomain = null;
       currentWindow = null;
+      causedByDrain = null;
     }
   }
 
@@ -2295,10 +2296,10 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
    * DoFn.OnWindowExpiration @OnWindowExpiration}.
    */
   private class OnWindowExpirationContext<K> extends 
BaseArgumentProvider<InputT, OutputT> {
-    private class Context extends DoFn<InputT, 
OutputT>.OnWindowExpirationContext
+    private class WindowExpirationContext extends DoFn<InputT, 
OutputT>.OnWindowExpirationContext
         implements OutputReceiver<OutputT> {
 
-      private Context() {
+      private WindowExpirationContext() {
         doFn.super();
       }
 
@@ -2369,7 +2370,7 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
                 null,
                 currentTimer.causedByDrain(),
                 null,
-                currentElement.getValueKind()));
+                ValueKind.INSERT));
       }
 
       @Override
@@ -2395,6 +2396,9 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
         checkOnWindowExpirationTimestamp(timestamp);
         FnDataReceiver<WindowedValue<T>> consumer =
             (FnDataReceiver) localNameToConsumer.get(tag.getId());
+        if (consumer == null) {
+          throw new IllegalArgumentException(String.format("Unknown output tag 
%s", tag));
+        }
         outputTo(consumer, WindowedValues.of(output, timestamp, windows, 
paneInfo));
       }
 
@@ -2405,7 +2409,12 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
 
       @Override
       public <T> void outputWindowedValue(TupleTag<T> tag, WindowedValue<T> 
windowedValue) {
-        outputTo((FnDataReceiver) localNameToConsumer.get(tag.getId()), 
windowedValue);
+        FnDataReceiver<WindowedValue<T>> consumer =
+            (FnDataReceiver) localNameToConsumer.get(tag.getId());
+        if (consumer == null) {
+          throw new IllegalArgumentException(String.format("Unknown output tag 
%s", tag));
+        }
+        outputTo(consumer, windowedValue);
       }
 
       @SuppressWarnings(
@@ -2435,8 +2444,7 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
       }
     }
 
-    private final OnWindowExpirationContext.Context context =
-        new OnWindowExpirationContext.Context();
+    private final WindowExpirationContext context = new 
WindowExpirationContext();
 
     @Override
     public DoFn<InputT, OutputT>.OnWindowExpirationContext 
onWindowExpirationContext(
@@ -2459,11 +2467,6 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
       return currentTimer.getHoldTimestamp();
     }
 
-    @Override
-    public TimeDomain timeDomain(DoFn<InputT, OutputT> doFn) {
-      return currentTimeDomain;
-    }
-
     @Override
     public K key() {
       return (K) currentTimer.getUserKey();
@@ -2622,9 +2625,9 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
   /** Provides arguments for a {@link DoFnInvoker} for {@link DoFn.OnTimer 
@OnTimer}. */
   private class OnTimerContext<K> extends BaseArgumentProvider<InputT, 
OutputT> {
 
-    private class Context extends DoFn<InputT, OutputT>.OnTimerContext
+    private class TimerContext extends DoFn<InputT, OutputT>.OnTimerContext
         implements OutputReceiver<OutputT> {
-      private Context() {
+      private TimerContext() {
         doFn.super();
       }
 
@@ -2719,7 +2722,12 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
 
       @Override
       public <T> void outputWindowedValue(TupleTag<T> tag, WindowedValue<T> 
windowedValue) {
-        outputTo((FnDataReceiver) localNameToConsumer.get(tag.getId()), 
windowedValue);
+        FnDataReceiver<WindowedValue<T>> consumer =
+            (FnDataReceiver) localNameToConsumer.get(tag.getId());
+        if (consumer == null) {
+          throw new IllegalArgumentException(String.format("Unknown output tag 
%s", tag));
+        }
+        outputTo(consumer, windowedValue);
       }
 
       @Override
@@ -2728,7 +2736,15 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
           T output,
           Instant timestamp,
           Collection<? extends BoundedWindow> windows,
-          PaneInfo paneInfo) {}
+          PaneInfo paneInfo) {
+        checkTimerTimestamp(timestamp);
+        FnDataReceiver<WindowedValue<T>> consumer =
+            (FnDataReceiver) localNameToConsumer.get(tag.getId());
+        if (consumer == null) {
+          throw new IllegalArgumentException(String.format("Unknown output tag 
%s", tag));
+        }
+        outputTo(consumer, WindowedValues.of(output, timestamp, windows, 
paneInfo));
+      }
 
       @Override
       public TimeDomain timeDomain() {
@@ -2772,7 +2788,7 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
       }
     }
 
-    private final OnTimerContext.Context context = new 
OnTimerContext.Context();
+    private final TimerContext context = new TimerContext();
 
     @Override
     public BoundedWindow window() {
@@ -2822,8 +2838,12 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
 
               @Override
               public OutputBuilder<Row> builder(Row value) {
-                return WindowedValues.builder(currentElement)
-                    .withValue(value)
+                return WindowedValues.<Row>builder()
+                    .setValue(value)
+                    .setTimestamp(currentTimer.getHoldTimestamp())
+                    .setWindow(currentWindow)
+                    .setPaneInfo(currentTimer.getPaneInfo())
+                    .setCausedByDrain(currentTimer.causedByDrain())
                     .setReceiver(
                         windowedValue ->
                             context.outputWindowedValue(
@@ -2860,7 +2880,7 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
                     .setWindow(currentWindow)
                     .setCausedByDrain(currentTimer.causedByDrain())
                     .setPaneInfo(currentTimer.getPaneInfo())
-                    .setReceiver(windowedValue -> 
context.outputWindowedValue(windowedValue));
+                    .setReceiver(windowedValue -> 
context.outputWindowedValue(tag, windowedValue));
               }
             };
           }
@@ -2887,7 +2907,7 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
               @Override
               public OutputBuilder<Row> builder(Row value) {
                 return WindowedValues.<Row>builder()
-                    .withValue(value)
+                    .setValue(value)
                     .setTimestamp(currentTimer.getHoldTimestamp())
                     .setWindow(currentWindow)
                     .setPaneInfo(currentTimer.getPaneInfo())
@@ -2895,6 +2915,7 @@ public class FnApiDoFnRunner<InputT, RestrictionT, 
PositionT, WatermarkEstimator
                     .setReceiver(
                         windowedValue ->
                             context.outputWindowedValue(
+                                tag,
                                 windowedValue.withValue(
                                     
fromRowFunction.apply(windowedValue.getValue()))));
               }
diff --git 
a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java
 
b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java
index d24ab39c047..4166b2a48dc 100644
--- 
a/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java
+++ 
b/sdks/java/harness/src/test/java/org/apache/beam/fn/harness/FnApiDoFnRunnerTest.java
@@ -87,6 +87,7 @@ import org.apache.beam.sdk.metrics.Metrics;
 import org.apache.beam.sdk.metrics.MetricsEnvironment;
 import org.apache.beam.sdk.options.ExperimentalOptions;
 import org.apache.beam.sdk.options.PipelineOptionsFactory;
+import org.apache.beam.sdk.schemas.Schema;
 import org.apache.beam.sdk.state.BagState;
 import org.apache.beam.sdk.state.CombiningState;
 import org.apache.beam.sdk.state.StateSpec;
@@ -128,6 +129,7 @@ import org.apache.beam.sdk.values.KV;
 import org.apache.beam.sdk.values.PCollection;
 import org.apache.beam.sdk.values.PCollectionTuple;
 import org.apache.beam.sdk.values.PCollectionView;
+import org.apache.beam.sdk.values.Row;
 import org.apache.beam.sdk.values.TupleTag;
 import org.apache.beam.sdk.values.TupleTagList;
 import org.apache.beam.sdk.values.WindowedValue;
@@ -1140,6 +1142,296 @@ public class FnApiDoFnRunnerTest implements 
Serializable {
       assertThat(result, containsInAnyOrder(expected.toArray()));
     }
 
+    private static class TestTimerTaggedOutputDoFn extends DoFn<KV<String, 
String>, String> {
+      @TimerId("event")
+      private final TimerSpec eventTimerSpec = 
TimerSpecs.timer(TimeDomain.EVENT_TIME);
+
+      private final TupleTag<String> additionalOutput;
+
+      private TestTimerTaggedOutputDoFn(TupleTag<String> additionalOutput) {
+        this.additionalOutput = additionalOutput;
+      }
+
+      @ProcessElement
+      public void processElement(ProcessContext context, @TimerId("event") 
Timer eventTimeTimer) {
+        
eventTimeTimer.withOutputTimestamp(context.timestamp()).set(context.timestamp());
+      }
+
+      @OnTimer("event")
+      public void eventTimer(
+          OnTimerContext context, @Key String key, MultiOutputReceiver 
receiver) {
+        context.output("main:" + key);
+        context.output(additionalOutput, "output:" + key);
+        context.outputWindowedValue(
+            additionalOutput,
+            "outputWindowedValue:" + key,
+            context.timestamp(),
+            Collections.singletonList(GlobalWindow.INSTANCE),
+            PaneInfo.NO_FIRING);
+        receiver.get(additionalOutput).output("receiver:" + key);
+      }
+    }
+
+    @Test
+    public void testTimerTaggedOutputs() throws Exception {
+      Pipeline p = Pipeline.create();
+      PCollection<KV<String, String>> valuePCollection =
+          p.apply(Create.of(KV.of("unused", "unused")));
+      TupleTag<String> mainOutput = new TupleTag<String>("main") {};
+      TupleTag<String> additionalOutput = new TupleTag<String>("additional") 
{};
+      PCollectionTuple outputPCollection =
+          valuePCollection.apply(
+              TEST_TRANSFORM_ID,
+              ParDo.of(new TestTimerTaggedOutputDoFn(additionalOutput))
+                  .withOutputTags(mainOutput, 
TupleTagList.of(additionalOutput)));
+
+      SdkComponents sdkComponents = SdkComponents.create();
+      sdkComponents.registerEnvironment(Environment.getDefaultInstance());
+      RunnerApi.Pipeline pProto = PipelineTranslation.toProto(p, 
sdkComponents);
+      String outputPCollectionId =
+          sdkComponents.registerPCollection(outputPCollection.get(mainOutput));
+      String additionalPCollectionId =
+          
sdkComponents.registerPCollection(outputPCollection.get(additionalOutput));
+      RunnerApi.PTransform pTransform =
+          pProto.getComponents().getTransformsOrThrow(TEST_TRANSFORM_ID);
+
+      List<WindowedValue<String>> mainOutputValues = new ArrayList<>();
+      List<WindowedValue<String>> additionalOutputValues = new ArrayList<>();
+      PTransformRunnerFactoryTestContext context =
+          PTransformRunnerFactoryTestContext.builder(TEST_TRANSFORM_ID, 
pTransform)
+              .beamFnStateClient(new 
FakeBeamFnStateClient(StringUtf8Coder.of(), ImmutableMap.of()))
+              .processBundleInstructionId("57L")
+              .components(
+                  RunnerApi.Components.newBuilder()
+                      .putAllCoders(pProto.getComponents().getCodersMap())
+                      .putAllEnvironments(Collections.emptyMap())
+                      
.putAllWindowingStrategies(pProto.getComponents().getWindowingStrategiesMap())
+                      
.putAllPcollections(pProto.getComponentsOrBuilder().getPcollectionsMap())
+                      .build())
+              .outboundAggregators(
+                  ImmutableMap.of(
+                      ApiServiceDescriptor.getDefaultInstance(),
+                      new TestBeamFnDataOutboundAggregator(() -> "57L")))
+              
.timerApiServiceDescriptor(ApiServiceDescriptor.getDefaultInstance())
+              .build();
+      context.addPCollectionConsumer(
+          outputPCollectionId,
+          (FnDataReceiver) (FnDataReceiver<WindowedValue<String>>) 
mainOutputValues::add);
+      context.addPCollectionConsumer(
+          additionalPCollectionId,
+          (FnDataReceiver) (FnDataReceiver<WindowedValue<String>>) 
additionalOutputValues::add);
+
+      new FnApiDoFnRunner.Factory<>().addRunnerForPTransform(context);
+      Iterables.getOnlyElement(context.getStartBundleFunctions()).run();
+
+      context
+          .getIncomingTimerEndpoint("ts-event")
+          .getReceiver()
+          .accept(timerInGlobalWindow("A", new Instant(1400L), new 
Instant(2400L)));
+
+      assertThat(mainOutputValues, contains(isValueInGlobalWindow("main:A", 
new Instant(1400L))));
+      assertThat(
+          additionalOutputValues,
+          contains(
+              isValueInGlobalWindow("output:A", new Instant(1400L)),
+              isValueInGlobalWindow("outputWindowedValue:A", new 
Instant(1400L)),
+              isValueInGlobalWindow("receiver:A", new Instant(1400L))));
+
+      Iterables.getOnlyElement(context.getFinishBundleFunctions()).run();
+      Iterables.getOnlyElement(context.getTearDownFunctions()).run();
+    }
+
+    private static final Schema ROW_SCHEMA =
+        Schema.of(Schema.Field.of("field", Schema.FieldType.STRING));
+
+    private static Row row(String value) {
+      return Row.withSchema(ROW_SCHEMA).addValue(value).build();
+    }
+
+    private static class TestTimerRowOutputDoFn extends DoFn<KV<String, 
String>, Row> {
+      @TimerId("event")
+      private final TimerSpec eventTimerSpec = 
TimerSpecs.timer(TimeDomain.EVENT_TIME);
+
+      private final TupleTag<Row> mainOutput;
+      private final TupleTag<Row> additionalOutput;
+
+      private TestTimerRowOutputDoFn(TupleTag<Row> mainOutput, TupleTag<Row> 
additionalOutput) {
+        this.mainOutput = mainOutput;
+        this.additionalOutput = additionalOutput;
+      }
+
+      @ProcessElement
+      public void processElement(ProcessContext context, @TimerId("event") 
Timer eventTimeTimer) {
+        
eventTimeTimer.withOutputTimestamp(context.timestamp()).set(context.timestamp());
+      }
+
+      @OnTimer("event")
+      public void eventTimer(@Key String key, MultiOutputReceiver receiver) {
+        receiver.getRowReceiver(mainOutput).output(row("mainRow:" + key));
+        receiver.getRowReceiver(additionalOutput).output(row("taggedRow:" + 
key));
+      }
+    }
+
+    @Test
+    public void testTimerRowOutputReceivers() throws Exception {
+      Pipeline p = Pipeline.create();
+      PCollection<KV<String, String>> valuePCollection =
+          p.apply(Create.of(KV.of("unused", "unused")));
+      TupleTag<Row> mainOutput = new TupleTag<Row>("main") {};
+      TupleTag<Row> additionalOutput = new TupleTag<Row>("additional") {};
+      PCollectionTuple outputPCollection =
+          valuePCollection.apply(
+              TEST_TRANSFORM_ID,
+              ParDo.of(new TestTimerRowOutputDoFn(mainOutput, 
additionalOutput))
+                  .withOutputTags(mainOutput, 
TupleTagList.of(additionalOutput)));
+      outputPCollection.get(mainOutput).setRowSchema(ROW_SCHEMA);
+      outputPCollection.get(additionalOutput).setRowSchema(ROW_SCHEMA);
+
+      SdkComponents sdkComponents = SdkComponents.create();
+      sdkComponents.registerEnvironment(Environment.getDefaultInstance());
+      RunnerApi.Pipeline pProto = PipelineTranslation.toProto(p, 
sdkComponents);
+      String outputPCollectionId =
+          sdkComponents.registerPCollection(outputPCollection.get(mainOutput));
+      String additionalPCollectionId =
+          
sdkComponents.registerPCollection(outputPCollection.get(additionalOutput));
+      RunnerApi.PTransform pTransform =
+          pProto.getComponents().getTransformsOrThrow(TEST_TRANSFORM_ID);
+
+      List<WindowedValue<Row>> mainOutputValues = new ArrayList<>();
+      List<WindowedValue<Row>> additionalOutputValues = new ArrayList<>();
+      PTransformRunnerFactoryTestContext context =
+          PTransformRunnerFactoryTestContext.builder(TEST_TRANSFORM_ID, 
pTransform)
+              .beamFnStateClient(new 
FakeBeamFnStateClient(StringUtf8Coder.of(), ImmutableMap.of()))
+              .processBundleInstructionId("57L")
+              .components(
+                  RunnerApi.Components.newBuilder()
+                      .putAllCoders(pProto.getComponents().getCodersMap())
+                      .putAllEnvironments(Collections.emptyMap())
+                      
.putAllWindowingStrategies(pProto.getComponents().getWindowingStrategiesMap())
+                      
.putAllPcollections(pProto.getComponentsOrBuilder().getPcollectionsMap())
+                      .build())
+              .outboundAggregators(
+                  ImmutableMap.of(
+                      ApiServiceDescriptor.getDefaultInstance(),
+                      new TestBeamFnDataOutboundAggregator(() -> "57L")))
+              
.timerApiServiceDescriptor(ApiServiceDescriptor.getDefaultInstance())
+              .build();
+      context.addPCollectionConsumer(
+          outputPCollectionId,
+          (FnDataReceiver) (FnDataReceiver<WindowedValue<Row>>) 
mainOutputValues::add);
+      context.addPCollectionConsumer(
+          additionalPCollectionId,
+          (FnDataReceiver) (FnDataReceiver<WindowedValue<Row>>) 
additionalOutputValues::add);
+
+      new FnApiDoFnRunner.Factory<>().addRunnerForPTransform(context);
+      Iterables.getOnlyElement(context.getStartBundleFunctions()).run();
+
+      context
+          .getIncomingTimerEndpoint("ts-event")
+          .getReceiver()
+          .accept(timerInGlobalWindow("A", new Instant(1400L), new 
Instant(2400L)));
+
+      assertThat(
+          mainOutputValues, contains(isValueInGlobalWindow(row("mainRow:A"), 
new Instant(1400L))));
+      assertThat(
+          additionalOutputValues,
+          contains(isValueInGlobalWindow(row("taggedRow:A"), new 
Instant(1400L))));
+
+      Iterables.getOnlyElement(context.getFinishBundleFunctions()).run();
+      Iterables.getOnlyElement(context.getTearDownFunctions()).run();
+    }
+
+    private static class TestWindowExpirationDoFn extends DoFn<KV<String, 
String>, String> {
+      @StateId("bag")
+      private final StateSpec<BagState<String>> bagStateSpec = 
StateSpecs.bag(StringUtf8Coder.of());
+
+      private final TupleTag<String> additionalOutput;
+
+      private TestWindowExpirationDoFn(TupleTag<String> additionalOutput) {
+        this.additionalOutput = additionalOutput;
+      }
+
+      @ProcessElement
+      public void processElement(
+          ProcessContext context, @StateId("bag") BagState<String> bagState) {
+        bagState.add(context.element().getValue());
+      }
+
+      @OnWindowExpiration
+      public void onWindowExpiration(OnWindowExpirationContext context, @Key 
String key) {
+        context.output("main:" + key);
+        context.output(additionalOutput, "output:" + key);
+      }
+    }
+
+    @Test
+    public void testOnWindowExpirationTaggedOutputs() throws Exception {
+      Pipeline p = Pipeline.create();
+      PCollection<KV<String, String>> valuePCollection =
+          p.apply(Create.of(KV.of("unused", "unused")));
+      TupleTag<String> mainOutput = new TupleTag<String>("main") {};
+      TupleTag<String> additionalOutput = new TupleTag<String>("additional") 
{};
+      PCollectionTuple outputPCollection =
+          valuePCollection.apply(
+              TEST_TRANSFORM_ID,
+              ParDo.of(new TestWindowExpirationDoFn(additionalOutput))
+                  .withOutputTags(mainOutput, 
TupleTagList.of(additionalOutput)));
+
+      SdkComponents sdkComponents = SdkComponents.create();
+      sdkComponents.registerEnvironment(Environment.getDefaultInstance());
+      RunnerApi.Pipeline pProto = PipelineTranslation.toProto(p, 
sdkComponents);
+      String outputPCollectionId =
+          sdkComponents.registerPCollection(outputPCollection.get(mainOutput));
+      String additionalPCollectionId =
+          
sdkComponents.registerPCollection(outputPCollection.get(additionalOutput));
+      RunnerApi.PTransform pTransform =
+          pProto.getComponents().getTransformsOrThrow(TEST_TRANSFORM_ID);
+      String onWindowExpirationFamilyId =
+          RunnerApi.ParDoPayload.parseFrom(pTransform.getSpec().getPayload())
+              .getOnWindowExpirationTimerFamilySpec();
+
+      List<WindowedValue<String>> mainOutputValues = new ArrayList<>();
+      List<WindowedValue<String>> additionalOutputValues = new ArrayList<>();
+      PTransformRunnerFactoryTestContext context =
+          PTransformRunnerFactoryTestContext.builder(TEST_TRANSFORM_ID, 
pTransform)
+              .beamFnStateClient(new 
FakeBeamFnStateClient(StringUtf8Coder.of(), ImmutableMap.of()))
+              .processBundleInstructionId("57L")
+              .components(
+                  RunnerApi.Components.newBuilder()
+                      .putAllCoders(pProto.getComponents().getCodersMap())
+                      .putAllEnvironments(Collections.emptyMap())
+                      
.putAllWindowingStrategies(pProto.getComponents().getWindowingStrategiesMap())
+                      
.putAllPcollections(pProto.getComponentsOrBuilder().getPcollectionsMap())
+                      .build())
+              .outboundAggregators(
+                  ImmutableMap.of(
+                      ApiServiceDescriptor.getDefaultInstance(),
+                      new TestBeamFnDataOutboundAggregator(() -> "57L")))
+              
.timerApiServiceDescriptor(ApiServiceDescriptor.getDefaultInstance())
+              .build();
+      context.addPCollectionConsumer(
+          outputPCollectionId,
+          (FnDataReceiver) (FnDataReceiver<WindowedValue<String>>) 
mainOutputValues::add);
+      context.addPCollectionConsumer(
+          additionalPCollectionId,
+          (FnDataReceiver) (FnDataReceiver<WindowedValue<String>>) 
additionalOutputValues::add);
+
+      new FnApiDoFnRunner.Factory<>().addRunnerForPTransform(context);
+      Iterables.getOnlyElement(context.getStartBundleFunctions()).run();
+
+      context
+          .getIncomingTimerEndpoint(onWindowExpirationFamilyId)
+          .getReceiver()
+          .accept(timerInGlobalWindow("A", new Instant(1400L), new 
Instant(2400L)));
+
+      assertThat(mainOutputValues, contains(isValueInGlobalWindow("main:A", 
new Instant(1400L))));
+      assertThat(
+          additionalOutputValues, contains(isValueInGlobalWindow("output:A", 
new Instant(1400L))));
+
+      Iterables.getOnlyElement(context.getFinishBundleFunctions()).run();
+      Iterables.getOnlyElement(context.getTearDownFunctions()).run();
+    }
+
     private <K> org.apache.beam.sdk.util.construction.Timer<K> 
timerInGlobalWindow(
         K userKey, Instant holdTimestamp, Instant fireTimestamp) {
       return dynamicTimerInGlobalWindow(userKey, "", holdTimestamp, 
fireTimestamp);

Reply via email to