Skip to content

Commit 1b54ca3

Browse files
committed
Refactor metadata propagation in ReduceFnRunner to support extensible PipelineMetadata
1 parent 63f6985 commit 1b54ca3

9 files changed

Lines changed: 430 additions & 28 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.runners.core;
19+
20+
import com.google.auto.value.AutoValue;
21+
import java.io.IOException;
22+
import java.io.InputStream;
23+
import java.io.OutputStream;
24+
import org.apache.beam.model.fnexecution.v1.BeamFnApi;
25+
import org.apache.beam.sdk.coders.AtomicCoder;
26+
import org.apache.beam.sdk.values.CausedByDrain;
27+
28+
/**
29+
* Encapsulates metadata that propagates with elements in the pipeline.
30+
*
31+
* <p>This metadata is sent along with elements. It currently includes fields like {@link
32+
* CausedByDrain}, and is designed to be extensible to support future metadata fields such as
33+
* OpenTelemetry context or CDC (Change Data Capture) kind.
34+
*
35+
* <p>The purpose of this class is to group targeted metadata fields together. This makes it easier
36+
* to define combination strategies (e.g., when accumulating state in {@code ReduceFnRunner}) when
37+
* multiple elements are merged or grouped, without having to extend method signatures or state
38+
* handling for every new metadata field.
39+
*/
40+
@AutoValue
41+
public abstract class CombinedMetadata {
42+
public abstract CausedByDrain causedByDrain();
43+
44+
public static CombinedMetadata create(CausedByDrain causedByDrain) {
45+
return new AutoValue_CombinedMetadata(causedByDrain);
46+
}
47+
48+
public static CombinedMetadata createDefault() {
49+
return create(CausedByDrain.NORMAL);
50+
}
51+
52+
public static class Coder extends AtomicCoder<CombinedMetadata> {
53+
private static final Coder INSTANCE = new Coder();
54+
55+
public static Coder of() {
56+
return INSTANCE;
57+
}
58+
59+
@Override
60+
public void encode(CombinedMetadata value, OutputStream outStream) throws IOException {
61+
BeamFnApi.Elements.ElementMetadata proto =
62+
BeamFnApi.Elements.ElementMetadata.newBuilder()
63+
.setDrain(
64+
value.causedByDrain() == CausedByDrain.CAUSED_BY_DRAIN
65+
? BeamFnApi.Elements.DrainMode.Enum.DRAINING
66+
: BeamFnApi.Elements.DrainMode.Enum.NOT_DRAINING)
67+
.build();
68+
proto.writeDelimitedTo(outStream);
69+
}
70+
71+
@Override
72+
public CombinedMetadata decode(InputStream inStream) throws IOException {
73+
BeamFnApi.Elements.ElementMetadata proto =
74+
BeamFnApi.Elements.ElementMetadata.parseDelimitedFrom(inStream);
75+
if (proto == null) {
76+
return CombinedMetadata.createDefault();
77+
}
78+
79+
CausedByDrain causedByDrain =
80+
proto.getDrain() == BeamFnApi.Elements.DrainMode.Enum.DRAINING
81+
? CausedByDrain.CAUSED_BY_DRAIN
82+
: CausedByDrain.NORMAL;
83+
84+
return CombinedMetadata.create(causedByDrain);
85+
}
86+
}
87+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.runners.core;
19+
20+
import org.apache.beam.sdk.transforms.Combine.CombineFn;
21+
import org.apache.beam.sdk.values.CausedByDrain;
22+
23+
/** Combiner for CombinedMetadata. */
24+
class CombinedMetadataCombiner
25+
extends CombineFn<CombinedMetadata, CombinedMetadata, CombinedMetadata> {
26+
private static final CombinedMetadataCombiner INSTANCE = new CombinedMetadataCombiner();
27+
28+
public static CombinedMetadataCombiner of() {
29+
return INSTANCE;
30+
}
31+
32+
@Override
33+
public CombinedMetadata createAccumulator() {
34+
return CombinedMetadata.create(CausedByDrainCombiner.of().createAccumulator());
35+
}
36+
37+
@Override
38+
public CombinedMetadata addInput(CombinedMetadata accumulator, CombinedMetadata input) {
39+
return CombinedMetadata.create(
40+
CausedByDrainCombiner.of().addInput(accumulator.causedByDrain(), input.causedByDrain()));
41+
}
42+
43+
@Override
44+
public CombinedMetadata mergeAccumulators(Iterable<CombinedMetadata> accumulators) {
45+
CombinedMetadata result = createAccumulator();
46+
for (CombinedMetadata accum : accumulators) {
47+
result = addInput(result, accum);
48+
}
49+
return result;
50+
}
51+
52+
@Override
53+
public CombinedMetadata extractOutput(CombinedMetadata accumulator) {
54+
return accumulator;
55+
}
56+
57+
/** Combiner for CausedByDrain metadata. */
58+
static class CausedByDrainCombiner implements MetadataCombiner<CausedByDrain> {
59+
private static final CausedByDrainCombiner INSTANCE = new CausedByDrainCombiner();
60+
61+
public static CausedByDrainCombiner of() {
62+
return INSTANCE;
63+
}
64+
65+
@Override
66+
public CausedByDrain createAccumulator() {
67+
return CausedByDrain.NORMAL;
68+
}
69+
70+
@Override
71+
public CausedByDrain addInput(CausedByDrain current, CausedByDrain input) {
72+
if (current == CausedByDrain.CAUSED_BY_DRAIN || input == CausedByDrain.CAUSED_BY_DRAIN) {
73+
return CausedByDrain.CAUSED_BY_DRAIN;
74+
}
75+
return CausedByDrain.NORMAL;
76+
}
77+
}
78+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
package org.apache.beam.runners.core;
19+
20+
/** Interface for combining pipeline metadata. */
21+
interface MetadataCombiner<T> {
22+
T createAccumulator();
23+
24+
T addInput(T accumulator, T input);
25+
}

runners/core-java/src/main/java/org/apache/beam/runners/core/ReduceFnRunner.java

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.apache.beam.sdk.metrics.Counter;
3939
import org.apache.beam.sdk.metrics.Metrics;
4040
import org.apache.beam.sdk.options.PipelineOptions;
41+
import org.apache.beam.sdk.state.CombiningState;
4142
import org.apache.beam.sdk.state.TimeDomain;
4243
import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
4344
import org.apache.beam.sdk.transforms.windowing.PaneInfo;
@@ -107,6 +108,12 @@ public class ReduceFnRunner<K, InputT, OutputT, W extends BoundedWindow> {
107108
* <li>It uses discarding or accumulation mode according to the {@link WindowingStrategy}.
108109
* </ul>
109110
*/
111+
static final StateTag<CombiningState<CombinedMetadata, CombinedMetadata, CombinedMetadata>>
112+
METADATA_TAG =
113+
StateTags.makeSystemTagInternal(
114+
StateTags.combiningValue(
115+
"combinedMetadata", CombinedMetadata.Coder.of(), CombinedMetadataCombiner.of()));
116+
110117
private final WindowingStrategy<Object, W> windowingStrategy;
111118

112119
private final WindowedValueReceiver<KV<K, OutputT>> outputter;
@@ -376,7 +383,7 @@ public void processElements(Iterable<WindowedValue<InputT>> values) throws Excep
376383
emit(
377384
contextFactory.base(window, StateStyle.DIRECT),
378385
contextFactory.base(window, StateStyle.RENAMED),
379-
CausedByDrain.NORMAL);
386+
CombinedMetadata.createDefault());
380387
}
381388

382389
// We're all done with merging and emitting elements so can compress the activeWindow state.
@@ -590,6 +597,7 @@ private void processElement(Map<W, W> windowToMergeResult, WindowedValue<InputT>
590597
value.getTimestamp(),
591598
StateStyle.DIRECT,
592599
value.causedByDrain());
600+
593601
if (triggerRunner.isClosed(directContext.state())) {
594602
// This window has already been closed.
595603
droppedDueToClosedWindow.inc();
@@ -604,6 +612,11 @@ private void processElement(Map<W, W> windowToMergeResult, WindowedValue<InputT>
604612
continue;
605613
}
606614

615+
CombinedMetadata metadata = CombinedMetadata.create(value.causedByDrain());
616+
if (!metadata.equals(CombinedMetadata.createDefault())) {
617+
directContext.state().access(METADATA_TAG).add(metadata);
618+
}
619+
607620
activeWindows.ensureWindowIsActive(window);
608621
ReduceFn<K, InputT, OutputT, W>.ProcessValueContext renamedContext =
609622
contextFactory.forValue(
@@ -649,15 +662,15 @@ private class WindowActivation {
649662
// garbage collect the window. We'll consider any timer at or after the
650663
// end-of-window time to be a signal to garbage collect.
651664
public final boolean isGarbageCollection;
652-
public final CausedByDrain causedByDrain;
665+
public final CombinedMetadata combinedMetadata;
653666

654667
WindowActivation(
655668
ReduceFn<K, InputT, OutputT, W>.Context directContext,
656669
ReduceFn<K, InputT, OutputT, W>.Context renamedContext,
657-
CausedByDrain causedByDrain) {
670+
CombinedMetadata combinedMetadata) {
658671
this.directContext = directContext;
659672
this.renamedContext = renamedContext;
660-
this.causedByDrain = causedByDrain;
673+
this.combinedMetadata = combinedMetadata;
661674
W window = directContext.window();
662675

663676
// The output watermark is before the end of the window if it is either unknown
@@ -742,7 +755,8 @@ public void onTimers(Iterable<TimerData> timers) throws Exception {
742755
ReduceFn<K, InputT, OutputT, W>.Context renamedContext =
743756
contextFactory.base(window, StateStyle.RENAMED);
744757
WindowActivation windowActivation =
745-
new WindowActivation(directContext, renamedContext, timer.causedByDrain());
758+
new WindowActivation(
759+
directContext, renamedContext, CombinedMetadata.create(timer.causedByDrain()));
746760
windowActivations.put(window, windowActivation);
747761

748762
// Perform prefetching of state to determine if the trigger should fire.
@@ -778,7 +792,7 @@ public void onTimers(Iterable<TimerData> timers) throws Exception {
778792
directContext.window(),
779793
timerInternals.currentInputWatermarkTime(),
780794
timerInternals.currentOutputWatermarkTime(),
781-
windowActivation.causedByDrain);
795+
windowActivation.combinedMetadata.causedByDrain());
782796

783797
boolean windowIsActiveAndOpen = windowActivation.windowIsActiveAndOpen();
784798
if (windowIsActiveAndOpen) {
@@ -792,7 +806,7 @@ public void onTimers(Iterable<TimerData> timers) throws Exception {
792806
renamedContext,
793807
true /* isFinished */,
794808
windowActivation.isEndOfWindow,
795-
windowActivation.causedByDrain);
809+
windowActivation.combinedMetadata);
796810
checkState(newHold == null, "Hold placed at %s despite isFinished being true.", newHold);
797811
}
798812

@@ -810,7 +824,7 @@ public void onTimers(Iterable<TimerData> timers) throws Exception {
810824
if (windowActivation.windowIsActiveAndOpen()
811825
&& triggerRunner.shouldFire(
812826
directContext.window(), directContext.timers(), directContext.state())) {
813-
emit(directContext, renamedContext, windowActivation.causedByDrain);
827+
emit(directContext, renamedContext, windowActivation.combinedMetadata);
814828
}
815829

816830
if (windowActivation.isEndOfWindow) {
@@ -874,6 +888,7 @@ private void clearAllState(
874888
triggerRunner.clearState(
875889
directContext.window(), directContext.timers(), directContext.state());
876890
paneInfoTracker.clear(directContext.state());
891+
directContext.state().access(METADATA_TAG).clear();
877892
} else {
878893
// If !windowIsActiveAndOpen then !activeWindows.isActive (1) or triggerRunner.isClosed (2).
879894
// For (1), if !activeWindows.isActive then the window must be merging and has been
@@ -934,8 +949,9 @@ private void prefetchEmit(
934949
private void emit(
935950
ReduceFn<K, InputT, OutputT, W>.Context directContext,
936951
ReduceFn<K, InputT, OutputT, W>.Context renamedContext,
937-
CausedByDrain causedByDrain)
952+
CombinedMetadata metadata)
938953
throws Exception {
954+
939955
checkState(
940956
triggerRunner.shouldFire(
941957
directContext.window(), directContext.timers(), directContext.state()));
@@ -950,13 +966,14 @@ private void emit(
950966
// Run onTrigger to produce the actual pane contents.
951967
// As a side effect it will clear all element holds, but not necessarily any
952968
// end-of-window or garbage collection holds.
953-
onTrigger(directContext, renamedContext, isFinished, false /*isEndOfWindow*/, causedByDrain);
969+
onTrigger(directContext, renamedContext, isFinished, false /*isEndOfWindow*/, metadata);
954970

955971
// Now that we've triggered, the pane is empty.
956972
nonEmptyPanes.clearPane(renamedContext.state());
957973

958974
// Cleanup buffered data if appropriate
959975
if (shouldDiscard) {
976+
directContext.state().access(METADATA_TAG).clear();
960977
// Cleanup flavor C: The user does not want any buffered data to persist between panes.
961978
reduceFn.clearState(renamedContext);
962979
}
@@ -1009,8 +1026,22 @@ private void prefetchOnTrigger(
10091026
ReduceFn<K, InputT, OutputT, W>.Context renamedContext,
10101027
final boolean isFinished,
10111028
boolean isEndOfWindow,
1012-
CausedByDrain causedByDrain)
1029+
CombinedMetadata metadata)
10131030
throws Exception {
1031+
CombiningState<CombinedMetadata, CombinedMetadata, CombinedMetadata> metadataState =
1032+
directContext.state().access(METADATA_TAG);
1033+
CombinedMetadata aggregatedMetadata = metadataState.read();
1034+
if (aggregatedMetadata == null) {
1035+
aggregatedMetadata = CombinedMetadata.createDefault();
1036+
}
1037+
CombinedMetadata fullyAggregatedMetadata =
1038+
CombinedMetadataCombiner.of().addInput(aggregatedMetadata, metadata);
1039+
final CausedByDrain aggregatedCausedByDrain = fullyAggregatedMetadata.causedByDrain();
1040+
if (isFinished) {
1041+
metadataState.clear();
1042+
} else if (!metadata.equals(CombinedMetadata.createDefault())) {
1043+
metadataState.add(metadata);
1044+
}
10141045
// Extract the window hold, and as a side effect clear it.
10151046
final WatermarkHold.OldAndNewHolds pair =
10161047
watermarkHold.extractAndRelease(renamedContext, isFinished).read();
@@ -1081,12 +1112,12 @@ private void prefetchOnTrigger(
10811112
.setValue(KV.of(key, toOutput))
10821113
.setTimestamp(outputTimestamp)
10831114
.setWindows(windows)
1084-
.setCausedByDrain(causedByDrain)
1115+
.setCausedByDrain(aggregatedCausedByDrain)
10851116
.setPaneInfo(paneInfo)
10861117
.setReceiver(outputter)
10871118
.output();
10881119
},
1089-
causedByDrain);
1120+
aggregatedCausedByDrain);
10901121

10911122
reduceFn.onTrigger(renamedTriggerContext);
10921123
}

0 commit comments

Comments
 (0)