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

shunping 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 03db1a07096 Fix DataflowOutputCounter calculation for 
ValueInEmptyWindows (#39487)
03db1a07096 is described below

commit 03db1a070961eb8fd5a04d9124a3b4423919672b
Author: Shunping Huang <[email protected]>
AuthorDate: Thu Jul 30 17:47:46 2026 -0400

    Fix DataflowOutputCounter calculation for ValueInEmptyWindows (#39487)
    
    * Fix DataflowOutputCounter calculation for ValueInEmptyWindows
    
    When processing shuffle or streaming data in Dataflow Legacy Runner
    (e.g., from GroupingShuffleReader or WindowingWindmillReader),
    KeyedWorkItems are wrapped inside a ValueInEmptyWindows (windows.size() == 
0).
    
    Previously, DataflowOutputCounter.update() counted these as 1 element.
    This caused inaccurate element counts because:
    1. A KeyedWorkItem can contain multiple elements.
    2. Elements may belong to multiple windows and need to be fanned out 
accordingly.
    3. KeyedWorkItems containing only timers were incorrectly incrementing 
element counters.
    
    * Address non keyedworkitems
    
    * Spotless
    
    * Add elementWindowsIterable to only decode window metadata and use it in 
DataflowOutputCounter
    
    * Use the elementWindowsIterable in ReduceFnRunner
    
    * Refactor: separate batch and streaming DataflowOutputCounter 
implementations
    
    * Minor change on tests.
    
    * Address reviewer comments
    
    * Spotless
    
    * Remove unnecessary comments
    
    * Address comments
---
 .../core/GroupAlsoByWindowViaWindowSetNewDoFn.java |   2 +-
 .../apache/beam/runners/core/KeyedWorkItem.java    |   9 ++
 .../apache/beam/runners/core/ReduceFnRunner.java   |  18 +++-
 .../dataflow/worker/DataflowOutputCounter.java     |  68 ++++++++++---
 .../worker/IntrinsicMapTaskExecutorFactory.java    |  11 ++-
 .../dataflow/worker/SimpleParDoFnHelpers.java      |   5 +-
 .../StreamingGroupAlsoByWindowViaWindowSetFn.java  |   2 +-
 .../dataflow/worker/WindmillKeyedWorkItem.java     |  26 ++++-
 .../dataflow/worker/DataflowOutputCounterTest.java | 108 +++++++++++++++++++++
 .../IntrinsicMapTaskExecutorFactoryTest.java       |  14 ++-
 10 files changed, 234 insertions(+), 29 deletions(-)

diff --git 
a/runners/core-java/src/main/java/org/apache/beam/runners/core/GroupAlsoByWindowViaWindowSetNewDoFn.java
 
b/runners/core-java/src/main/java/org/apache/beam/runners/core/GroupAlsoByWindowViaWindowSetNewDoFn.java
index f242c7d1000..349e109930f 100644
--- 
a/runners/core-java/src/main/java/org/apache/beam/runners/core/GroupAlsoByWindowViaWindowSetNewDoFn.java
+++ 
b/runners/core-java/src/main/java/org/apache/beam/runners/core/GroupAlsoByWindowViaWindowSetNewDoFn.java
@@ -109,7 +109,7 @@ public class GroupAlsoByWindowViaWindowSetNewDoFn<
             reduceFn,
             c.getPipelineOptions());
 
-    reduceFnRunner.processElements(keyedWorkItem.elementsIterable());
+    reduceFnRunner.processElements(keyedWorkItem);
     reduceFnRunner.onTimers(keyedWorkItem.timersIterable());
     reduceFnRunner.persist();
   }
diff --git 
a/runners/core-java/src/main/java/org/apache/beam/runners/core/KeyedWorkItem.java
 
b/runners/core-java/src/main/java/org/apache/beam/runners/core/KeyedWorkItem.java
index 4901c5cbed5..2be8b079030 100644
--- 
a/runners/core-java/src/main/java/org/apache/beam/runners/core/KeyedWorkItem.java
+++ 
b/runners/core-java/src/main/java/org/apache/beam/runners/core/KeyedWorkItem.java
@@ -35,4 +35,13 @@ public interface KeyedWorkItem<K, ElemT> {
 
   /** Returns an iterable containing the elements. */
   Iterable<WindowedValue<ElemT>> elementsIterable();
+
+  /**
+   * Returns an iterable containing windowed values without guaranteeing 
element payload decoding.
+   * Useful for lightweight inspection of windowing metadata without payload 
deserialization
+   * overhead.
+   */
+  default Iterable<WindowedValue<?>> elementWindowsIterable() {
+    return (Iterable) elementsIterable();
+  }
 }
