Skip to content

Commit 0a58392

Browse files
[FLINK-40093][Runtime / Task] Race condition between watermark alignment and idleness detection locks split in idle
1 parent 3e3e270 commit 0a58392

2 files changed

Lines changed: 100 additions & 2 deletions

File tree

flink-runtime/src/main/java/org/apache/flink/streaming/api/operators/SourceOperator.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -889,11 +889,17 @@ private void pauseOrResumeSplits(
889889

890890
private void reportPausedOrResumed(
891891
Collection<String> splitsToPause, Collection<String> splitsToResume) {
892+
InternalSourceSplitMetricGroup metricGroup;
892893
for (String splitId : splitsToResume) {
893-
getOrCreateSplitMetricGroup(splitId).markNotPaused();
894+
metricGroup = getOrCreateSplitMetricGroup(splitId);
895+
metricGroup.markNotPaused();
894896
}
895897
for (String splitId : splitsToPause) {
896-
getOrCreateSplitMetricGroup(splitId).markPaused();
898+
metricGroup = getOrCreateSplitMetricGroup(splitId);
899+
if (metricGroup.isIdle()) {
900+
currentlyIdleSplits.remove(splitId);
901+
}
902+
metricGroup.markPaused();
897903
}
898904
}
899905

flink-runtime/src/test/java/org/apache/flink/streaming/api/operators/SourceOperatorSplitWatermarkAlignmentTest.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,71 @@ void testAlignmentCheckIsDeferredForIdleSplits() throws Exception {
569569
0L, operator.getSplitMetricGroup(split0.splitId()).getAccumulatedPausedTime());
570570
}
571571

572+
@Test
573+
void testPausedSplitDoesNotGetStuckWhenMarkedIdleDuringPause() throws Exception {
574+
final long idleTimeout = 10000;
575+
576+
final RaceInjectingMockSourceReader sourceReader =
577+
new RaceInjectingMockSourceReader(
578+
WaitingForSplits.DO_NOT_WAIT_FOR_SPLITS, false, true);
579+
580+
final TestProcessingTimeService processingTimeService = new TestProcessingTimeService();
581+
processingTimeService.setCurrentTime(0);
582+
583+
final SourceOperator operator =
584+
createAndOpenSourceOperatorWithIdleness(
585+
sourceReader, processingTimeService, idleTimeout);
586+
587+
final MockSourceSplit split0 = new MockSourceSplit(0, 0, 10);
588+
final MockSourceSplit split1 = new MockSourceSplit(1, 10, 20);
589+
590+
// split0 will become ahead and get paused
591+
split0.addRecord(5);
592+
593+
// split1 is the slower split
594+
split1.addRecord(0);
595+
596+
operator.handleOperatorEvent(
597+
new AddSplitEvent<>(
598+
Arrays.asList(split0, split1), new MockSourceSplitSerializer()));
599+
600+
final CollectingDataOutput actualOutput = new CollectingDataOutput<>();
601+
602+
operator.emitNext(actualOutput); // split0 emits 5
603+
operator.emitNext(actualOutput); // split1 emits 0
604+
sampleAllWatermarks(processingTimeService);
605+
606+
// Inject the race:
607+
//
608+
// SourceOperator.pauseOrResumeSplits does:
609+
// 1. sourceReader.pauseOrResumeSplits(...)
610+
// 2. eventTimeLogic.pauseOrResumeSplits(...)
611+
//
612+
// We simulate the idleness timer firing between those two calls
613+
sourceReader.runAfterNextPause(
614+
() -> operator.updateCurrentSplitIdle(split0.splitId(), true));
615+
616+
operator.handleOperatorEvent(new WatermarkAlignmentEvent(4));
617+
618+
assertThat(sourceReader.getPausedSplits()).containsExactly(split0.splitId());
619+
620+
// Now the slow split catches up. This should make split0 resumable
621+
sourceReader.getAssignedSplits().get(1).addRecord(4);
622+
operator.emitNext(actualOutput); // split1 emits 4
623+
sampleAllWatermarks(processingTimeService);
624+
625+
operator.handleOperatorEvent(new WatermarkAlignmentEvent(5));
626+
627+
// Assert split0 resumed after the first alignment is done
628+
assertThat(sourceReader.getPausedSplits()).doesNotContain(split0.splitId());
629+
630+
// Verify that the split can emit again
631+
sourceReader.getAssignedSplits().get(0).addRecord(6);
632+
operator.emitNext(actualOutput);
633+
634+
assertOutput(actualOutput, Arrays.asList(5, 0, 4, 6));
635+
}
636+
572637
private void sampleAllWatermarks(TestProcessingTimeService timeService) throws Exception {
573638
sampleWatermarks(timeService, WATERMARK_ALIGNMENT_BUFFER_SIZE.defaultValue());
574639
}
@@ -698,6 +763,33 @@ public void onPeriodicEmit(WatermarkOutput output) {
698763
}
699764
}
700765

766+
private static class RaceInjectingMockSourceReader extends MockSourceReader {
767+
private Runnable afterNextPause = () -> {};
768+
769+
RaceInjectingMockSourceReader(
770+
WaitingForSplits waitingForSplitsBehaviour,
771+
boolean markIdleOnNoSplits,
772+
boolean usePerSplitOutputs) {
773+
super(waitingForSplitsBehaviour, markIdleOnNoSplits, usePerSplitOutputs);
774+
}
775+
776+
void runAfterNextPause(Runnable afterNextPause) {
777+
this.afterNextPause = afterNextPause;
778+
}
779+
780+
@Override
781+
public void pauseOrResumeSplits(
782+
Collection<String> splitsToPause, Collection<String> splitsToResume) {
783+
super.pauseOrResumeSplits(splitsToPause, splitsToResume);
784+
785+
if (!splitsToPause.isEmpty()) {
786+
Runnable callback = afterNextPause;
787+
afterNextPause = () -> {};
788+
callback.run();
789+
}
790+
}
791+
}
792+
701793
/** Condition checking if there is no watermark above a certain value among StreamElements. */
702794
public static class WatermarkAbove extends Condition<Object> {
703795
public WatermarkAbove(int maxEmittedWatermark) {

0 commit comments

Comments
 (0)