This is an automated email from the ASF dual-hosted git repository.
MartijnVisser pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/flink.git
The following commit(s) were added to refs/heads/master by this push:
new 533e5fc321c [FLINK-40098][test-utils] Add FailingCheckpointedSource
for exactly-once source tests (#28686)
533e5fc321c is described below
commit 533e5fc321cea2b13807e5006f3efbda8b4b0f24
Author: Martijn Visser <[email protected]>
AuthorDate: Wed Oct 21 19:56:14 2026 +0200
[FLINK-40098][test-utils] Add FailingCheckpointedSource for exactly-once
source tests (#28686)
* [FLINK-40098][test-utils] Add FailingCheckpointedSource for exactly-once
source tests
Replaces the fail-once-and-recover semantics of the legacy
SourceFunction-based FailingSource with a Source V2 utility
* [FLINK-40098][tests] Migrate EventTimeAllWindowCheckpointingITCase to
Source V2
Replaces the legacy FailingSource with FailingCheckpointedSource,
pinned to parallelism 1 like the implicitly non-parallel legacy source.
KeyedEventTimeGenerator implements both generator interfaces until the
remaining FailingSource consumers are migrated.
* [FLINK-40098][tests] Migrate ProcessingTimeWindowCheckpointingITCase to
Source V2
Exercises the idle-after-emission mode of FailingCheckpointedSource,
which mirrors the usingProcessingTime flag of the legacy FailingSource
Generated-by: Claude Code (Fable 5.1)
---
.../flink-test-utils-connector/pom.xml | 9 +
.../util/source/FailingCheckpointedSource.java | 458 +++++++++++++++++++++
.../util/source/FailingCheckpointedSourceTest.java | 329 +++++++++++++++
.../EventTimeAllWindowCheckpointingITCase.java | 46 ++-
.../EventTimeWindowCheckpointingITCase.java | 20 +-
.../ProcessingTimeWindowCheckpointingITCase.java | 50 ++-
6 files changed, 880 insertions(+), 32 deletions(-)
diff --git a/flink-test-utils-parent/flink-test-utils-connector/pom.xml
b/flink-test-utils-parent/flink-test-utils-connector/pom.xml
index 250f6252a38..7b37b2072ab 100644
--- a/flink-test-utils-parent/flink-test-utils-connector/pom.xml
+++ b/flink-test-utils-parent/flink-test-utils-connector/pom.xml
@@ -49,6 +49,15 @@ under the License.
<version>${project.version}</version>
<scope>test</scope>
</dependency>
+
+ <!-- For the source mocks (MockSplitEnumeratorContext) -->
+ <dependency>
+ <groupId>org.apache.flink</groupId>
+ <artifactId>flink-core</artifactId>
+ <version>${project.version}</version>
+ <type>test-jar</type>
+ <scope>test</scope>
+ </dependency>
</dependencies>
</project>
\ No newline at end of file
diff --git
a/flink-test-utils-parent/flink-test-utils-connector/src/main/java/org/apache/flink/test/util/source/FailingCheckpointedSource.java
b/flink-test-utils-parent/flink-test-utils-connector/src/main/java/org/apache/flink/test/util/source/FailingCheckpointedSource.java
new file mode 100644
index 00000000000..8c887e44f85
--- /dev/null
+++
b/flink-test-utils-parent/flink-test-utils-connector/src/main/java/org/apache/flink/test/util/source/FailingCheckpointedSource.java
@@ -0,0 +1,458 @@
+/*
+ * 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.flink.test.util.source;
+
+import org.apache.flink.annotation.Experimental;
+import org.apache.flink.api.common.eventtime.Watermark;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
+import org.apache.flink.api.connector.source.Boundedness;
+import org.apache.flink.api.connector.source.ReaderOutput;
+import org.apache.flink.api.connector.source.Source;
+import org.apache.flink.api.connector.source.SourceReader;
+import org.apache.flink.api.connector.source.SourceReaderContext;
+import org.apache.flink.api.connector.source.SourceSplit;
+import org.apache.flink.api.connector.source.SplitEnumerator;
+import org.apache.flink.api.connector.source.SplitEnumeratorContext;
+import org.apache.flink.api.java.typeutils.ResultTypeQueryable;
+import org.apache.flink.core.io.InputStatus;
+import org.apache.flink.core.io.SimpleVersionedSerializer;
+import org.apache.flink.core.memory.DataInputDeserializer;
+import org.apache.flink.core.memory.DataOutputSerializer;
+import org.apache.flink.util.FlinkRuntimeException;
+
+import javax.annotation.Nullable;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+import static org.apache.flink.util.Preconditions.checkArgument;
+import static org.apache.flink.util.Preconditions.checkNotNull;
+import static org.apache.flink.util.Preconditions.checkState;
+
+/**
+ * A test source with a checkpointed emission position that fails subtask 0
exactly once after a
+ * completed checkpoint and resumes from the restored position, for verifying
exactly-once behavior
+ * across recovery.
+ *
+ * <p>Each subtask invokes the {@link EventGenerator} once per sequence
number. Subtask 0 fails
+ * after half of its emit calls, like the legacy {@code FailingSource}: a
snapshot taken past a
+ * quarter of the emit calls arms the failure, emission continues to the
failure position, and the
+ * reader throws once the armed checkpoint completes. Emission holds just past
the arming threshold
+ * until a snapshot arms, so recovery always replays the records between the
arming and failure
+ * positions, independent of emission speed. Checkpointing must be enabled.
All records of one
+ * {@link EventGenerator#emit} call are emitted in a single {@code pollNext}
and cannot be split by
+ * a checkpoint. Rescaling is not supported; use a fixed source parallelism.
+ *
+ * <p>{@link #withIdleAfterEmission()} keeps the source idle instead of
finishing, for
+ * processing-time tests whose job is terminated externally.
+ */
+@Experimental
+public class FailingCheckpointedSource<T>
+ implements Source<T, FailingCheckpointedSource.SequenceSplit, Void>,
+ ResultTypeQueryable<T> {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * Generates and emits the records (and optionally timestamps and
watermarks) for one sequence
+ * number of one subtask.
+ */
+ @FunctionalInterface
+ public interface EventGenerator<T> extends Serializable {
+ void emit(int subtaskIndex, long sequenceNo, GeneratorOutput<T>
output);
+ }
+
+ /** Output for the {@link EventGenerator}, bridging to the reader output.
*/
+ public interface GeneratorOutput<T> {
+ void collect(T record);
+
+ void collect(T record, long timestamp);
+
+ void emitWatermark(long timestamp);
+ }
+
+ private final EventGenerator<T> generator;
+ private final long emitCallsPerSubtask;
+ private final long failAfterEmitCalls;
+ private final long armAfterEmitCalls;
+ private final TypeInformation<T> producedType;
+ private final boolean idleAfterEmission;
+
+ private FailingCheckpointedSource(
+ EventGenerator<T> generator,
+ long emitCallsPerSubtask,
+ TypeInformation<T> producedType,
+ boolean idleAfterEmission) {
+ // fewer than two emit calls leave no checkpointable position before
the failure
+ checkArgument(emitCallsPerSubtask >= 2, "emitCallsPerSubtask must be
>= 2");
+ this.generator = checkNotNull(generator);
+ this.emitCallsPerSubtask = emitCallsPerSubtask;
+ this.failAfterEmitCalls = emitCallsPerSubtask / 2;
+ this.armAfterEmitCalls = failAfterEmitCalls / 2;
+ this.producedType = checkNotNull(producedType);
+ this.idleAfterEmission = idleAfterEmission;
+ }
+
+ /**
+ * Creates a source whose subtasks each invoke the generator {@code
emitCallsPerSubtask} times
+ * and whose subtask 0 fails once after half of them.
+ */
+ public static <T> FailingCheckpointedSource<T> of(
+ EventGenerator<T> generator,
+ long emitCallsPerSubtask,
+ TypeInformation<T> producedType) {
+ return new FailingCheckpointedSource<>(generator, emitCallsPerSubtask,
producedType, false);
+ }
+
+ /**
+ * Returns a copy of this source that stays idle after emitting all
sequence numbers instead of
+ * finishing, for processing-time tests whose job is terminated externally.
+ */
+ public FailingCheckpointedSource<T> withIdleAfterEmission() {
+ return new FailingCheckpointedSource<>(generator, emitCallsPerSubtask,
producedType, true);
+ }
+
+ @Override
+ public Boundedness getBoundedness() {
+ return Boundedness.CONTINUOUS_UNBOUNDED;
+ }
+
+ @Override
+ public SourceReader<T, SequenceSplit> createReader(SourceReaderContext
readerContext) {
+ return new FailingCheckpointedSourceReader<>(
+ generator,
+ emitCallsPerSubtask,
+ failAfterEmitCalls,
+ armAfterEmitCalls,
+ idleAfterEmission);
+ }
+
+ @Override
+ public SplitEnumerator<SequenceSplit, Void> createEnumerator(
+ SplitEnumeratorContext<SequenceSplit> enumContext) {
+ return new SequenceSplitEnumerator(enumContext, true);
+ }
+
+ @Override
+ public SplitEnumerator<SequenceSplit, Void> restoreEnumerator(
+ SplitEnumeratorContext<SequenceSplit> enumContext, Void
checkpoint) {
+ // After restore the readers recover their splits from their own
state, so the enumerator
+ // must not assign fresh splits to re-registering readers.
+ return new SequenceSplitEnumerator(enumContext, false);
+ }
+
+ @Override
+ public SimpleVersionedSerializer<SequenceSplit> getSplitSerializer() {
+ return SequenceSplit.SERIALIZER;
+ }
+
+ @Override
+ public SimpleVersionedSerializer<Void> getEnumeratorCheckpointSerializer()
{
+ return VoidSerializer.INSTANCE;
+ }
+
+ @Override
+ public TypeInformation<T> getProducedType() {
+ return producedType;
+ }
+
+ /**
+ * A split carrying the emission position of one source subtask, so that
the reader can resume
+ * from the checkpointed position after recovery. The subtask index
assumes a fixed source
+ * parallelism.
+ */
+ static final class SequenceSplit implements SourceSplit {
+
+ static final SimpleVersionedSerializer<SequenceSplit> SERIALIZER =
+ new SequenceSplitSerializer();
+
+ private final int subtaskIndex;
+ private final long position;
+
+ SequenceSplit(int subtaskIndex, long position) {
+ this.subtaskIndex = subtaskIndex;
+ this.position = position;
+ }
+
+ int getSubtaskIndex() {
+ return subtaskIndex;
+ }
+
+ long getPosition() {
+ return position;
+ }
+
+ @Override
+ public String splitId() {
+ return "sequence-split-" + subtaskIndex;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof SequenceSplit)) {
+ return false;
+ }
+ final SequenceSplit that = (SequenceSplit) o;
+ return subtaskIndex == that.subtaskIndex && position ==
that.position;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(subtaskIndex, position);
+ }
+
+ @Override
+ public String toString() {
+ return "SequenceSplit{subtaskIndex=" + subtaskIndex + ",
position=" + position + "}";
+ }
+ }
+
+ private static final class SequenceSplitSerializer
+ implements SimpleVersionedSerializer<SequenceSplit> {
+
+ @Override
+ public int getVersion() {
+ return 1;
+ }
+
+ @Override
+ public byte[] serialize(SequenceSplit split) throws IOException {
+ final DataOutputSerializer out = new DataOutputSerializer(12);
+ out.writeInt(split.subtaskIndex);
+ out.writeLong(split.position);
+ return out.getCopyOfBuffer();
+ }
+
+ @Override
+ public SequenceSplit deserialize(int version, byte[] serialized)
throws IOException {
+ final DataInputDeserializer in = new
DataInputDeserializer(serialized);
+ return new SequenceSplit(in.readInt(), in.readLong());
+ }
+ }
+
+ /** Assigns one position split per subtask on fresh starts. */
+ private static final class SequenceSplitEnumerator
+ implements SplitEnumerator<SequenceSplit, Void> {
+
+ private final SplitEnumeratorContext<SequenceSplit> context;
+ private final boolean assignSplitsOnRegistration;
+ private final Map<Integer, SequenceSplit> splitsToReassign = new
HashMap<>();
+ private final Set<Integer> assignedSubtasks = new HashSet<>();
+
+ private SequenceSplitEnumerator(
+ SplitEnumeratorContext<SequenceSplit> context, boolean
assignSplitsOnRegistration) {
+ this.context = context;
+ this.assignSplitsOnRegistration = assignSplitsOnRegistration;
+ }
+
+ @Override
+ public void start() {}
+
+ @Override
+ public void handleSplitRequest(int subtaskId, @Nullable String
requesterHostname) {}
+
+ @Override
+ public void addSplitsBack(List<SequenceSplit> splits, int subtaskId) {
+ for (SequenceSplit split : splits) {
+ splitsToReassign.put(split.getSubtaskIndex(), split);
+ }
+ }
+
+ @Override
+ public void addReader(int subtaskId) {
+ final SequenceSplit returnedSplit =
splitsToReassign.remove(subtaskId);
+ if (returnedSplit != null) {
+ context.assignSplit(returnedSplit, subtaskId);
+ } else if (assignSplitsOnRegistration &&
assignedSubtasks.add(subtaskId)) {
+ // only assign on the very first registration; on partial
failover this
+ // enumerator survives and the reader recovers its split from
its own state
+ context.assignSplit(new SequenceSplit(subtaskId, 0),
subtaskId);
+ }
+ }
+
+ @Override
+ public Void snapshotState(long checkpointId) {
+ return null;
+ }
+
+ @Override
+ public void close() {}
+ }
+
+ /**
+ * The reader. All methods are invoked in the task's mailbox thread, so no
synchronization is
+ * required between emission, snapshots, and checkpoint notifications.
+ */
+ private static final class FailingCheckpointedSourceReader<T>
+ implements SourceReader<T, SequenceSplit> {
+
+ private final EventGenerator<T> generator;
+ private final long emitCallsPerSubtask;
+ private final long failAfterEmitCalls;
+ private final long armAfterEmitCalls;
+ private final boolean idleAfterEmission;
+ private final BridgingGeneratorOutput generatorOutput = new
BridgingGeneratorOutput();
+
+ private boolean hasSplit;
+ private int subtaskIndex;
+ private long position;
+ private boolean eligibleToFail;
+ @Nullable private Long armedCheckpointId;
+ private boolean failureRequested;
+ private CompletableFuture<Void> availability =
CompletableFuture.completedFuture(null);
+
+ private FailingCheckpointedSourceReader(
+ EventGenerator<T> generator,
+ long emitCallsPerSubtask,
+ long failAfterEmitCalls,
+ long armAfterEmitCalls,
+ boolean idleAfterEmission) {
+ this.generator = generator;
+ this.emitCallsPerSubtask = emitCallsPerSubtask;
+ this.failAfterEmitCalls = failAfterEmitCalls;
+ this.armAfterEmitCalls = armAfterEmitCalls;
+ this.idleAfterEmission = idleAfterEmission;
+ }
+
+ @Override
+ public void start() {}
+
+ @Override
+ public InputStatus pollNext(ReaderOutput<T> output) {
+ if (failureRequested && position >= failAfterEmitCalls) {
+ throw new FlinkRuntimeException("Artificial Failure");
+ }
+ if (!hasSplit) {
+ return pause();
+ }
+ if (eligibleToFail) {
+ if (armedCheckpointId == null && position > armAfterEmitCalls)
{
+ // hold until a snapshot arms, so the armed checkpoint
provably captures a
+ // pre-failure position and recovery replays the records
in between
+ return pause();
+ }
+ if (position >= failAfterEmitCalls) {
+ // hold at the failure position until the armed checkpoint
completes
+ return pause();
+ }
+ }
+ if (position >= emitCallsPerSubtask) {
+ return idleAfterEmission ? pause() : InputStatus.END_OF_INPUT;
+ }
+ generatorOutput.delegate = output;
+ generator.emit(subtaskIndex, position++, generatorOutput);
+ return InputStatus.MORE_AVAILABLE;
+ }
+
+ private InputStatus pause() {
+ if (availability.isDone()) {
+ availability = new CompletableFuture<>();
+ }
+ return InputStatus.NOTHING_AVAILABLE;
+ }
+
+ @Override
+ public List<SequenceSplit> snapshotState(long checkpointId) {
+ if (!hasSplit) {
+ return Collections.emptyList();
+ }
+ // only arm past the threshold, so recovery demonstrably restores
progress
+ if (eligibleToFail && armedCheckpointId == null && position >
armAfterEmitCalls) {
+ armedCheckpointId = checkpointId;
+ // resume emission toward the failure position
+ availability.complete(null);
+ }
+ return Collections.singletonList(new SequenceSplit(subtaskIndex,
position));
+ }
+
+ @Override
+ public void notifyCheckpointComplete(long checkpointId) {
+ if (eligibleToFail && armedCheckpointId != null &&
armedCheckpointId == checkpointId) {
+ failureRequested = true;
+ availability.complete(null);
+ }
+ }
+
+ @Override
+ public void notifyCheckpointAborted(long checkpointId) {
+ // let a later snapshot re-arm after an abort
+ if (armedCheckpointId != null && armedCheckpointId ==
checkpointId) {
+ armedCheckpointId = null;
+ }
+ }
+
+ @Override
+ public void addSplits(List<SequenceSplit> splits) {
+ checkState(
+ !hasSplit && splits.size() == 1,
+ "expecting exactly one split per subtask, got %s (split
already present: %s)",
+ splits,
+ hasSplit);
+ final SequenceSplit split = splits.get(0);
+ hasSplit = true;
+ subtaskIndex = split.getSubtaskIndex();
+ position = split.getPosition();
+ // restored position > 0 identifies the recovery attempt (no
attempt number in
+ // SourceReaderContext)
+ eligibleToFail = split.getSubtaskIndex() == 0 &&
split.getPosition() == 0;
+ availability.complete(null);
+ }
+
+ @Override
+ public CompletableFuture<Void> isAvailable() {
+ return availability;
+ }
+
+ @Override
+ public void notifyNoMoreSplits() {}
+
+ @Override
+ public void close() {
+ availability.complete(null);
+ }
+
+ private final class BridgingGeneratorOutput implements
GeneratorOutput<T> {
+
+ @Nullable private ReaderOutput<T> delegate;
+
+ @Override
+ public void collect(T record) {
+ delegate.collect(record);
+ }
+
+ @Override
+ public void collect(T record, long timestamp) {
+ delegate.collect(record, timestamp);
+ }
+
+ @Override
+ public void emitWatermark(long timestamp) {
+ delegate.emitWatermark(new Watermark(timestamp));
+ }
+ }
+ }
+}
diff --git
a/flink-test-utils-parent/flink-test-utils-connector/src/test/java/org/apache/flink/test/util/source/FailingCheckpointedSourceTest.java
b/flink-test-utils-parent/flink-test-utils-connector/src/test/java/org/apache/flink/test/util/source/FailingCheckpointedSourceTest.java
new file mode 100644
index 00000000000..9573bfcdb3f
--- /dev/null
+++
b/flink-test-utils-parent/flink-test-utils-connector/src/test/java/org/apache/flink/test/util/source/FailingCheckpointedSourceTest.java
@@ -0,0 +1,329 @@
+/*
+ * 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.flink.test.util.source;
+
+import org.apache.flink.api.common.typeinfo.Types;
+import org.apache.flink.api.connector.source.SourceReader;
+import org.apache.flink.api.connector.source.SplitEnumerator;
+import org.apache.flink.api.connector.source.mocks.MockSplitEnumeratorContext;
+import org.apache.flink.core.io.InputStatus;
+import
org.apache.flink.test.util.source.FailingCheckpointedSource.SequenceSplit;
+import org.apache.flink.util.FlinkRuntimeException;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.LongStream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for the failure protocol of {@link FailingCheckpointedSource}: arm on
a snapshot past the
+ * threshold, pause at the failure position, fail once the armed checkpoint
completes, and resume
+ * from the restored position without failing again.
+ */
+class FailingCheckpointedSourceTest {
+
+ private static final long EMIT_CALLS = 20;
+ private static final long FAILURE_POSITION = EMIT_CALLS / 2;
+
+ private static FailingCheckpointedSource<Long> failingSource() {
+ return FailingCheckpointedSource.of(
+ (subtaskIndex, sequenceNo, output) ->
output.collect(sequenceNo),
+ EMIT_CALLS,
+ Types.LONG);
+ }
+
+ private static SourceReader<Long, SequenceSplit> readerWithSplit(
+ FailingCheckpointedSource<Long> source, SequenceSplit split) {
+ final SourceReader<Long, SequenceSplit> reader =
source.createReader(null);
+ reader.addSplits(Collections.singletonList(split));
+ return reader;
+ }
+
+ private static List<Long> range(long startInclusive, long endExclusive) {
+ return LongStream.range(startInclusive,
endExclusive).boxed().collect(Collectors.toList());
+ }
+
+ @Test
+ void testFailsOnceAfterArmedCheckpointCompletes() throws Exception {
+ final SourceReader<Long, SequenceSplit> reader =
+ readerWithSplit(failingSource(), new SequenceSplit(0, 0));
+ final TestReaderOutput<Long> output = new TestReaderOutput<>();
+
+ // Emission holds just past the arming threshold until a snapshot arms
the failure.
+ final long armHoldPosition = FAILURE_POSITION / 2 + 1;
+ for (long i = 0; i < armHoldPosition; i++) {
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.MORE_AVAILABLE);
+ }
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.NOTHING_AVAILABLE);
+ assertThat(reader.isAvailable()).isNotDone();
+ assertThat(output.getCollected()).isEqualTo(range(0, armHoldPosition));
+
+ // The arming snapshot captures a position strictly before the failure
position, so
+ // recovery is guaranteed to replay the records in between.
+ final List<SequenceSplit> snapshot = reader.snapshotState(1);
+ assertThat(snapshot).hasSize(1);
+ assertThat(snapshot.get(0).getPosition())
+ .isEqualTo(armHoldPosition)
+ .isLessThan(FAILURE_POSITION);
+ assertThat(reader.isAvailable()).isDone();
+
+ // Emission resumes to the failure position and holds for the armed
checkpoint.
+ for (long i = armHoldPosition; i < FAILURE_POSITION; i++) {
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.MORE_AVAILABLE);
+ }
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.NOTHING_AVAILABLE);
+ assertThat(reader.isAvailable()).isNotDone();
+
+ reader.notifyCheckpointComplete(1);
+ assertThat(reader.isAvailable()).isDone();
+ assertThatThrownBy(() -> reader.pollNext(output))
+ .isInstanceOf(FlinkRuntimeException.class)
+ .hasMessage("Artificial Failure");
+ assertThat(output.getCollected()).isEqualTo(range(0,
FAILURE_POSITION));
+ }
+
+ @Test
+ void testDoesNotArmOnCheckpointBeforeThreshold() throws Exception {
+ final SourceReader<Long, SequenceSplit> reader =
+ readerWithSplit(failingSource(), new SequenceSplit(0, 0));
+ final TestReaderOutput<Long> output = new TestReaderOutput<>();
+
+ // Only 3 emit calls: below the arming threshold of FAILURE_POSITION /
2.
+ for (long i = 0; i < 3; i++) {
+ reader.pollNext(output);
+ }
+ reader.snapshotState(1);
+ reader.notifyCheckpointComplete(1);
+
+ // The early checkpoint must not trigger the failure; emission
continues to the arm hold.
+ final long armHoldPosition = FAILURE_POSITION / 2 + 1;
+ for (long i = 3; i < armHoldPosition; i++) {
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.MORE_AVAILABLE);
+ }
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.NOTHING_AVAILABLE);
+
+ reader.snapshotState(2);
+ for (long i = armHoldPosition; i < FAILURE_POSITION; i++) {
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.MORE_AVAILABLE);
+ }
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.NOTHING_AVAILABLE);
+ reader.notifyCheckpointComplete(2);
+ assertThatThrownBy(() -> reader.pollNext(output))
+ .isInstanceOf(FlinkRuntimeException.class)
+ .hasMessage("Artificial Failure");
+ }
+
+ @Test
+ void testRestoredReaderResumesWithoutFailingAgain() throws Exception {
+ final SourceReader<Long, SequenceSplit> reader =
+ readerWithSplit(failingSource(), new SequenceSplit(0,
FAILURE_POSITION));
+ final TestReaderOutput<Long> output = new TestReaderOutput<>();
+
+ InputStatus status = InputStatus.MORE_AVAILABLE;
+ while (status == InputStatus.MORE_AVAILABLE) {
+ status = reader.pollNext(output);
+ }
+ assertThat(status).isEqualTo(InputStatus.END_OF_INPUT);
+ assertThat(output.getCollected()).isEqualTo(range(FAILURE_POSITION,
EMIT_CALLS));
+ }
+
+ @Test
+ void testNonZeroSubtaskNeverFails() throws Exception {
+ final SourceReader<Long, SequenceSplit> reader =
+ readerWithSplit(failingSource(), new SequenceSplit(1, 0));
+ final TestReaderOutput<Long> output = new TestReaderOutput<>();
+
+ InputStatus status = InputStatus.MORE_AVAILABLE;
+ while (status == InputStatus.MORE_AVAILABLE) {
+ reader.snapshotState(output.getCollectedCount());
+ reader.notifyCheckpointComplete(output.getCollectedCount());
+ status = reader.pollNext(output);
+ }
+ assertThat(status).isEqualTo(InputStatus.END_OF_INPUT);
+ assertThat(output.getCollected()).isEqualTo(range(0, EMIT_CALLS));
+ }
+
+ @Test
+ void testIdleAfterEmissionKeepsReaderAlive() throws Exception {
+ final SourceReader<Long, SequenceSplit> reader =
+ readerWithSplit(
+ failingSource().withIdleAfterEmission(),
+ new SequenceSplit(0, FAILURE_POSITION));
+ final TestReaderOutput<Long> output = new TestReaderOutput<>();
+
+ InputStatus status = InputStatus.MORE_AVAILABLE;
+ while (status == InputStatus.MORE_AVAILABLE) {
+ status = reader.pollNext(output);
+ }
+ assertThat(status).isEqualTo(InputStatus.NOTHING_AVAILABLE);
+ assertThat(reader.isAvailable()).isNotDone();
+ assertThat(output.getCollected()).isEqualTo(range(FAILURE_POSITION,
EMIT_CALLS));
+ }
+
+ @Test
+ void testGeneratorEmitsMultipleRecordsWithTimestampsAtomically() throws
Exception {
+ final FailingCheckpointedSource<Long> source =
+ FailingCheckpointedSource.of(
+ (subtaskIndex, sequenceNo, output) -> {
+ output.collect(sequenceNo, sequenceNo);
+ output.collect(-sequenceNo, sequenceNo);
+ output.emitWatermark(sequenceNo);
+ },
+ 3,
+ Types.LONG);
+ // subtask 1 never fails, so the whole sequence is observable without
a checkpoint
+ final SourceReader<Long, SequenceSplit> reader =
+ readerWithSplit(source, new SequenceSplit(1, 0));
+ final TestReaderOutput<Long> output = new TestReaderOutput<>();
+
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.MORE_AVAILABLE);
+ assertThat(output.getCollected()).containsExactly(0L, 0L);
+
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.MORE_AVAILABLE);
+ assertThat(output.getCollected()).containsExactly(0L, 0L, 1L, -1L);
+ }
+
+ @Test
+ void testRejectsFewerThanTwoEmitCalls() {
+ assertThatThrownBy(
+ () ->
+ FailingCheckpointedSource.of(
+ (subtaskIndex, sequenceNo, output) ->
+ output.collect(sequenceNo),
+ 1,
+ Types.LONG))
+ .isInstanceOf(IllegalArgumentException.class);
+ }
+
+ @Test
+ void testAbortedArmedCheckpointRearmsOnNextSnapshot() throws Exception {
+ final SourceReader<Long, SequenceSplit> reader =
+ readerWithSplit(failingSource(), new SequenceSplit(0, 0));
+ final TestReaderOutput<Long> output = new TestReaderOutput<>();
+
+ final long armHoldPosition = FAILURE_POSITION / 2 + 1;
+ for (long i = 0; i < armHoldPosition; i++) {
+ reader.pollNext(output);
+ }
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.NOTHING_AVAILABLE);
+
+ reader.snapshotState(1);
+ reader.notifyCheckpointAborted(1);
+ // the aborted checkpoint must not trigger the failure and must not
stay armed; the
+ // reader holds at the arming position again
+ reader.notifyCheckpointComplete(1);
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.NOTHING_AVAILABLE);
+
+ reader.snapshotState(2);
+ for (long i = armHoldPosition; i < FAILURE_POSITION; i++) {
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.MORE_AVAILABLE);
+ }
+
assertThat(reader.pollNext(output)).isEqualTo(InputStatus.NOTHING_AVAILABLE);
+ reader.notifyCheckpointComplete(2);
+ assertThatThrownBy(() -> reader.pollNext(output))
+ .isInstanceOf(FlinkRuntimeException.class)
+ .hasMessage("Artificial Failure");
+ }
+
+ @Test
+ void testEnumeratorAssignsOneSplitPerSubtaskOnFreshStart() throws
Exception {
+ final MockSplitEnumeratorContext<SequenceSplit> context =
+ new MockSplitEnumeratorContext<>(2);
+ final SplitEnumerator<SequenceSplit, Void> enumerator =
+ failingSource().createEnumerator(context);
+
+ enumerator.addReader(0);
+ enumerator.addReader(1);
+
+ assertThat(assignedSplits(context, 0)).containsExactly(new
SequenceSplit(0, 0));
+ assertThat(assignedSplits(context, 1)).containsExactly(new
SequenceSplit(1, 0));
+ }
+
+ @Test
+ void testEnumeratorDoesNotReassignOnReRegistration() throws Exception {
+ // Regression test: on a partial failover the enumerator instance
survives and the
+ // restarted reader re-registers; it recovers its split from its own
checkpointed state,
+ // so the enumerator must not assign a second, fresh split.
+ final MockSplitEnumeratorContext<SequenceSplit> context =
+ new MockSplitEnumeratorContext<>(1);
+ final SplitEnumerator<SequenceSplit, Void> enumerator =
+ failingSource().createEnumerator(context);
+
+ enumerator.addReader(0);
+ enumerator.addReader(0);
+
+ assertThat(assignedSplits(context, 0)).containsExactly(new
SequenceSplit(0, 0));
+ }
+
+ @Test
+ void testEnumeratorReassignsSplitsReturnedAfterFailure() throws Exception {
+ final MockSplitEnumeratorContext<SequenceSplit> context =
+ new MockSplitEnumeratorContext<>(1);
+ final SplitEnumerator<SequenceSplit, Void> enumerator =
+ failingSource().createEnumerator(context);
+
+ enumerator.addReader(0);
+ enumerator.addSplitsBack(
+ Collections.singletonList(new SequenceSplit(0,
FAILURE_POSITION)), 0);
+ enumerator.addReader(0);
+
+ assertThat(assignedSplits(context, 0))
+ .containsExactly(new SequenceSplit(0, 0), new SequenceSplit(0,
FAILURE_POSITION));
+ }
+
+ @Test
+ void testRestoredEnumeratorAssignsNothingOnRegistration() throws Exception
{
+ final MockSplitEnumeratorContext<SequenceSplit> context =
+ new MockSplitEnumeratorContext<>(1);
+ final SplitEnumerator<SequenceSplit, Void> enumerator =
+ failingSource().restoreEnumerator(context, null);
+
+ enumerator.addReader(0);
+
+ assertThat(context.getSplitsAssignmentSequence()).isEmpty();
+ }
+
+ private static List<SequenceSplit> assignedSplits(
+ MockSplitEnumeratorContext<SequenceSplit> context, int subtask) {
+ return context.getSplitsAssignmentSequence().stream()
+ .flatMap(
+ assignment ->
+ assignment
+ .assignment()
+ .getOrDefault(subtask,
Collections.emptyList())
+ .stream())
+ .collect(Collectors.toList());
+ }
+
+ @Test
+ void testSplitSerializerRoundTrip() throws Exception {
+ final SequenceSplit split = new SequenceSplit(3, 42);
+ final byte[] bytes = SequenceSplit.SERIALIZER.serialize(split);
+ final SequenceSplit restored =
+
SequenceSplit.SERIALIZER.deserialize(SequenceSplit.SERIALIZER.getVersion(),
bytes);
+ assertThat(restored.getSubtaskIndex()).isEqualTo(3);
+ assertThat(restored.getPosition()).isEqualTo(42);
+ assertThat(restored.splitId()).isEqualTo(split.splitId());
+ }
+}
diff --git
a/flink-tests/src/test/java/org/apache/flink/test/checkpointing/EventTimeAllWindowCheckpointingITCase.java
b/flink-tests/src/test/java/org/apache/flink/test/checkpointing/EventTimeAllWindowCheckpointingITCase.java
index 2a1f82028dd..06da892f2d1 100644
---
a/flink-tests/src/test/java/org/apache/flink/test/checkpointing/EventTimeAllWindowCheckpointingITCase.java
+++
b/flink-tests/src/test/java/org/apache/flink/test/checkpointing/EventTimeAllWindowCheckpointingITCase.java
@@ -18,8 +18,11 @@
package org.apache.flink.test.checkpointing;
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.OpenContext;
import org.apache.flink.api.common.functions.ReduceFunction;
+import org.apache.flink.api.common.typeinfo.TypeHint;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.api.java.tuple.Tuple4;
import org.apache.flink.configuration.Configuration;
@@ -27,16 +30,17 @@ import org.apache.flink.configuration.MemorySize;
import org.apache.flink.configuration.RpcOptions;
import org.apache.flink.configuration.TaskManagerOptions;
import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration;
+import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import
org.apache.flink.streaming.api.functions.windowing.RichAllWindowFunction;
import
org.apache.flink.streaming.api.windowing.assigners.SlidingEventTimeWindows;
import
org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.streaming.util.RestartStrategyUtils;
-import org.apache.flink.test.checkpointing.utils.FailingSource;
import org.apache.flink.test.checkpointing.utils.IntType;
import org.apache.flink.test.checkpointing.utils.ValidatingSink;
import org.apache.flink.test.junit5.MiniClusterExtension;
+import org.apache.flink.test.util.source.FailingCheckpointedSource;
import org.apache.flink.util.Collector;
import org.apache.flink.util.TestLoggerExtension;
@@ -75,6 +79,22 @@ class EventTimeAllWindowCheckpointingITCase {
return config;
}
+ private static DataStream<Tuple2<Long, IntType>> createFailingSourceStream(
+ StreamExecutionEnvironment env,
+ int numKeys,
+ int numElementsPerWindow,
+ int numElementsPerKey) {
+ final FailingCheckpointedSource<Tuple2<Long, IntType>> source =
+ FailingCheckpointedSource.of(
+ new
EventTimeWindowCheckpointingITCase.KeyedEventTimeGenerator(
+ numKeys, numElementsPerWindow),
+ numElementsPerKey,
+ TypeInformation.of(new TypeHint<Tuple2<Long,
IntType>>() {}));
+ // like the legacy FailingSource, the source is non-parallel
+ return env.fromSource(source, WatermarkStrategy.noWatermarks(),
"Failing Source")
+ .setParallelism(1);
+ }
+
// ------------------------------------------------------------------------
@Test
@@ -88,11 +108,7 @@ class EventTimeAllWindowCheckpointingITCase {
env.enableCheckpointing(100);
RestartStrategyUtils.configureFixedDelayRestartStrategy(env, 1, 0L);
- env.addSource(
- new FailingSource(
- new
EventTimeWindowCheckpointingITCase.KeyedEventTimeGenerator(
- numKeys, windowSize),
- numElementsPerKey))
+ createFailingSourceStream(env, numKeys, windowSize, numElementsPerKey)
.rebalance()
.windowAll(TumblingEventTimeWindows.of(Duration.ofMillis(windowSize)))
.apply(
@@ -160,11 +176,7 @@ class EventTimeAllWindowCheckpointingITCase {
env.enableCheckpointing(100);
RestartStrategyUtils.configureFixedDelayRestartStrategy(env, 1, 0L);
- env.addSource(
- new FailingSource(
- new
EventTimeWindowCheckpointingITCase.KeyedEventTimeGenerator(
- numKeys, windowSlide),
- numElementsPerKey))
+ createFailingSourceStream(env, numKeys, windowSlide, numElementsPerKey)
.rebalance()
.windowAll(
SlidingEventTimeWindows.of(
@@ -233,11 +245,7 @@ class EventTimeAllWindowCheckpointingITCase {
env.enableCheckpointing(100);
RestartStrategyUtils.configureFixedDelayRestartStrategy(env, 1, 0L);
- env.addSource(
- new FailingSource(
- new
EventTimeWindowCheckpointingITCase.KeyedEventTimeGenerator(
- numKeys, windowSize),
- numElementsPerKey))
+ createFailingSourceStream(env, numKeys, windowSize, numElementsPerKey)
.rebalance()
.windowAll(TumblingEventTimeWindows.of(Duration.ofMillis(windowSize)))
.reduce(
@@ -309,11 +317,7 @@ class EventTimeAllWindowCheckpointingITCase {
env.enableCheckpointing(100);
RestartStrategyUtils.configureFixedDelayRestartStrategy(env, 1, 0L);
- env.addSource(
- new FailingSource(
- new
EventTimeWindowCheckpointingITCase.KeyedEventTimeGenerator(
- numKeys, windowSlide),
- numElementsPerKey))
+ createFailingSourceStream(env, numKeys, windowSlide, numElementsPerKey)
.rebalance()
.windowAll(
SlidingEventTimeWindows.of(
diff --git
a/flink-tests/src/test/java/org/apache/flink/test/checkpointing/EventTimeWindowCheckpointingITCase.java
b/flink-tests/src/test/java/org/apache/flink/test/checkpointing/EventTimeWindowCheckpointingITCase.java
index c062a69de19..a4b990a55f5 100644
---
a/flink-tests/src/test/java/org/apache/flink/test/checkpointing/EventTimeWindowCheckpointingITCase.java
+++
b/flink-tests/src/test/java/org/apache/flink/test/checkpointing/EventTimeWindowCheckpointingITCase.java
@@ -52,6 +52,7 @@ import
org.apache.flink.test.checkpointing.utils.FailingSource;
import org.apache.flink.test.checkpointing.utils.IntType;
import org.apache.flink.test.checkpointing.utils.ValidatingSink;
import org.apache.flink.test.util.MiniClusterWithClientResource;
+import org.apache.flink.test.util.source.FailingCheckpointedSource;
import org.apache.flink.testutils.junit.extensions.parameterized.Parameter;
import
org.apache.flink.testutils.junit.extensions.parameterized.ParameterizedTestExtension;
import org.apache.flink.testutils.junit.extensions.parameterized.Parameters;
@@ -795,7 +796,11 @@ class EventTimeWindowCheckpointingITCase {
}
}
- static class KeyedEventTimeGenerator implements
FailingSource.EventEmittingGenerator {
+ // implements both generator interfaces until all FailingSource consumers
are migrated to
+ // FailingCheckpointedSource
+ static class KeyedEventTimeGenerator
+ implements FailingSource.EventEmittingGenerator,
+ FailingCheckpointedSource.EventGenerator<Tuple2<Long,
IntType>> {
private final int keyUniverseSize;
private final int watermarkTrailing;
@@ -819,6 +824,19 @@ class EventTimeWindowCheckpointingITCase {
ctx.emitWatermark(new Watermark(eventSequenceNo -
watermarkTrailing));
}
+
+ @Override
+ public void emit(
+ int subtaskIndex,
+ long sequenceNo,
+ FailingCheckpointedSource.GeneratorOutput<Tuple2<Long,
IntType>> output) {
+ final IntType intTypeNext = new IntType((int) sequenceNo);
+ for (long i = 0; i < keyUniverseSize; i++) {
+ output.collect(new Tuple2<>(i, intTypeNext), sequenceNo);
+ }
+
+ output.emitWatermark(sequenceNo - watermarkTrailing);
+ }
}
private int numElementsPerKey() {
diff --git
a/flink-tests/src/test/java/org/apache/flink/test/checkpointing/ProcessingTimeWindowCheckpointingITCase.java
b/flink-tests/src/test/java/org/apache/flink/test/checkpointing/ProcessingTimeWindowCheckpointingITCase.java
index 8471e9ec187..f26739a6051 100644
---
a/flink-tests/src/test/java/org/apache/flink/test/checkpointing/ProcessingTimeWindowCheckpointingITCase.java
+++
b/flink-tests/src/test/java/org/apache/flink/test/checkpointing/ProcessingTimeWindowCheckpointingITCase.java
@@ -18,26 +18,30 @@
package org.apache.flink.test.checkpointing;
+import org.apache.flink.api.common.eventtime.WatermarkStrategy;
import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.functions.OpenContext;
import org.apache.flink.api.common.functions.ReduceFunction;
+import org.apache.flink.api.common.typeinfo.TypeHint;
+import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.api.java.tuple.Tuple2;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.configuration.MemorySize;
import org.apache.flink.configuration.TaskManagerOptions;
import org.apache.flink.runtime.testutils.MiniClusterResourceConfiguration;
+import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
-import org.apache.flink.streaming.api.functions.source.legacy.SourceFunction;
import org.apache.flink.streaming.api.functions.windowing.RichWindowFunction;
import
org.apache.flink.streaming.api.windowing.assigners.SlidingProcessingTimeWindows;
import
org.apache.flink.streaming.api.windowing.assigners.TumblingProcessingTimeWindows;
import org.apache.flink.streaming.api.windowing.windows.TimeWindow;
import org.apache.flink.streaming.util.RestartStrategyUtils;
-import org.apache.flink.test.checkpointing.utils.FailingSource;
import org.apache.flink.test.checkpointing.utils.IntType;
import org.apache.flink.test.checkpointing.utils.ValidatingSink;
import org.apache.flink.test.junit5.MiniClusterExtension;
+import org.apache.flink.test.util.source.FailingCheckpointedSource;
import org.apache.flink.util.Collector;
+import org.apache.flink.util.FlinkRuntimeException;
import org.apache.flink.util.TestLoggerExtension;
import org.junit.jupiter.api.Test;
@@ -75,6 +79,20 @@ class ProcessingTimeWindowCheckpointingITCase {
return config;
}
+ private static DataStream<Tuple2<Long, IntType>> createFailingSourceStream(
+ StreamExecutionEnvironment env, int numElements) {
+ final FailingCheckpointedSource<Tuple2<Long, IntType>> source =
+ FailingCheckpointedSource.of(
+ new Generator(),
+ numElements,
+ TypeInformation.of(new TypeHint<Tuple2<Long,
IntType>>() {}))
+ // the sink terminates the job once all windows are
validated
+ .withIdleAfterEmission();
+ // like the legacy FailingSource, the source is non-parallel
+ return env.fromSource(source, WatermarkStrategy.noWatermarks(),
"Failing Source")
+ .setParallelism(1);
+ }
+
// ------------------------------------------------------------------------
@Test
@@ -90,7 +108,7 @@ class ProcessingTimeWindowCheckpointingITCase {
SinkValidatorUpdaterAndChecker updaterAndChecker =
new SinkValidatorUpdaterAndChecker(numElements, 1);
- env.addSource(new FailingSource(new Generator(), numElements, true))
+ createFailingSourceStream(env, numElements)
.rebalance()
.keyBy(x -> x.f0)
.window(TumblingProcessingTimeWindows.of(Duration.ofMillis(100)))
@@ -143,7 +161,7 @@ class ProcessingTimeWindowCheckpointingITCase {
RestartStrategyUtils.configureFixedDelayRestartStrategy(env, 1, 0L);
SinkValidatorUpdaterAndChecker updaterAndChecker =
new SinkValidatorUpdaterAndChecker(numElements, 3);
- env.addSource(new FailingSource(new Generator(), numElements, true))
+ createFailingSourceStream(env, numElements)
.rebalance()
.keyBy(x -> x.f0)
.window(
@@ -198,7 +216,7 @@ class ProcessingTimeWindowCheckpointingITCase {
RestartStrategyUtils.configureFixedDelayRestartStrategy(env, 1, 0L);
SinkValidatorUpdaterAndChecker updaterAndChecker =
new SinkValidatorUpdaterAndChecker(numElements, 1);
- env.addSource(new FailingSource(new Generator(), numElements, true))
+ createFailingSourceStream(env, numElements)
.map(
new MapFunction<Tuple2<Long, IntType>, Tuple2<Long,
IntType>>() {
@Override
@@ -236,7 +254,7 @@ class ProcessingTimeWindowCheckpointingITCase {
RestartStrategyUtils.configureFixedDelayRestartStrategy(env, 1, 0L);
SinkValidatorUpdaterAndChecker updaterAndChecker =
new SinkValidatorUpdaterAndChecker(numElements, 3);
- env.addSource(new FailingSource(new Generator(), numElements, true))
+ createFailingSourceStream(env, numElements)
.map(
new MapFunction<Tuple2<Long, IntType>, Tuple2<Long,
IntType>>() {
@Override
@@ -268,12 +286,24 @@ class ProcessingTimeWindowCheckpointingITCase {
// Utilities
// ------------------------------------------------------------------------
- static class Generator implements FailingSource.EventEmittingGenerator {
+ static class Generator
+ implements FailingCheckpointedSource.EventGenerator<Tuple2<Long,
IntType>> {
@Override
- public void emitEvent(
- SourceFunction.SourceContext<Tuple2<Long, IntType>> ctx, int
eventSequenceNo) {
- ctx.collect(new Tuple2<>((long) eventSequenceNo, new
IntType(eventSequenceNo)));
+ public void emit(
+ int subtaskIndex,
+ long sequenceNo,
+ FailingCheckpointedSource.GeneratorOutput<Tuple2<Long,
IntType>> output) {
+ output.collect(new Tuple2<>(sequenceNo, new IntType((int)
sequenceNo)));
+ // Spread the elements over many processing-time windows, like the
legacy
+ // FailingSource did, so that every run restores open window state
and rolls back
+ // sink output on recovery. Duplicate detection itself does not
depend on this.
+ try {
+ Thread.sleep(1);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new FlinkRuntimeException(e);
+ }
}
}