@@ -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