@@ -38,32 +38,24 @@ public class MetronomeReader implements SourceReader<RowData, MetronomeSplit> {
3838 public static final long UNINITIALIZED = Long .MIN_VALUE ;
3939
4040 private final SourceReaderContext readerContext ;
41- private final boolean replayOnFailure ;
4241 @ Nullable private final Long numberOfRows ;
4342 private final ScheduledExecutorService availabilityExecutor ;
4443
4544 private CompletableFuture <Void > availability ;
4645 private boolean noMoreSplits ;
4746 private boolean closed ;
4847 private boolean splitAssigned ;
49- private boolean skipReplayOnRecovery ;
5048 private long lastEmittedNumber ;
51- private long startTimestampSec ;
49+ private long lastEmissionTimestampMillis ;
5250
53- public MetronomeReader (
54- SourceReaderContext readerContext , boolean replayOnFailure , @ Nullable Long numberOfRows ) {
51+ public MetronomeReader (SourceReaderContext readerContext , @ Nullable Long numberOfRows ) {
5552 this .readerContext = readerContext ;
5653 this .numberOfRows = numberOfRows ;
57- this .replayOnFailure = replayOnFailure ;
5854 this .availability = new CompletableFuture <>();
5955 this .availabilityExecutor =
6056 Executors .newSingleThreadScheduledExecutor (new ExecutorThreadFactory ("metronome-source" ));
6157 this .lastEmittedNumber = 0L ;
62- this .startTimestampSec = UNINITIALIZED ;
63- }
64-
65- public MetronomeReader (SourceReaderContext readerContext , @ Nullable Long numberOfRows ) {
66- this (readerContext , true , numberOfRows );
58+ this .lastEmissionTimestampMillis = UNINITIALIZED ;
6759 }
6860
6961 @ Override
@@ -75,11 +67,9 @@ public void start() {
7567 * Emits the next due metronome row, or schedules the reader to become available at the next
7668 * second boundary.
7769 *
78- * <p>The emitted sequence number is derived from wall-clock epoch seconds relative to the first
79- * observed start second. If the reader wakes up late, repeated calls emit all missing sequence
80- * numbers with the current wall-clock timestamp until the source catches up. If checkpoint
81- * recovery replay is disabled, only the first poll after restoring checkpointed progress skips
82- * the recovery backlog.
70+ * <p>The emitted sequence number is always the next number after the checkpointed progress. The
71+ * reader never drains wall-clock backlog in a burst: after every emitted row, it records the
72+ * current time so the next row cannot become due before a full second has elapsed.
8373 */
8474 @ Override
8575 public InputStatus pollNext (ReaderOutput <RowData > output ) {
@@ -91,37 +81,16 @@ public InputStatus pollNext(ReaderOutput<RowData> output) {
9181 return noMoreSplits ? InputStatus .END_OF_INPUT : InputStatus .NOTHING_AVAILABLE ;
9282 }
9383
94- long currentTimestampSec = currentTimestampSec ();
95- if (startTimestampSec == UNINITIALIZED ) {
96- startTimestampSec = currentTimestampSec ;
97- }
84+ var currentTimestamp = Instant .now ();
85+ long currentTimestampMillis = currentTimestamp .toEpochMilli ();
9886
99- long targetNumber = currentTimestampSec - startTimestampSec ;
100- if (numberOfRows != null ) {
101- targetNumber = Math .min (targetNumber , numberOfRows );
102- }
103-
104- // One-shot checkpoint recovery skip; normal late wakeups still replay.
105- boolean skipReplay = skipReplayOnRecovery ;
106- skipReplayOnRecovery = false ;
107-
108- if (lastEmittedNumber < targetNumber ) {
87+ if (isDue (currentTimestampMillis )
88+ && (numberOfRows == null || lastEmittedNumber < numberOfRows )) {
10989 long nextNumber = lastEmittedNumber + 1 ;
110- long eventTimestampMillis = currentTimestampSec * 1000L ;
111- var row =
112- GenericRowData .of (
113- nextNumber , TimestampData .fromInstant (Instant .ofEpochSecond (currentTimestampSec )));
90+ var row = GenericRowData .of (nextNumber , TimestampData .fromInstant (currentTimestamp ));
11491 lastEmittedNumber = nextNumber ;
115- if (skipReplay ) {
116- // Realign time so the restored backlog is dropped after this record.
117- startTimestampSec = currentTimestampSec - lastEmittedNumber ;
118- }
119- output .collect (row , eventTimestampMillis );
120-
121- // Skip waits for the next tick; otherwise drain due records until caught up.
122- return !skipReplay && lastEmittedNumber < targetNumber
123- ? InputStatus .MORE_AVAILABLE
124- : nextStatus ();
92+ lastEmissionTimestampMillis = currentTimestampMillis ;
93+ output .collect (row , currentTimestampMillis );
12594 }
12695
12796 return nextStatus ();
@@ -131,15 +100,15 @@ public InputStatus pollNext(ReaderOutput<RowData> output) {
131100 * Snapshots the assigned split as the reader's progress state.
132101 *
133102 * <p>The split carries the only source progress that must survive failover: the last emitted
134- * sequence number and the source's effective start epoch second .
103+ * sequence number.
135104 */
136105 @ Override
137106 public List <MetronomeSplit > snapshotState (long checkpointId ) {
138107 if (!splitAssigned || closed || (numberOfRows != null && lastEmittedNumber >= numberOfRows )) {
139108 return List .of ();
140109 }
141110
142- return List .of (new MetronomeSplit (lastEmittedNumber , startTimestampSec ));
111+ return List .of (new MetronomeSplit (lastEmittedNumber ));
143112 }
144113
145114 /** Returns the current availability future used by the Flink runtime to avoid busy polling. */
@@ -153,10 +122,6 @@ public CompletableFuture<Void> isAvailable() {
153122 public void addSplits (List <MetronomeSplit > splits ) {
154123 for (var split : splits ) {
155124 lastEmittedNumber = split .lastEmittedNumber ();
156- startTimestampSec = split .startTimestampSec ();
157- // Non-initial split means checkpoint/savepoint recovery.
158- skipReplayOnRecovery =
159- !replayOnFailure && (startTimestampSec != UNINITIALIZED || lastEmittedNumber > 0L );
160125 splitAssigned = true ;
161126 break ;
162127 }
@@ -181,8 +146,8 @@ public void close() {
181146 * Determines the next source status after a poll cycle.
182147 *
183148 * <p>The source ends when bounded output is exhausted or the sequence reaches {@link
184- * Long#MAX_VALUE}. Otherwise, it replaces the availability future and schedules it for the next
185- * epoch-second boundary .
149+ * Long#MAX_VALUE}. Otherwise, it replaces the availability future and schedules it one second
150+ * later .
186151 */
187152 private InputStatus nextStatus () {
188153 if (lastEmittedNumber == Long .MAX_VALUE
@@ -191,36 +156,28 @@ private InputStatus nextStatus() {
191156 return InputStatus .END_OF_INPUT ;
192157 }
193158
194- if (noMoreSplits && startTimestampSec == UNINITIALIZED ) {
195- return InputStatus .END_OF_INPUT ;
196- }
197-
198159 availability = new CompletableFuture <>();
199- scheduleAvailability (nanosUntilNextSecond () );
160+ scheduleAvailability ();
200161
201162 return InputStatus .NOTHING_AVAILABLE ;
202163 }
203164
165+ private boolean isDue (long currentTimestampMillis ) {
166+ return lastEmissionTimestampMillis == UNINITIALIZED
167+ || currentTimestampMillis - lastEmissionTimestampMillis >= TimeUnit .SECONDS .toMillis (1 );
168+ }
169+
204170 private void completeAvailability () {
205171 availability .complete (null );
206172 }
207173
208- /** Schedules completion of the current availability future using nanosecond delay precision . */
209- private void scheduleAvailability (long delayNanos ) {
174+ /** Schedules completion of the current availability future one second later . */
175+ private void scheduleAvailability () {
210176 if (!closed ) {
211- availabilityExecutor .schedule (this ::completeAvailability , delayNanos , TimeUnit .NANOSECONDS );
177+ // Capture the current future so stale timers cannot wake newer poll cycles
178+ var scheduledAvailability = availability ;
179+ availabilityExecutor .schedule (
180+ () -> scheduledAvailability .complete (null ), 1 , TimeUnit .SECONDS );
212181 }
213182 }
214-
215- /** Returns the current wall-clock epoch second used for sequence catch-up calculations. */
216- private static long currentTimestampSec () {
217- return Instant .now ().getEpochSecond ();
218- }
219-
220- /** Returns the remaining nanoseconds until the next wall-clock epoch-second boundary. */
221- private static long nanosUntilNextSecond () {
222- int currentNano = Instant .now ().getNano ();
223-
224- return TimeUnit .SECONDS .toNanos (1L ) - currentNano ;
225- }
226183}
0 commit comments