diff --git 
a/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java
 
b/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java
index 7fe3b711aa0..e49c858393f 100644
--- 
a/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java
+++ 
b/runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java
@@ -60,6 +60,7 @@ import 
org.apache.beam.sdk.values.WindowingStrategy.AccumulationMode;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.FluentIterable;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableSet;
+import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Iterables;
 import org.checkerframework.checker.nullness.qual.Nullable;
 import org.joda.time.Duration;
 import org.joda.time.Instant;
@@ -361,13 +362,24 @@ public class ReduceFnRunner<K, InputT, OutputT, W extends 
BoundedWindow> {
    *       setting holds, and invoking {@link ReduceFn#onTrigger}.
    * </ol>
    */
+  public void processElements(KeyedWorkItem<?, InputT> keyedWorkItem) throws 
Exception {
+    processElementsInternal(
+        keyedWorkItem.elementWindowsIterable(), 
keyedWorkItem.elementsIterable());
+  }
+
   public void processElements(Iterable<WindowedValue<InputT>> values) throws 
Exception {
-    if (!values.iterator().hasNext()) {
+    processElementsInternal(values, values);
+  }
+
+  private void processElementsInternal(
+      Iterable<? extends WindowedValue<?>> elementWindows, 
Iterable<WindowedValue<InputT>> values)
+      throws Exception {
+    if (Iterables.isEmpty(elementWindows)) {
       return;
     }
 
     // Determine all the windows for elements.
-    Set<W> windows = collectWindows(values);
+    Set<W> windows = collectWindows(elementWindows);
     // If an incoming element introduces a new window, attempt to merge it 
into an existing
     // window eagerly.
     Map<W, W> windowToMergeResult = mergeWindows(windows);
@@ -426,7 +438,7 @@ public class ReduceFnRunner<K, InputT, OutputT, W extends 
BoundedWindow> {
   }
 
   /** Extract the windows associated with the values. */
-  private Set<W> collectWindows(Iterable<WindowedValue<InputT>> values) throws 
Exception {
+  private Set<W> collectWindows(Iterable<? extends WindowedValue<?>> values) 
throws Exception {
     Set<W> windows = new HashSet<>();
     for (WindowedValue<?> value : values) {
       for (BoundedWindow untypedWindow : value.getWindows()) {
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounter.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounter.java
index 7c5859a9d32..1a927c03c61 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounter.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounter.java
@@ -18,12 +18,14 @@
 package org.apache.beam.runners.dataflow.worker;
 
 import org.apache.beam.runners.core.ElementByteSizeObservable;
+import org.apache.beam.runners.core.KeyedWorkItem;
 import org.apache.beam.runners.dataflow.worker.counters.Counter;
 import org.apache.beam.runners.dataflow.worker.counters.CounterFactory;
 import org.apache.beam.runners.dataflow.worker.counters.CounterName;
 import org.apache.beam.runners.dataflow.worker.counters.NameContext;
 import 
org.apache.beam.runners.dataflow.worker.util.common.worker.ElementCounter;
 import 
org.apache.beam.runners.dataflow.worker.util.common.worker.OutputObjectAndByteCounter;
+import org.apache.beam.sdk.annotations.Internal;
 import org.apache.beam.sdk.values.WindowedValue;
 import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.VisibleForTesting;
 
@@ -33,6 +35,7 @@ import 
org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.annotations.Vi
 @SuppressWarnings({
   "nullness" // TODO(https://github.com/apache/beam/issues/20497)
 })
+@Internal
 public class DataflowOutputCounter implements ElementCounter {
   /** Number of logical element and single window pairs that were processed. */
   private static final String ELEMENT_COUNTER_NAME = "-ElementCount";
@@ -41,20 +44,36 @@ public class DataflowOutputCounter implements 
ElementCounter {
 
   private OutputObjectAndByteCounter objectAndByteCounter;
   private Counter<Long, ?> elementCount;
+  private final boolean isStreaming;
 
-  public DataflowOutputCounter(
-      String outputName, CounterFactory counterFactory, NameContext 
nameContext) {
-    this(outputName, null, counterFactory, nameContext);
+  public static DataflowOutputCounter create(
+      String outputName,
+      ElementByteSizeObservable<?> elementByteSizeObservable,
+      CounterFactory counterFactory,
+      NameContext nameContext,
+      boolean isStreaming) {
+    return new DataflowOutputCounter(
+        outputName, elementByteSizeObservable, counterFactory, nameContext, 
isStreaming);
+  }
+
+  public static DataflowOutputCounter create(
+      String outputName,
+      CounterFactory counterFactory,
+      NameContext nameContext,
+      boolean isStreaming) {
+    return new DataflowOutputCounter(outputName, null, counterFactory, 
nameContext, isStreaming);
   }
 
-  public DataflowOutputCounter(
+  private DataflowOutputCounter(
       String outputName,
       ElementByteSizeObservable<?> elementByteSizeObservable,
       CounterFactory counterFactory,
-      NameContext nameContext) {
-    objectAndByteCounter =
+      NameContext nameContext,
+      boolean isStreaming) {
+    this.isStreaming = isStreaming;
+    this.objectAndByteCounter =
         new OutputObjectAndByteCounter(elementByteSizeObservable, 
counterFactory, nameContext);
-    objectAndByteCounter.countMeanByte(outputName + MEAN_BYTE_COUNTER_NAME);
+    this.objectAndByteCounter.countMeanByte(outputName + 
MEAN_BYTE_COUNTER_NAME);
     createElementCounter(counterFactory, outputName + ELEMENT_COUNTER_NAME);
   }
 
@@ -63,15 +82,42 @@ public class DataflowOutputCounter implements 
ElementCounter {
     objectAndByteCounter.update(elem);
     long windowsSize = ((WindowedValue<?>) elem).getWindows().size();
     if (windowsSize == 0) {
-      // GroupingShuffleReader produces ValueInEmptyWindows.
-      // For now, we count the element at least once to keep the current 
counter
-      // behavior.
-      elementCount.addValue(1L);
+      updateEmptyWindows((WindowedValue<?>) elem);
     } else {
+      // Standard WindowedValue.
       elementCount.addValue(windowsSize);
     }
   }
 
+  private void updateEmptyWindows(WindowedValue<?> elem) {
+    if (isStreaming) {
+      Object value = elem.getValue();
+      if (value instanceof KeyedWorkItem<?, ?>) {
+        // KeyedWorkItem wrapped in ValueInEmptyWindows
+        // (e.g. WindowingWindmillReader for Streaming GBK)
+        KeyedWorkItem<?, ?> keyedWorkItem = (KeyedWorkItem<?, ?>) value;
+        long totalElementCount = 0;
+        // Iterate through elementWindowsIterable and ignore timers in 
KeyedWorkItem.
+        // Uses lightweight metadata-only iteration without payload 
deserialization overhead.
+        for (WindowedValue<?> element : 
keyedWorkItem.elementWindowsIterable()) {
+          long elementWindowsSize = element.getWindows().size();
+          // Fan out for windows.
+          totalElementCount += (elementWindowsSize == 0 ? 1L : 
elementWindowsSize);
+        }
+        elementCount.addValue(totalElementCount);
+      } else {
+        // NOTE: in streaming mode, this should not normally happen.
+        // Counting as 1 element serves as a fallback to maintain counter 
behavior without failing
+        // execution.
+        elementCount.addValue(1L);
+      }
+    } else {
+      // Non-KeyedWorkItem wrapped in ValueInEmptyWindows
+      // (e.g. GroupingShuffleReader KV output for Batch GBK)
+      elementCount.addValue(1L);
+    }
+  }
+
   @Override
   public void finishLazyUpdate(Object elem) {
     objectAndByteCounter.finishLazyUpdate(elem);
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactory.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactory.java
index d3f2aacc74d..3ea29787eb3 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactory.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactory.java
@@ -63,6 +63,7 @@ import org.apache.beam.sdk.coders.Coder;
 import org.apache.beam.sdk.coders.KvCoder;
 import org.apache.beam.sdk.fn.IdGenerator;
 import org.apache.beam.sdk.options.PipelineOptions;
+import org.apache.beam.sdk.options.StreamingOptions;
 import org.apache.beam.sdk.util.common.ElementByteSizeObserver;
 import org.apache.beam.sdk.values.TupleTag;
 import org.apache.beam.sdk.values.WindowedValues.WindowedValueCoder;
@@ -102,8 +103,9 @@ public class IntrinsicMapTaskExecutorFactory implements 
DataflowMapTaskExecutorF
       IdGenerator idGenerator) {
 
     // Swap out all the InstructionOutput nodes with OutputReceiver nodes
+    boolean isStreaming = options.as(StreamingOptions.class).isStreaming();
     Networks.replaceDirectedNetworkNodes(
-        network, createOutputReceiversTransform(stageName, counterSet));
+        network, createOutputReceiversTransform(stageName, counterSet, 
isStreaming));
 
     // Swap out all the ParallelInstruction nodes with Operation nodes. While 
updating the network,
     // we keep track of
@@ -345,7 +347,7 @@ public class IntrinsicMapTaskExecutorFactory implements 
DataflowMapTaskExecutorF
    * Returns a function which can convert {@link InstructionOutput}s into 
{@link OutputReceiver}s.
    */
   static Function<Node, Node> createOutputReceiversTransform(
-      final String stageName, final CounterFactory counterFactory) {
+      final String stageName, final CounterFactory counterFactory, final 
boolean isStreaming) {
     return new 
TypeSafeNodeFunction<InstructionOutputNode>(InstructionOutputNode.class) {
       @Override
       public Node typedApply(InstructionOutputNode input) {
@@ -355,7 +357,7 @@ public class IntrinsicMapTaskExecutorFactory implements 
DataflowMapTaskExecutorF
             
CloudObjects.coderFromCloudObject(CloudObject.fromSpec(cloudOutput.getCodec()));
 
         ElementCounter outputCounter =
-            new DataflowOutputCounter(
+            DataflowOutputCounter.create(
                 cloudOutput.getName(),
                 new ElementByteSizeObservableCoder<>(coder),
                 counterFactory,
@@ -363,7 +365,8 @@ public class IntrinsicMapTaskExecutorFactory implements 
DataflowMapTaskExecutorF
                     stageName,
                     cloudOutput.getOriginalName(),
                     cloudOutput.getSystemName(),
-                    cloudOutput.getName()));
+                    cloudOutput.getName()),
+                isStreaming);
         outputReceiver.addOutputCounter(outputCounter);
 
         return OutputReceiverNode.create(outputReceiver, coder, 
input.getPcollectionId());
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpers.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpers.java
index 964cf2323d5..15bfba9bbc4 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpers.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/SimpleParDoFnHelpers.java
@@ -190,9 +190,10 @@ class SimpleParDoFnHelpers<InputT, OutputT, W extends 
BoundedWindow> {
               // doesn't today.)
               OutputReceiver undeclaredReceiver = new OutputReceiver();
 
+              boolean isStreaming = 
options.as(StreamingOptions.class).isStreaming();
               ElementCounter outputCounter =
-                  new DataflowOutputCounter(
-                      outputName, counterFactory, 
stepContext.getNameContext());
+                  DataflowOutputCounter.create(
+                      outputName, counterFactory, 
stepContext.getNameContext(), isStreaming);
               undeclaredReceiver.addOutputCounter(outputCounter);
               undeclaredOutputs.put(tag, undeclaredReceiver);
               receiver = undeclaredReceiver;
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowViaWindowSetFn.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowViaWindowSetFn.java
index ec36644d1e6..a183df19b6e 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowViaWindowSetFn.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/StreamingGroupAlsoByWindowViaWindowSetFn.java
@@ -93,7 +93,7 @@ public class StreamingGroupAlsoByWindowViaWindowSetFn<K, 
InputT, OutputT, W exte
             reduceFn,
             options);
 
-    reduceFnRunner.processElements(keyedWorkItem.elementsIterable());
+    reduceFnRunner.processElements(keyedWorkItem);
     reduceFnRunner.onTimers(keyedWorkItem.timersIterable());
     reduceFnRunner.persist();
   }
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java
 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java
index 82116e0b2d8..ff5be071ede 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/main/java/org/apache/beam/runners/dataflow/worker/WindmillKeyedWorkItem.java
@@ -140,6 +140,16 @@ public class WindmillKeyedWorkItem<K, ElemT> implements 
KeyedWorkItem<K, ElemT>
   }
 
   private @Nullable WindowedValue<ElemT> parseElem(Windmill.Message message) {
+    return parseElemInternal(message, true);
+  }
+
+  private @Nullable WindowedValue<?> parseElemWindowOnly(Windmill.Message 
message) {
+    return parseElemInternal(message, false);
+  }
+
+  @SuppressWarnings("nullness")
+  private @Nullable WindowedValue<ElemT> parseElemInternal(
+      Windmill.Message message, boolean parseValue) {
     try {
       Instant timestamp = 
WindmillTimeUtils.windmillToHarnessTimestamp(message.getTimestamp());
       Collection<? extends BoundedWindow> windows =
@@ -162,8 +172,11 @@ public class WindmillKeyedWorkItem<K, ElemT> implements 
KeyedWorkItem<K, ElemT>
         valueKind = 
WindmillValueKindHelper.fromProto(elementMetadata.getValueKind());
         openTelemetryContext = 
WindmillOpenTelemetryContextPropagator.read(elementMetadata);
       }
-      InputStream inputStream = message.getData().newInput();
-      ElemT value = valueCoder.decode(inputStream, Coder.Context.OUTER);
+      ElemT value = null;
+      if (parseValue) {
+        InputStream inputStream = message.getData().newInput();
+        value = valueCoder.decode(inputStream, Coder.Context.OUTER);
+      }
       return WindowedValues.of(
           value,
           timestamp,
@@ -187,6 +200,15 @@ public class WindmillKeyedWorkItem<K, ElemT> implements 
KeyedWorkItem<K, ElemT>
     }
   }
 
+  @Override
+  @SuppressWarnings("nullness")
+  public Iterable<WindowedValue<?>> elementWindowsIterable() {
+    return FluentIterable.from(workItem.getMessageBundlesList())
+        .transformAndConcat(Windmill.InputMessageBundle::getMessagesList)
+        .transform(this::parseElemWindowOnly)
+        .filter(Objects::nonNull);
+  }
+
   @Override
   @SuppressWarnings("nullness")
   public Iterable<WindowedValue<ElemT>> elementsIterable() {
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounterTest.java
 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounterTest.java
new file mode 100644
index 00000000000..b5c49ee639b
--- /dev/null
+++ 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/DataflowOutputCounterTest.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.runners.dataflow.worker;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import java.util.Arrays;
+import org.apache.beam.runners.core.KeyedWorkItem;
+import org.apache.beam.runners.dataflow.worker.counters.CounterName;
+import org.apache.beam.runners.dataflow.worker.counters.CounterSet;
+import org.apache.beam.runners.dataflow.worker.counters.NameContext;
+import org.apache.beam.runners.dataflow.worker.util.ValueInEmptyWindows;
+import org.apache.beam.sdk.values.KV;
+import org.apache.beam.sdk.values.WindowedValue;
+import org.apache.beam.sdk.values.WindowedValues;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link DataflowOutputCounter}. */
+@RunWith(JUnit4.class)
+public class DataflowOutputCounterTest {
+  private static final String OUTPUT_NAME = "test_output";
+  private CounterSet counterSet;
+  private NameContext nameContext;
+
+  @Before
+  public void setUp() {
+    counterSet = new CounterSet();
+    nameContext = NameContext.create("stage", "original", "system", 
OUTPUT_NAME);
+  }
+
+  @Test
+  public void testBatchOutputCounterWithEmptyWindows() throws Exception {
+    DataflowOutputCounter batchCounter =
+        DataflowOutputCounter.create(OUTPUT_NAME, counterSet, nameContext, 
false);
+
+    ValueInEmptyWindows<KV<String, String>> shuffleValue =
+        new ValueInEmptyWindows<>(KV.of("key", "value"));
+    batchCounter.update(shuffleValue);
+
+    long elementCount =
+        (Long)
+            counterSet
+                .getExistingCounter(
+                    
CounterName.named(DataflowOutputCounter.getElementCounterName(OUTPUT_NAME)))
+                .getAggregate();
+    assertEquals(1L, elementCount);
+  }
+
+  @Test
+  public void testStreamingOutputCounterWithKeyedWorkItem() throws Exception {
+    DataflowOutputCounter streamingCounter =
+        DataflowOutputCounter.create(OUTPUT_NAME, counterSet, nameContext, 
true);
+
+    KeyedWorkItem<String, String> kwi = mock(KeyedWorkItem.class);
+    WindowedValue<String> element1 = WindowedValues.valueInGlobalWindow("v1");
+    WindowedValue<String> element2 = WindowedValues.valueInGlobalWindow("v2");
+    doReturn(Arrays.asList(element1, 
element2)).when(kwi).elementWindowsIterable();
+
+    ValueInEmptyWindows<KeyedWorkItem<String, String>> streamingValue =
+        new ValueInEmptyWindows<>(kwi);
+    streamingCounter.update(streamingValue);
+
+    long elementCount =
+        (Long)
+            counterSet
+                .getExistingCounter(
+                    
CounterName.named(DataflowOutputCounter.getElementCounterName(OUTPUT_NAME)))
+                .getAggregate();
+    assertEquals(2L, elementCount);
+  }
+
+  @Test
+  public void testStandardWindowedValueCounting() throws Exception {
+    DataflowOutputCounter counter =
+        DataflowOutputCounter.create(OUTPUT_NAME, counterSet, nameContext, 
false);
+
+    WindowedValue<String> standardValue = 
WindowedValues.valueInGlobalWindow("v1");
+    counter.update(standardValue);
+
+    long elementCount =
+        (Long)
+            counterSet
+                .getExistingCounter(
+                    
CounterName.named(DataflowOutputCounter.getElementCounterName(OUTPUT_NAME)))
+                .getAggregate();
+    assertEquals(1L, elementCount);
+  }
+}
diff --git 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactoryTest.java
 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactoryTest.java
index 3443ae0022b..d3a424758f6 100644
--- 
a/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactoryTest.java
+++ 
b/runners/google-cloud-dataflow-java/worker/src/test/java/org/apache/beam/runners/dataflow/worker/IntrinsicMapTaskExecutorFactoryTest.java
@@ -330,7 +330,8 @@ public class IntrinsicMapTaskExecutorFactoryTest {
     when(network.successors(instructionNode))
         .thenReturn(
             ImmutableSet.<Node>of(
-                
IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, 
counterSet)
+                IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(
+                        STAGE, counterSet, false)
                     .apply(
                         InstructionOutputNode.create(
                             
instructionNode.getParallelInstruction().getOutputs().get(0),
@@ -535,7 +536,7 @@ public class IntrinsicMapTaskExecutorFactoryTest {
             ExecutionLocation.UNKNOWN);
 
     Node outputReceiverNode =
-        IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, 
counterSet)
+        IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, 
counterSet, false)
             .apply(
                 InstructionOutputNode.create(
                     
instructionNode.getParallelInstruction().getOutputs().get(0), PCOLLECTION_ID));
@@ -614,7 +615,8 @@ public class IntrinsicMapTaskExecutorFactoryTest {
     when(network.successors(instructionNode))
         .thenReturn(
             ImmutableSet.<Node>of(
-                
IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, 
counterSet)
+                IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(
+                        STAGE, counterSet, false)
                     .apply(
                         InstructionOutputNode.create(
                             
instructionNode.getParallelInstruction().getOutputs().get(0),
@@ -669,7 +671,8 @@ public class IntrinsicMapTaskExecutorFactoryTest {
     when(network.successors(instructionNode))
         .thenReturn(
             ImmutableSet.<Node>of(
-                
IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, 
counterSet)
+                IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(
+                        STAGE, counterSet, false)
                     .apply(
                         InstructionOutputNode.create(
                             
instructionNode.getParallelInstruction().getOutputs().get(0),
@@ -750,7 +753,8 @@ public class IntrinsicMapTaskExecutorFactoryTest {
     when(network.successors(instructionNode))
         .thenReturn(
             ImmutableSet.<Node>of(
-                
IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(STAGE, 
counterSet)
+                IntrinsicMapTaskExecutorFactory.createOutputReceiversTransform(
+                        STAGE, counterSet, false)
                     .apply(
                         InstructionOutputNode.create(
                             
instructionNode.getParallelInstruction().getOutputs().get(0),

Reply via email to