Skip to main content

laminar_core/time/watermark/
mod.rs

1//! Watermark generators and multi-source tracking.
2
3use std::time::{Duration, Instant};
4
5use super::Watermark;
6
7/// Trait for generating watermarks from event timestamps.
8///
9/// Implementations track observed timestamps and produce watermarks that indicate
10/// event-time progress. The watermark is an assertion that no events with timestamps
11/// earlier than the watermark are expected.
12pub trait WatermarkGenerator: Send {
13    /// Process an event timestamp and potentially emit a new watermark.
14    ///
15    /// Called for each event processed. Returns `Some(watermark)` if the watermark
16    /// should advance, `None` otherwise.
17    fn on_event(&mut self, timestamp: i64) -> Option<Watermark>;
18
19    /// Called periodically to emit watermarks based on wall-clock time.
20    ///
21    /// Useful for generating watermarks even when no events are arriving.
22    fn on_periodic(&mut self) -> Option<Watermark>;
23
24    /// Returns the current watermark value without advancing it.
25    fn current_watermark(&self) -> i64;
26
27    /// Advances the watermark to at least the given timestamp from an external source.
28    ///
29    /// Called when the source provides an explicit watermark (e.g., `Source::watermark()`).
30    /// Returns `Some(Watermark)` if the watermark advanced, `None` if the timestamp
31    /// was not higher than the current watermark.
32    fn advance_watermark(&mut self, timestamp: i64) -> Option<Watermark>;
33
34    /// Replaces the generator's watermark with an exact recovered value.
35    ///
36    /// Unlike [`Self::advance_watermark`], this may move the watermark
37    /// backwards. It is exclusively for restoring a committed checkpoint
38    /// while source intake and computation are fenced. Calling it while the
39    /// pipeline is running can make already-finalized event-time state visible
40    /// again and is therefore incorrect.
41    fn restore_watermark_for_recovery(&mut self, timestamp: i64);
42
43    /// Whether the watermark is processing-time based (wall clock), rather than
44    /// derived from the event-time column. Such a watermark lives in a different
45    /// time domain than the event timestamps, so comparing the two to drop "late"
46    /// rows would discard every event — callers skip source-side late-filtering
47    /// when this is `true`. Defaults to `false` (event-time generators).
48    fn is_processing_time(&self) -> bool {
49        false
50    }
51}
52
53/// Default `max_future_skew_ms`: 5 min.
54pub const DEFAULT_MAX_FUTURE_SKEW_MS: i64 = 5 * 60 * 1000;
55
56/// `true` if `timestamp` is more than `skew_ms` beyond wall-clock now.
57/// `skew_ms <= 0` or an unset system clock disables the guard.
58#[inline]
59fn is_grossly_future(timestamp: i64, skew_ms: i64) -> bool {
60    if skew_ms <= 0 {
61        return false;
62    }
63    let now = super::now_unix_millis();
64    now > 0 && timestamp > now.saturating_add(skew_ms)
65}
66
67/// Watermark = `max_timestamp_seen - max_out_of_orderness`. `on_event`
68/// ignores timestamps far beyond wall clock for advancement.
69pub struct BoundedOutOfOrdernessGenerator {
70    max_out_of_orderness: i64,
71    current_max_timestamp: i64,
72    current_watermark: i64,
73    /// `0` (or negative) disables the guard (unbounded — legacy behaviour).
74    max_future_skew_ms: i64,
75}
76
77impl BoundedOutOfOrdernessGenerator {
78    /// Creates a new generator with the specified maximum out-of-orderness.
79    ///
80    /// # Arguments
81    ///
82    /// * `max_out_of_orderness` - Maximum allowed lateness in milliseconds
83    #[must_use]
84    pub fn new(max_out_of_orderness: i64) -> Self {
85        Self {
86            max_out_of_orderness,
87            current_max_timestamp: i64::MIN,
88            current_watermark: i64::MIN,
89            max_future_skew_ms: DEFAULT_MAX_FUTURE_SKEW_MS,
90        }
91    }
92
93    /// Sets the future-skew ceiling; `<= 0` disables the guard.
94    #[must_use]
95    pub fn with_max_future_skew(mut self, skew_ms: i64) -> Self {
96        self.max_future_skew_ms = skew_ms;
97        self
98    }
99
100    /// Creates a new generator from a `Duration`.
101    #[must_use]
102    #[allow(clippy::cast_possible_truncation)] // Duration.as_millis() fits i64 for practical values
103    pub fn from_duration(max_out_of_orderness: Duration) -> Self {
104        Self::new(max_out_of_orderness.as_millis() as i64)
105    }
106
107    /// Returns the maximum out-of-orderness in milliseconds.
108    #[must_use]
109    pub fn max_out_of_orderness(&self) -> i64 {
110        self.max_out_of_orderness
111    }
112}
113
114impl WatermarkGenerator for BoundedOutOfOrdernessGenerator {
115    #[inline]
116    fn on_event(&mut self, timestamp: i64) -> Option<Watermark> {
117        if is_grossly_future(timestamp, self.max_future_skew_ms) {
118            return None;
119        }
120        if timestamp > self.current_max_timestamp {
121            self.current_max_timestamp = timestamp;
122            let new_watermark = timestamp.saturating_sub(self.max_out_of_orderness);
123            if new_watermark > self.current_watermark {
124                self.current_watermark = new_watermark;
125                return Some(Watermark::new(new_watermark));
126            }
127        }
128        None
129    }
130
131    #[inline]
132    fn on_periodic(&mut self) -> Option<Watermark> {
133        // Bounded out-of-orderness doesn't emit periodic watermarks
134        None
135    }
136
137    #[inline]
138    fn current_watermark(&self) -> i64 {
139        self.current_watermark
140    }
141
142    #[inline]
143    fn advance_watermark(&mut self, timestamp: i64) -> Option<Watermark> {
144        if is_grossly_future(timestamp, self.max_future_skew_ms) {
145            return None;
146        }
147        if timestamp > self.current_watermark {
148            self.current_watermark = timestamp;
149            // Maintain invariant: current_max_timestamp >= current_watermark + max_out_of_orderness
150            let min_max = timestamp.saturating_add(self.max_out_of_orderness);
151            if min_max > self.current_max_timestamp {
152                self.current_max_timestamp = min_max;
153            }
154            Some(Watermark::new(timestamp))
155        } else {
156            None
157        }
158    }
159
160    #[inline]
161    fn restore_watermark_for_recovery(&mut self, timestamp: i64) {
162        self.current_watermark = timestamp;
163        // `on_event` only considers timestamps above this baseline. Restoring
164        // it along with the watermark prevents pre-recovery observations from
165        // suppressing valid replay after a backwards restore.
166        self.current_max_timestamp = timestamp.saturating_add(self.max_out_of_orderness);
167    }
168}
169
170/// Watermark generator for strictly ascending timestamps; the watermark
171/// equals the current timestamp.
172#[derive(Debug)]
173pub struct AscendingTimestampsGenerator {
174    current_watermark: i64,
175    /// `0` ⇒ disabled.
176    max_future_skew_ms: i64,
177}
178
179impl Default for AscendingTimestampsGenerator {
180    fn default() -> Self {
181        Self::new()
182    }
183}
184
185impl AscendingTimestampsGenerator {
186    /// Creates a new ascending timestamps generator.
187    #[must_use]
188    pub fn new() -> Self {
189        Self {
190            current_watermark: i64::MIN,
191            max_future_skew_ms: DEFAULT_MAX_FUTURE_SKEW_MS,
192        }
193    }
194
195    /// Override the future-skew ceiling (`0` disables).
196    #[must_use]
197    pub fn with_max_future_skew(mut self, skew_ms: i64) -> Self {
198        self.max_future_skew_ms = skew_ms;
199        self
200    }
201}
202
203impl WatermarkGenerator for AscendingTimestampsGenerator {
204    #[inline]
205    fn on_event(&mut self, timestamp: i64) -> Option<Watermark> {
206        if is_grossly_future(timestamp, self.max_future_skew_ms) {
207            return None;
208        }
209        if timestamp > self.current_watermark {
210            self.current_watermark = timestamp;
211            Some(Watermark::new(timestamp))
212        } else {
213            None
214        }
215    }
216
217    #[inline]
218    fn on_periodic(&mut self) -> Option<Watermark> {
219        None
220    }
221
222    #[inline]
223    fn current_watermark(&self) -> i64 {
224        self.current_watermark
225    }
226
227    #[inline]
228    fn advance_watermark(&mut self, timestamp: i64) -> Option<Watermark> {
229        if is_grossly_future(timestamp, self.max_future_skew_ms) {
230            return None;
231        }
232        if timestamp > self.current_watermark {
233            self.current_watermark = timestamp;
234            Some(Watermark::new(timestamp))
235        } else {
236            None
237        }
238    }
239
240    #[inline]
241    fn restore_watermark_for_recovery(&mut self, timestamp: i64) {
242        self.current_watermark = timestamp;
243    }
244}
245
246/// Wraps another generator and emits watermarks at fixed wall-clock
247/// intervals so idle sources don't stall time-based windows.
248pub struct PeriodicGenerator<G: WatermarkGenerator> {
249    inner: G,
250    period: Duration,
251    last_emit_time: Instant,
252    last_emitted_watermark: i64,
253}
254
255impl<G: WatermarkGenerator> PeriodicGenerator<G> {
256    /// Creates a new periodic generator wrapping another generator.
257    ///
258    /// # Arguments
259    ///
260    /// * `inner` - The underlying watermark generator
261    /// * `period` - How often to emit watermarks (wall-clock time)
262    #[must_use]
263    pub fn new(inner: G, period: Duration) -> Self {
264        Self {
265            inner,
266            period,
267            last_emit_time: Instant::now(),
268            last_emitted_watermark: i64::MIN,
269        }
270    }
271
272    /// Returns a reference to the inner generator.
273    #[must_use]
274    pub fn inner(&self) -> &G {
275        &self.inner
276    }
277
278    /// Returns a mutable reference to the inner generator.
279    pub fn inner_mut(&mut self) -> &mut G {
280        &mut self.inner
281    }
282}
283
284impl<G: WatermarkGenerator> WatermarkGenerator for PeriodicGenerator<G> {
285    fn on_event(&mut self, timestamp: i64) -> Option<Watermark> {
286        let wm = self.inner.on_event(timestamp);
287        if let Some(ref w) = wm {
288            self.last_emitted_watermark = w.timestamp();
289            self.last_emit_time = Instant::now();
290        }
291        wm
292    }
293
294    fn on_periodic(&mut self) -> Option<Watermark> {
295        // Check if enough wall-clock time has passed
296        if self.last_emit_time.elapsed() >= self.period {
297            let current = self.inner.current_watermark();
298            if current > self.last_emitted_watermark {
299                self.last_emitted_watermark = current;
300                self.last_emit_time = Instant::now();
301                return Some(Watermark::new(current));
302            }
303            self.last_emit_time = Instant::now();
304        }
305        None
306    }
307
308    fn current_watermark(&self) -> i64 {
309        self.inner.current_watermark()
310    }
311
312    fn advance_watermark(&mut self, timestamp: i64) -> Option<Watermark> {
313        let wm = self.inner.advance_watermark(timestamp);
314        if let Some(ref w) = wm {
315            self.last_emitted_watermark = w.timestamp();
316            self.last_emit_time = Instant::now();
317        }
318        wm
319    }
320
321    fn restore_watermark_for_recovery(&mut self, timestamp: i64) {
322        self.inner.restore_watermark_for_recovery(timestamp);
323        self.last_emitted_watermark = timestamp;
324        self.last_emit_time = Instant::now();
325    }
326
327    fn is_processing_time(&self) -> bool {
328        self.inner.is_processing_time()
329    }
330}
331
332/// Punctuated watermark generator that emits based on special events.
333///
334/// Uses a predicate function to identify watermark-carrying events. When the
335/// predicate returns `Some(watermark)`, that watermark is emitted.
336///
337/// # Example
338///
339/// ```rust
340/// use laminar_core::time::{PunctuatedGenerator, WatermarkGenerator, Watermark};
341///
342/// // Emit watermark on every 1000ms boundary
343/// let mut gen = PunctuatedGenerator::new(|ts| {
344///     if ts % 1000 == 0 {
345///         Some(Watermark::new(ts))
346///     } else {
347///         None
348///     }
349/// });
350///
351/// assert_eq!(gen.on_event(999), None);
352/// assert_eq!(gen.on_event(1000), Some(Watermark::new(1000)));
353/// ```
354pub struct PunctuatedGenerator<F>
355where
356    F: Fn(i64) -> Option<Watermark> + Send,
357{
358    predicate: F,
359    current_watermark: i64,
360}
361
362impl<F> PunctuatedGenerator<F>
363where
364    F: Fn(i64) -> Option<Watermark> + Send,
365{
366    /// Creates a new punctuated generator with the given predicate.
367    ///
368    /// # Arguments
369    ///
370    /// * `predicate` - Function that returns `Some(Watermark)` for watermark events
371    #[must_use]
372    pub fn new(predicate: F) -> Self {
373        Self {
374            predicate,
375            current_watermark: i64::MIN,
376        }
377    }
378}
379
380impl<F> WatermarkGenerator for PunctuatedGenerator<F>
381where
382    F: Fn(i64) -> Option<Watermark> + Send,
383{
384    fn on_event(&mut self, timestamp: i64) -> Option<Watermark> {
385        if let Some(wm) = (self.predicate)(timestamp) {
386            if wm.timestamp() > self.current_watermark {
387                self.current_watermark = wm.timestamp();
388                return Some(wm);
389            }
390        }
391        None
392    }
393
394    fn on_periodic(&mut self) -> Option<Watermark> {
395        None
396    }
397
398    fn current_watermark(&self) -> i64 {
399        self.current_watermark
400    }
401
402    fn advance_watermark(&mut self, timestamp: i64) -> Option<Watermark> {
403        if timestamp > self.current_watermark {
404            self.current_watermark = timestamp;
405            Some(Watermark::new(timestamp))
406        } else {
407            None
408        }
409    }
410
411    fn restore_watermark_for_recovery(&mut self, timestamp: i64) {
412        self.current_watermark = timestamp;
413    }
414}
415
416/// A recovered tracker snapshot did not match the tracker's source topology.
417#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
418#[error(
419    "watermark recovery source count mismatch: expected {expected}, got {watermarks} watermarks and {idle_statuses} idle statuses"
420)]
421pub struct WatermarkRestoreError {
422    expected: usize,
423    watermarks: usize,
424    idle_statuses: usize,
425}
426
427/// Tracks watermarks across multiple input sources.
428///
429/// For operators with multiple inputs (e.g., joins, unions), the combined
430/// watermark is the minimum across all sources. This ensures no late events
431/// from any source are missed.
432///
433/// # Example
434///
435/// ```rust
436/// use laminar_core::time::{WatermarkTracker, Watermark};
437///
438/// let mut tracker = WatermarkTracker::new(3); // 3 sources
439///
440/// // Source 0 advances to 1000
441/// let wm = tracker.update_source(0, 1000);
442/// assert_eq!(wm, None); // Other sources still at MIN
443///
444/// // Source 1 advances to 2000
445/// tracker.update_source(1, 2000);
446///
447/// // Source 2 advances to 500
448/// let wm = tracker.update_source(2, 500);
449/// assert_eq!(wm, Some(Watermark::new(500))); // Min of all sources
450/// ```
451#[derive(Debug)]
452pub struct WatermarkTracker {
453    /// Watermark for each source
454    source_watermarks: Vec<i64>,
455    /// Combined minimum watermark
456    combined_watermark: i64,
457    /// Idle status for each source
458    idle_sources: Vec<bool>,
459    /// Last activity time for each source
460    last_activity: Vec<Instant>,
461    /// Per-source idle timeout; `None` ⇒ that source never auto-idles
462    /// (the default — idleness is opt-in, like Flink `withIdleness`).
463    idle_timeout: Vec<Option<Duration>>,
464}
465
466impl WatermarkTracker {
467    /// Creates a tracker with idleness **disabled** for all sources
468    /// (strict correctness; configure per source via
469    /// [`Self::set_idle_timeout`]).
470    #[must_use]
471    pub fn new(num_sources: usize) -> Self {
472        Self {
473            source_watermarks: vec![i64::MIN; num_sources],
474            combined_watermark: i64::MIN,
475            idle_sources: vec![false; num_sources],
476            last_activity: vec![Instant::now(); num_sources],
477            idle_timeout: vec![None; num_sources],
478        }
479    }
480
481    /// Creates a tracker with the same idle timeout for every source.
482    #[must_use]
483    pub fn with_idle_timeout(num_sources: usize, idle_timeout: Duration) -> Self {
484        Self {
485            source_watermarks: vec![i64::MIN; num_sources],
486            combined_watermark: i64::MIN,
487            idle_sources: vec![false; num_sources],
488            last_activity: vec![Instant::now(); num_sources],
489            idle_timeout: vec![Some(idle_timeout); num_sources],
490        }
491    }
492
493    /// Sets (or clears, with `None`) the idle timeout for one source.
494    pub fn set_idle_timeout(&mut self, source_id: usize, timeout: Option<Duration>) {
495        if let Some(slot) = self.idle_timeout.get_mut(source_id) {
496            *slot = timeout;
497        }
498    }
499
500    /// Updates the watermark for a specific source.
501    ///
502    /// Returns `Some(Watermark)` if the combined watermark advances.
503    pub fn update_source(&mut self, source_id: usize, watermark: i64) -> Option<Watermark> {
504        if source_id >= self.source_watermarks.len() {
505            return None;
506        }
507
508        let was_idle = self.idle_sources[source_id];
509        // Mark source as active
510        self.idle_sources[source_id] = false;
511        self.last_activity[source_id] = Instant::now();
512
513        // Update source watermark
514        if watermark > self.source_watermarks[source_id] {
515            self.source_watermarks[source_id] = watermark;
516            self.update_combined()
517        } else if was_idle {
518            self.update_combined()
519        } else {
520            None
521        }
522    }
523
524    /// Marks a source as idle, excluding it from watermark calculation.
525    ///
526    /// Idle sources don't hold back the combined watermark.
527    pub fn mark_idle(&mut self, source_id: usize) -> Option<Watermark> {
528        if source_id >= self.idle_sources.len() {
529            return None;
530        }
531
532        self.idle_sources[source_id] = true;
533        self.update_combined()
534    }
535
536    /// Checks for sources that have been idle longer than the timeout.
537    ///
538    /// Should be called periodically to detect stalled sources.
539    pub fn check_idle_sources(&mut self) -> Option<Watermark> {
540        let mut any_marked = false;
541        for i in 0..self.idle_sources.len() {
542            let Some(timeout) = self.idle_timeout[i] else {
543                continue; // idleness disabled for this source
544            };
545            if !self.idle_sources[i] && self.last_activity[i].elapsed() >= timeout {
546                self.idle_sources[i] = true;
547                any_marked = true;
548            }
549        }
550        if any_marked {
551            self.update_combined()
552        } else {
553            None
554        }
555    }
556
557    /// Replaces all watermark state from a committed recovery snapshot.
558    ///
559    /// This is the only tracker operation allowed to lower source or combined
560    /// watermarks. The caller must hold the intake/compute recovery fence so no
561    /// events, idle transitions, or watermark reads race with the replacement.
562    /// Configured idle timeouts are preserved and activity timers restart at
563    /// the instant of restoration.
564    ///
565    /// `None` represents a source or combined frontier that has not been
566    /// initialized. `combined_watermark` is installed exactly rather than
567    /// recomputed because a committed cluster frontier may intentionally lag
568    /// this tracker's local source frontiers.
569    ///
570    /// # Errors
571    ///
572    /// Returns [`WatermarkRestoreError`] when either input does not contain
573    /// exactly one entry for every tracked source. No state is changed on
574    /// error.
575    pub fn restore_for_recovery(
576        &mut self,
577        source_watermarks: &[Option<i64>],
578        idle_sources: &[bool],
579        combined_watermark: Option<i64>,
580    ) -> Result<(), WatermarkRestoreError> {
581        let expected = self.source_watermarks.len();
582        if source_watermarks.len() != expected || idle_sources.len() != expected {
583            return Err(WatermarkRestoreError {
584                expected,
585                watermarks: source_watermarks.len(),
586                idle_statuses: idle_sources.len(),
587            });
588        }
589
590        for (target, recovered) in self
591            .source_watermarks
592            .iter_mut()
593            .zip(source_watermarks.iter())
594        {
595            *target = recovered.unwrap_or(i64::MIN);
596        }
597        self.idle_sources.copy_from_slice(idle_sources);
598        self.combined_watermark = combined_watermark.unwrap_or(i64::MIN);
599        let restored_at = Instant::now();
600        self.last_activity.fill(restored_at);
601        Ok(())
602    }
603
604    /// Returns the current combined watermark.
605    #[must_use]
606    pub fn current_watermark(&self) -> Option<Watermark> {
607        if self.combined_watermark == i64::MIN {
608            None
609        } else {
610            Some(Watermark::new(self.combined_watermark))
611        }
612    }
613
614    /// Returns the watermark for a specific source.
615    #[must_use]
616    pub fn source_watermark(&self, source_id: usize) -> Option<i64> {
617        self.source_watermarks.get(source_id).copied()
618    }
619
620    /// Returns whether a source is marked as idle.
621    #[must_use]
622    pub fn is_idle(&self, source_id: usize) -> bool {
623        self.idle_sources.get(source_id).copied().unwrap_or(false)
624    }
625
626    /// Returns the number of sources being tracked.
627    #[must_use]
628    pub fn num_sources(&self) -> usize {
629        self.source_watermarks.len()
630    }
631
632    /// Returns the number of active (non-idle) sources.
633    #[must_use]
634    pub fn active_source_count(&self) -> usize {
635        self.idle_sources.iter().filter(|&&idle| !idle).count()
636    }
637
638    /// Updates the combined watermark based on all active sources.
639    fn update_combined(&mut self) -> Option<Watermark> {
640        // Calculate minimum across active sources only
641        let mut min_watermark = i64::MAX;
642        let mut has_active = false;
643
644        for (i, &wm) in self.source_watermarks.iter().enumerate() {
645            if !self.idle_sources[i] {
646                has_active = true;
647                min_watermark = min_watermark.min(wm);
648            }
649        }
650
651        // If all sources are idle, use the max watermark
652        if !has_active {
653            min_watermark = self
654                .source_watermarks
655                .iter()
656                .copied()
657                .max()
658                .unwrap_or(i64::MIN);
659        }
660
661        if min_watermark > self.combined_watermark && min_watermark != i64::MAX {
662            self.combined_watermark = min_watermark;
663            Some(Watermark::new(min_watermark))
664        } else {
665            None
666        }
667    }
668}
669
670/// Watermark generator for sources with embedded watermarks.
671///
672/// Some sources provide a durable upstream event-time frontier directly.
673/// This generator tracks both event timestamps and explicit watermarks.
674pub struct SourceProvidedGenerator {
675    /// Last watermark from the source
676    source_watermark: i64,
677    /// Fallback generator for when source doesn't provide watermarks
678    fallback: BoundedOutOfOrdernessGenerator,
679    /// Whether to use source watermarks when available
680    prefer_source: bool,
681}
682
683impl SourceProvidedGenerator {
684    /// Creates a new source-provided generator.
685    ///
686    /// # Arguments
687    ///
688    /// * `fallback_lateness` - Lateness for fallback bounded generator
689    /// * `prefer_source` - If true, source watermarks take precedence
690    #[must_use]
691    pub fn new(fallback_lateness: i64, prefer_source: bool) -> Self {
692        Self {
693            source_watermark: i64::MIN,
694            fallback: BoundedOutOfOrdernessGenerator::new(fallback_lateness),
695            prefer_source,
696        }
697    }
698
699    /// Updates the watermark from the source.
700    ///
701    /// Call this when the source provides an explicit watermark.
702    pub fn on_source_watermark(&mut self, watermark: i64) -> Option<Watermark> {
703        if watermark > self.source_watermark {
704            self.source_watermark = watermark;
705            if self.prefer_source || watermark > self.fallback.current_watermark() {
706                return Some(Watermark::new(watermark));
707            }
708        }
709        None
710    }
711}
712
713impl WatermarkGenerator for SourceProvidedGenerator {
714    fn on_event(&mut self, timestamp: i64) -> Option<Watermark> {
715        let fallback_wm = self.fallback.on_event(timestamp);
716
717        if self.prefer_source {
718            // Only emit if source watermark allows and it's an advancement
719            if self.source_watermark > i64::MIN {
720                return None; // Wait for source watermark
721            }
722        }
723
724        fallback_wm
725    }
726
727    fn on_periodic(&mut self) -> Option<Watermark> {
728        None
729    }
730
731    fn current_watermark(&self) -> i64 {
732        if self.prefer_source && self.source_watermark > i64::MIN {
733            self.source_watermark
734        } else {
735            self.fallback.current_watermark().max(self.source_watermark)
736        }
737    }
738
739    fn advance_watermark(&mut self, timestamp: i64) -> Option<Watermark> {
740        self.on_source_watermark(timestamp)
741    }
742
743    fn restore_watermark_for_recovery(&mut self, timestamp: i64) {
744        self.source_watermark = timestamp;
745        self.fallback.restore_watermark_for_recovery(timestamp);
746    }
747}
748
749/// Processing-time watermark generator.
750///
751/// Ignores event timestamps entirely and advances the watermark based on
752/// wall-clock time. Use with `PROCTIME()` sources where events are processed
753/// in arrival order without event-time semantics.
754///
755/// - [`on_event`](WatermarkGenerator::on_event) returns `None` (event timestamps are ignored)
756/// - [`on_periodic`](WatermarkGenerator::on_periodic) returns `Some(Watermark::new(now_millis()))`
757///
758/// # Example
759///
760/// ```rust
761/// use laminar_core::time::{ProcessingTimeGenerator, WatermarkGenerator};
762///
763/// let mut gen = ProcessingTimeGenerator::new();
764/// // on_event ignores the timestamp
765/// assert_eq!(gen.on_event(1000), None);
766/// // on_periodic returns wall-clock time
767/// let wm = gen.on_periodic();
768/// assert!(wm.is_some());
769/// ```
770pub struct ProcessingTimeGenerator {
771    current_watermark: i64,
772}
773
774impl ProcessingTimeGenerator {
775    /// Creates a new processing-time watermark generator.
776    #[must_use]
777    pub fn new() -> Self {
778        Self {
779            current_watermark: i64::MIN,
780        }
781    }
782}
783
784impl Default for ProcessingTimeGenerator {
785    fn default() -> Self {
786        Self::new()
787    }
788}
789
790impl WatermarkGenerator for ProcessingTimeGenerator {
791    #[inline]
792    fn on_event(&mut self, _timestamp: i64) -> Option<Watermark> {
793        // Processing-time mode ignores event timestamps
794        None
795    }
796
797    #[inline]
798    fn on_periodic(&mut self) -> Option<Watermark> {
799        let now = super::now_unix_millis();
800        if now > self.current_watermark {
801            self.current_watermark = now;
802            Some(Watermark::new(now))
803        } else {
804            None
805        }
806    }
807
808    #[inline]
809    fn current_watermark(&self) -> i64 {
810        self.current_watermark
811    }
812
813    #[inline]
814    fn advance_watermark(&mut self, timestamp: i64) -> Option<Watermark> {
815        if timestamp > self.current_watermark {
816            self.current_watermark = timestamp;
817            Some(Watermark::new(timestamp))
818        } else {
819            None
820        }
821    }
822
823    #[inline]
824    fn restore_watermark_for_recovery(&mut self, timestamp: i64) {
825        self.current_watermark = timestamp;
826    }
827
828    #[inline]
829    fn is_processing_time(&self) -> bool {
830        true
831    }
832}
833
834#[cfg(test)]
835mod tests;