Skip to content

Commit f22400a

Browse files
authored
[Dataflow Streaming] Add experiment to use trigger state to know if a window is new and optimize reads (#37574)
If unstable_not_update_compatible_new_window_optimization experiment is set always write trigger state so it's absence can be used to know that the window has never existed to avoid reads. This is only performed for non-merging windows. Additionally optimize for all cases that trigger state is not rewritten if it is unchanged. This logic already existed but was broken due to missing equals method implementation.
1 parent ef703f6 commit f22400a

10 files changed

Lines changed: 535 additions & 47 deletions

File tree

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

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.apache.beam.runners.core.triggers.TriggerStateMachineRunner;
3838
import org.apache.beam.sdk.metrics.Counter;
3939
import org.apache.beam.sdk.metrics.Metrics;
40+
import org.apache.beam.sdk.options.ExperimentalOptions;
4041
import org.apache.beam.sdk.options.PipelineOptions;
4142
import org.apache.beam.sdk.state.CombiningState;
4243
import org.apache.beam.sdk.state.TimeDomain;
@@ -94,6 +95,11 @@
9495
}) // TODO(https://github.com/apache/beam/issues/20497)
9596
public class ReduceFnRunner<K, InputT, OutputT, W extends BoundedWindow> {
9697

98+
// Experiments guarding optimizations in development. No backward compatibility guarantees.
99+
public static final String UNSTABLE_NOT_UPDATE_COMPATIBLE_NEW_WINDOW_OPTIMIZATION =
100+
"unstable_not_update_compatible_new_window_optimization";
101+
public static final String UNSTABLE_DISABLE_WATERMARK_KNOWN_EMPTY_OPTIMIZATION =
102+
"unstable_disable_watermark_known_empty_optimization";
97103
/**
98104
* The {@link ReduceFnRunner} depends on most aspects of the {@link WindowingStrategy}.
99105
*
@@ -218,6 +224,9 @@ public class ReduceFnRunner<K, InputT, OutputT, W extends BoundedWindow> {
218224
*/
219225
private final NonEmptyPanes<K, W> nonEmptyPanes;
220226

227+
private final boolean useNewWindowOptimization;
228+
private final boolean disableWatermarkKnownEmptyOptimization;
229+
221230
public ReduceFnRunner(
222231
K key,
223232
WindowingStrategy<?, W> windowingStrategy,
@@ -244,6 +253,15 @@ public ReduceFnRunner(
244253

245254
this.nonEmptyPanes = NonEmptyPanes.create(this.windowingStrategy, this.reduceFn);
246255

256+
this.useNewWindowOptimization =
257+
windowingStrategy.getWindowFn().isNonMerging()
258+
&& ExperimentalOptions.hasExperiment(
259+
options, UNSTABLE_NOT_UPDATE_COMPATIBLE_NEW_WINDOW_OPTIMIZATION);
260+
261+
this.disableWatermarkKnownEmptyOptimization =
262+
ExperimentalOptions.hasExperiment(
263+
options, UNSTABLE_DISABLE_WATERMARK_KNOWN_EMPTY_OPTIMIZATION);
264+
247265
// Note this may incur I/O to load persisted window set data.
248266
this.activeWindows = createActiveWindowSet();
249267

@@ -263,7 +281,8 @@ public ReduceFnRunner(
263281
new TriggerStateMachineRunner<>(
264282
triggerStateMachine,
265283
new TriggerStateMachineContextFactory<>(
266-
windowingStrategy.getWindowFn(), stateInternals, activeWindows));
284+
windowingStrategy.getWindowFn(), stateInternals, activeWindows),
285+
this.useNewWindowOptimization);
267286
}
268287

269288
private ActiveWindowSet<W> createActiveWindowSet() {
@@ -282,6 +301,16 @@ boolean hasNoActiveWindows() {
282301
return activeWindows.getActiveAndNewWindows().isEmpty();
283302
}
284303

304+
@VisibleForTesting
305+
TriggerStateMachineRunner<W> getTriggerRunner() {
306+
return triggerRunner;
307+
}
308+
309+
@VisibleForTesting
310+
ReduceFnContextFactory<K, InputT, OutputT, W> getContextFactory() {
311+
return contextFactory;
312+
}
313+
285314
private Set<W> windowsThatAreOpen(Collection<W> windows) {
286315
Set<W> result = new HashSet<>();
287316
for (W window : windows) {
@@ -626,6 +655,20 @@ private void processElement(Map<W, W> windowToMergeResult, WindowedValue<InputT>
626655
StateStyle.RENAMED,
627656
value.causedByDrain());
628657

658+
// TODO: Make sure the NewWindowOptimization does not create unbounded trigger state
659+
// in GlobalWindow
660+
if (useNewWindowOptimization && triggerRunner.isNew(directContext.state())) {
661+
// Blindly clear state to ensure Windmill doesn't do unnecessary reads.
662+
// TODO: Instead of the clears here, we could mark these states as empty locally
663+
// in the state cache and/or explicitly tell that the entries are non-existent via api
664+
reduceFn.clearState(renamedContext);
665+
paneInfoTracker.clear(directContext.state());
666+
if (!disableWatermarkKnownEmptyOptimization) {
667+
watermarkHold.setKnownEmpty(renamedContext);
668+
}
669+
nonEmptyPanes.clearPane(renamedContext.state());
670+
}
671+
629672
nonEmptyPanes.recordContent(renamedContext.state());
630673
scheduleGarbageCollectionTimer(directContext);
631674

@@ -761,7 +804,7 @@ public void onTimers(Iterable<TimerData> timers) throws Exception {
761804

762805
// Perform prefetching of state to determine if the trigger should fire.
763806
if (windowActivation.isGarbageCollection) {
764-
triggerRunner.prefetchIsClosed(directContext.state());
807+
triggerRunner.prefetchFinishedSet(directContext.state());
765808
} else {
766809
triggerRunner.prefetchShouldFire(directContext.window(), directContext.state());
767810
}
@@ -941,7 +984,7 @@ private void prefetchEmit(
941984
ReduceFn<K, InputT, OutputT, W>.Context directContext,
942985
ReduceFn<K, InputT, OutputT, W>.Context renamedContext) {
943986
triggerRunner.prefetchShouldFire(directContext.window(), directContext.state());
944-
triggerRunner.prefetchIsClosed(directContext.state());
987+
triggerRunner.prefetchFinishedSet(directContext.state());
945988
prefetchOnTrigger(directContext, renamedContext);
946989
}
947990

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,23 @@ public void clearHolds(ReduceFn<?, ?, ?, W>.Context context) {
466466
context.state().access(EXTRA_HOLD_TAG).clear();
467467
}
468468

469+
/**
470+
* <b><i>For internal use only; no backwards-compatibility guarantees.</i></b>
471+
*
472+
* <p>Permit marking the watermark holds as empty locally, without necessarily clearing them in
473+
* the backend.
474+
*/
475+
public void setKnownEmpty(ReduceFn<?, ?, ?, W>.Context context) {
476+
WindowTracing.debug(
477+
"WatermarkHold.setKnownEmpty: For key:{}; window:{}; inputWatermark:{}; outputWatermark:{}",
478+
context.key(),
479+
context.window(),
480+
timerInternals.currentInputWatermarkTime(),
481+
timerInternals.currentOutputWatermarkTime());
482+
context.state().access(elementHoldTag).setKnownEmpty();
483+
context.state().access(EXTRA_HOLD_TAG).setKnownEmpty();
484+
}
485+
469486
/** Return the current data hold, or null if none. Does not clear. For debugging only. */
470487
public @Nullable Instant getDataCurrent(ReduceFn<?, ?, ?, W>.Context context) {
471488
return context.state().access(elementHoldTag).read();

runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/FinishedTriggersBitSet.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package org.apache.beam.runners.core.triggers;
1919

2020
import java.util.BitSet;
21+
import org.checkerframework.checker.nullness.qual.Nullable;
2122

2223
/** A {@link FinishedTriggers} implementation based on an underlying {@link BitSet}. */
2324
public class FinishedTriggersBitSet implements FinishedTriggers {
@@ -60,4 +61,17 @@ public void clearRecursively(ExecutableTriggerStateMachine trigger) {
6061
public FinishedTriggersBitSet copy() {
6162
return new FinishedTriggersBitSet((BitSet) bitSet.clone());
6263
}
64+
65+
@Override
66+
public boolean equals(@Nullable Object obj) {
67+
if (!(obj instanceof FinishedTriggersBitSet)) {
68+
return false;
69+
}
70+
return bitSet.equals(((FinishedTriggersBitSet) obj).bitSet);
71+
}
72+
73+
@Override
74+
public int hashCode() {
75+
return bitSet.hashCode();
76+
}
6377
}

runners/core-java/src/main/java/org/apache/beam/runners/core/triggers/TriggerStateMachineRunner.java

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,16 @@ public class TriggerStateMachineRunner<W extends BoundedWindow> {
6363

6464
private final ExecutableTriggerStateMachine rootTrigger;
6565
private final TriggerStateMachineContextFactory<W> contextFactory;
66+
private final boolean useNewWindowOptimization;
6667

6768
public TriggerStateMachineRunner(
6869
ExecutableTriggerStateMachine rootTrigger,
69-
TriggerStateMachineContextFactory<W> contextFactory) {
70+
TriggerStateMachineContextFactory<W> contextFactory,
71+
boolean useNewWindowOptimization) {
7072
checkState(rootTrigger.getTriggerIndex() == 0);
7173
this.rootTrigger = rootTrigger;
7274
this.contextFactory = contextFactory;
75+
this.useNewWindowOptimization = useNewWindowOptimization;
7376
}
7477

7578
private FinishedTriggersBitSet readFinishedBits(ValueState<BitSet> state) {
@@ -81,9 +84,11 @@ private FinishedTriggersBitSet readFinishedBits(ValueState<BitSet> state) {
8184
}
8285

8386
@Nullable BitSet bitSet = state.read();
84-
return bitSet == null
85-
? FinishedTriggersBitSet.emptyWithCapacity(rootTrigger.getFirstIndexAfterSubtree())
86-
: FinishedTriggersBitSet.fromBitSet(bitSet);
87+
if (bitSet == null) {
88+
return FinishedTriggersBitSet.emptyWithCapacity(rootTrigger.getFirstIndexAfterSubtree());
89+
}
90+
91+
return FinishedTriggersBitSet.fromBitSet(bitSet);
8792
}
8893

8994
private void clearFinishedBits(ValueState<BitSet> state) {
@@ -99,19 +104,29 @@ public boolean isClosed(StateAccessor<?> state) {
99104
return readFinishedBits(state.access(FINISHED_BITS_TAG)).isFinished(rootTrigger);
100105
}
101106

102-
public void prefetchIsClosed(StateAccessor<?> state) {
107+
/** Return true if the window is new (no trigger state has ever been persisted). */
108+
public boolean isNew(StateAccessor<?> state) {
109+
return isFinishedSetNeeded() && state.access(FINISHED_BITS_TAG).read() == null;
110+
}
111+
112+
@VisibleForTesting
113+
public BitSet getFinishedBits(StateAccessor<?> state) {
114+
return readFinishedBits(state.access(FINISHED_BITS_TAG)).getBitSet();
115+
}
116+
117+
public void prefetchFinishedSet(StateAccessor<?> state) {
103118
if (isFinishedSetNeeded()) {
104119
state.access(FINISHED_BITS_TAG).readLater();
105120
}
106121
}
107122

108123
public void prefetchForValue(W window, StateAccessor<?> state) {
109-
prefetchIsClosed(state);
124+
prefetchFinishedSet(state);
110125
rootTrigger.invokePrefetchOnElement(contextFactory.createPrefetchContext(window, rootTrigger));
111126
}
112127

113128
public void prefetchShouldFire(W window, StateAccessor<?> state) {
114-
prefetchIsClosed(state);
129+
prefetchFinishedSet(state);
115130
rootTrigger.invokePrefetchShouldFire(contextFactory.createPrefetchContext(window, rootTrigger));
116131
}
117132

@@ -180,13 +195,23 @@ public void onFire(W window, Timers timers, StateAccessor<?> state) throws Excep
180195
persistFinishedSet(state, finishedSet);
181196
}
182197

183-
private void persistFinishedSet(
184-
StateAccessor<?> state, FinishedTriggersBitSet modifiedFinishedSet) {
198+
@VisibleForTesting
199+
void persistFinishedSet(StateAccessor<?> state, FinishedTriggersBitSet modifiedFinishedSet) {
185200
if (!isFinishedSetNeeded()) {
186201
return;
187202
}
188203

189204
ValueState<BitSet> finishedSetState = state.access(FINISHED_BITS_TAG);
205+
206+
if (useNewWindowOptimization) {
207+
if (finishedSetState.read() == null
208+
|| !readFinishedBits(finishedSetState).equals(modifiedFinishedSet)) {
209+
// Write a value even if the bitset was empty
210+
finishedSetState.write(modifiedFinishedSet.getBitSet());
211+
}
212+
return;
213+
}
214+
190215
if (!readFinishedBits(finishedSetState).equals(modifiedFinishedSet)) {
191216
if (modifiedFinishedSet.getBitSet().isEmpty()) {
192217
finishedSetState.clear();

runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnRunnerTest.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,19 @@
4040

4141
import java.util.ArrayList;
4242
import java.util.Arrays;
43+
import java.util.BitSet;
44+
import java.util.Collections;
4345
import java.util.List;
4446
import java.util.Random;
4547
import java.util.concurrent.ThreadLocalRandom;
48+
import org.apache.beam.runners.core.ReduceFnContextFactory.StateStyle;
4649
import org.apache.beam.runners.core.metrics.MetricsContainerImpl;
4750
import org.apache.beam.runners.core.triggers.DefaultTriggerStateMachine;
4851
import org.apache.beam.runners.core.triggers.TriggerStateMachine;
4952
import org.apache.beam.sdk.coders.VarIntCoder;
5053
import org.apache.beam.sdk.metrics.MetricName;
5154
import org.apache.beam.sdk.metrics.MetricsEnvironment;
55+
import org.apache.beam.sdk.options.ExperimentalOptions;
5256
import org.apache.beam.sdk.options.PipelineOptions;
5357
import org.apache.beam.sdk.options.PipelineOptionsFactory;
5458
import org.apache.beam.sdk.state.TimeDomain;
@@ -2343,4 +2347,58 @@ public interface TestOptions extends PipelineOptions {
23432347

23442348
void setValue(int value);
23452349
}
2350+
2351+
@Test
2352+
public void testNewWindowOptimization() throws Exception {
2353+
WindowingStrategy<?, IntervalWindow> strategy =
2354+
WindowingStrategy.of(FixedWindows.of(Duration.millis(10)))
2355+
.withTrigger(AfterPane.elementCountAtLeast(2))
2356+
.withMode(AccumulationMode.ACCUMULATING_FIRED_PANES);
2357+
2358+
PipelineOptions options = PipelineOptionsFactory.create();
2359+
options
2360+
.as(ExperimentalOptions.class)
2361+
.setExperiments(
2362+
Collections.singletonList("unstable_not_update_compatible_new_window_optimization"));
2363+
ReduceFnTester<Integer, Iterable<Integer>, IntervalWindow> tester =
2364+
ReduceFnTester.nonCombining(strategy, options);
2365+
2366+
IntervalWindow window = new IntervalWindow(new Instant(0), new Instant(10));
2367+
2368+
assertTrue(
2369+
"Window should be new",
2370+
tester
2371+
.createRunner()
2372+
.getTriggerRunner()
2373+
.isNew(
2374+
tester.createRunner().getContextFactory().base(window, StateStyle.DIRECT).state()));
2375+
2376+
// 1. First element for a new window.
2377+
tester.injectElements(TimestampedValue.of(1, new Instant(1)));
2378+
2379+
BitSet bitSet =
2380+
tester
2381+
.createRunner()
2382+
.getTriggerRunner()
2383+
.getFinishedBits(
2384+
tester.createRunner().getContextFactory().base(window, StateStyle.DIRECT).state());
2385+
assertTrue("Bitset should be empty", bitSet.isEmpty());
2386+
assertFalse("Trigger should not be finished", bitSet.get(0));
2387+
2388+
assertFalse(
2389+
"Window should no longer be new",
2390+
tester
2391+
.createRunner()
2392+
.getTriggerRunner()
2393+
.isNew(
2394+
tester.createRunner().getContextFactory().base(window, StateStyle.DIRECT).state()));
2395+
2396+
// 2. Second element for the same window.
2397+
tester.injectElements(TimestampedValue.of(2, new Instant(2)));
2398+
2399+
// Extract output.
2400+
List<WindowedValue<Iterable<Integer>>> output = tester.extractOutput();
2401+
// 2 elements fired at end of window.
2402+
assertThat(output, contains(isSingleWindowedValue(containsInAnyOrder(1, 2), 9, 0, 10)));
2403+
}
23462404
}

runners/core-java/src/test/java/org/apache/beam/runners/core/ReduceFnTester.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,19 @@ ReduceFnTester<Integer, Iterable<Integer>, W> nonCombining(
129129
NullSideInputReader.empty());
130130
}
131131

132+
public static <W extends BoundedWindow>
133+
ReduceFnTester<Integer, Iterable<Integer>, W> nonCombining(
134+
WindowingStrategy<?, W> windowingStrategy, PipelineOptions options) throws Exception {
135+
return new ReduceFnTester<>(
136+
windowingStrategy,
137+
TriggerStateMachines.stateMachineForTrigger(
138+
TriggerTranslation.toProto(windowingStrategy.getTrigger())),
139+
SystemReduceFn.buffering(VarIntCoder.of()),
140+
IterableCoder.of(VarIntCoder.of()),
141+
options,
142+
NullSideInputReader.empty());
143+
}
144+
132145
/**
133146
* Creates a {@link ReduceFnTester} for the given {@link WindowingStrategy} and {@link
134147
* TriggerStateMachine}, for mocking the interactions between {@link ReduceFnRunner} and the

0 commit comments

Comments
 (0)