Skip to main content

laminar_core/streaming/
source.rs

1//! Source — entry point for data into a streaming pipeline.
2
3use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
4use std::sync::{Arc, OnceLock};
5use std::time::Duration;
6
7use arrow::array::RecordBatch;
8use arrow::datatypes::SchemaRef;
9
10use super::channel::{channel_with_config, Producer};
11use super::config::SourceConfig;
12use super::error::{StreamingError, TryPushError};
13use super::sink::Sink;
14
15/// Trait for types that can be streamed through a Source.
16pub trait Record: Clone + Send + Sized + 'static {
17    /// Returns the Arrow schema for this record type.
18    fn schema() -> SchemaRef;
19
20    /// Converts this record to an Arrow `RecordBatch`.
21    ///
22    /// The batch will contain a single row with this record's data.
23    fn to_record_batch(&self) -> RecordBatch;
24
25    /// Returns the event time for this record, if applicable.
26    ///
27    /// Event time is used for watermark generation and window assignment.
28    /// Returns `None` if the record doesn't have an event time.
29    fn event_time(&self) -> Option<i64> {
30        None
31    }
32
33    /// Converts a batch of records to an Arrow `RecordBatch`.
34    ///
35    /// The default implementation converts each record individually and concatenates them.
36    /// Derived implementations can override this to optimize allocation and copying.
37    fn to_record_batch_from_iter<I>(records: I) -> RecordBatch
38    where
39        I: IntoIterator<Item = Self>,
40    {
41        let batches: Vec<RecordBatch> = records.into_iter().map(|r| r.to_record_batch()).collect();
42        if batches.is_empty() {
43            return RecordBatch::new_empty(Self::schema());
44        }
45        arrow::compute::concat_batches(&Self::schema(), &batches)
46            .unwrap_or_else(|_| RecordBatch::new_empty(Self::schema()))
47    }
48}
49
50/// Internal message type that wraps records and control signals.
51#[derive(Clone)]
52pub(crate) enum SourceMessage<T> {
53    /// A data record.
54    Record(T),
55
56    /// A batch of Arrow records.
57    Batch(RecordBatch),
58}
59
60/// Shared state for watermark tracking.
61struct SourceWatermark {
62    /// Current watermark value.
63    /// Atomically updated to support multi-producer scenarios.
64    /// Wrapped in `Arc` so the checkpoint manager can read it without locking.
65    current: Arc<AtomicI64>,
66}
67
68impl SourceWatermark {
69    fn new() -> Self {
70        Self {
71            current: Arc::new(AtomicI64::new(i64::MIN)),
72        }
73    }
74
75    fn from_arc(arc: Arc<AtomicI64>) -> Self {
76        Self { current: arc }
77    }
78
79    fn update(&self, timestamp: i64) {
80        // Only advance watermark, never go backwards
81        let mut current = self.current.load(Ordering::Acquire);
82        while timestamp > current {
83            match self.current.compare_exchange_weak(
84                current,
85                timestamp,
86                Ordering::AcqRel,
87                Ordering::Acquire,
88            ) {
89                Ok(_) => break,
90                Err(actual) => current = actual,
91            }
92        }
93    }
94
95    fn restore_for_recovery(&self, timestamp: i64) {
96        self.current.store(timestamp, Ordering::Release);
97    }
98
99    fn get(&self) -> i64 {
100        self.current.load(Ordering::Acquire)
101    }
102
103    fn arc(&self) -> Arc<AtomicI64> {
104        Arc::clone(&self.current)
105    }
106}
107
108/// Shared state for a Source/Sink pair.
109struct SourceInner<T: Record> {
110    /// Channel producer for sending records.
111    producer: Producer<SourceMessage<T>>,
112
113    /// Watermark state.
114    watermark: SourceWatermark,
115
116    /// Schema for type validation.
117    schema: SchemaRef,
118
119    /// Source name (for debugging/metrics).
120    name: Option<String>,
121
122    /// Monotonic sequence counter, incremented on each successful push.
123    /// Wrapped in `Arc` so the checkpoint manager can read it without locking.
124    sequence: Arc<AtomicU64>,
125
126    /// Event-time column name set via programmatic API.
127    /// Read once at pipeline startup, not on the hot path.
128    event_time_column: OnceLock<String>,
129
130    /// Max out-of-orderness bound, paired with `event_time_column`.
131    /// Read once at pipeline startup, not on the hot path.
132    max_out_of_orderness: OnceLock<Duration>,
133}
134
135/// A streaming data source. Cloneable for multi-producer use.
136pub struct Source<T: Record> {
137    inner: Arc<SourceInner<T>>,
138}
139
140impl<T: Record> Source<T> {
141    /// Creates a new Source/Sink pair.
142    pub(crate) fn new(config: SourceConfig) -> (Self, Sink<T>) {
143        let channel_config = config.channel;
144        let (producer, consumer) = channel_with_config::<SourceMessage<T>>(&channel_config);
145
146        let schema = T::schema();
147
148        let inner = Arc::new(SourceInner {
149            producer,
150            watermark: SourceWatermark::new(),
151            schema: schema.clone(),
152            name: config.name,
153            sequence: Arc::new(AtomicU64::new(0)),
154            event_time_column: OnceLock::new(),
155            max_out_of_orderness: OnceLock::new(),
156        });
157
158        let source = Self { inner };
159        let sink = Sink::new(consumer, schema);
160
161        (source, sink)
162    }
163
164    /// Pushes a record. Non-blocking — returns `ChannelFull` if the buffer is full.
165    ///
166    /// # Errors
167    ///
168    /// Returns `StreamingError::ChannelFull` if the buffer is full or the sink was dropped.
169    pub fn push(&self, record: T) -> Result<(), StreamingError> {
170        if let Some(event_time) = record.event_time() {
171            self.inner.watermark.update(event_time);
172        }
173
174        self.inner
175            .producer
176            .push(SourceMessage::Record(record))
177            .map_err(|_| StreamingError::ChannelFull)?;
178
179        self.inner.sequence.fetch_add(1, Ordering::Relaxed);
180        Ok(())
181    }
182
183    /// Pushes a record, returning it on failure.
184    ///
185    /// # Errors
186    ///
187    /// Returns `TryPushError` containing the record if the channel is full.
188    pub fn try_push(&self, record: T) -> Result<(), TryPushError<T>> {
189        if let Some(event_time) = record.event_time() {
190            self.inner.watermark.update(event_time);
191        }
192
193        self.inner
194            .producer
195            .push(SourceMessage::Record(record))
196            .map_err(|msg| match msg {
197                SourceMessage::Record(r) => TryPushError {
198                    value: r,
199                    error: StreamingError::ChannelFull,
200                },
201                SourceMessage::Batch(_) => unreachable!("only Record is pushed here"),
202            })?;
203
204        self.inner.sequence.fetch_add(1, Ordering::Relaxed);
205        Ok(())
206    }
207
208    /// Pushes multiple records (cloned). Stops at the first failure.
209    pub fn push_batch(&self, records: &[T]) -> usize
210    where
211        T: Clone,
212    {
213        self.push_batch_drain(records.iter().cloned())
214    }
215
216    /// Pushes records from an iterator, consuming them (zero-clone).
217    /// Stops at the first failure. Returns the number pushed.
218    pub fn push_batch_drain<I>(&self, records: I) -> usize
219    where
220        I: IntoIterator<Item = T>,
221    {
222        let mut count = 0;
223        for record in records {
224            if self.push(record).is_err() {
225                break;
226            }
227            count += 1;
228        }
229        count
230    }
231
232    /// Pushes an Arrow `RecordBatch` directly.
233    ///
234    /// This is more efficient than pushing individual records when you
235    /// already have data in Arrow format.
236    ///
237    /// # Errors
238    ///
239    /// Returns `StreamingError::SchemaMismatch` if the batch schema doesn't match.
240    /// Returns `StreamingError::ChannelClosed` if the sink has been dropped.
241    pub fn push_arrow(&self, batch: RecordBatch) -> Result<(), StreamingError> {
242        // Validate schema matches (skip for type-erased sources with empty schema)
243        if !self.inner.schema.fields().is_empty() && batch.schema() != self.inner.schema {
244            return Err(StreamingError::SchemaMismatch {
245                expected: self
246                    .inner
247                    .schema
248                    .fields()
249                    .iter()
250                    .map(|f| f.name().clone())
251                    .collect(),
252                actual: batch
253                    .schema()
254                    .fields()
255                    .iter()
256                    .map(|f| f.name().clone())
257                    .collect(),
258            });
259        }
260
261        self.inner
262            .producer
263            .push(SourceMessage::Batch(batch))
264            .map_err(|_| StreamingError::ChannelFull)?;
265
266        self.inner.sequence.fetch_add(1, Ordering::Relaxed);
267        Ok(())
268    }
269
270    /// Emits a watermark timestamp.
271    ///
272    /// Watermarks signal that no events with timestamps less than or equal
273    /// to this value will arrive in the future. This enables window triggers
274    /// and garbage collection.
275    ///
276    /// Watermarks are monotonically increasing - if a lower timestamp is
277    /// passed, it will be ignored.
278    pub fn watermark(&self, timestamp: i64) {
279        // The shared atomic is the authoritative watermark: the pipeline's
280        // watermark UDF, late-row filter, and checkpoint registration all
281        // read it via `watermark_atomic()`. Subscribers receive data only,
282        // so there is no in-band watermark message to emit.
283        self.inner.watermark.update(timestamp);
284    }
285
286    /// Replaces the shared watermark with an exact recovered value.
287    ///
288    /// Unlike [`Self::watermark`], this may lower the watermark. The caller
289    /// must hold the intake/compute recovery fence and ensure every clone and
290    /// producer of this source is quiesced until restoration completes. Use
291    /// [`Self::watermark`] during normal operation.
292    pub fn restore_watermark_for_recovery(&self, timestamp: i64) {
293        self.inner.watermark.restore_for_recovery(timestamp);
294    }
295
296    /// Returns the current watermark value.
297    #[must_use]
298    pub fn current_watermark(&self) -> i64 {
299        self.inner.watermark.get()
300    }
301
302    /// Returns the schema for this source.
303    #[must_use]
304    pub fn schema(&self) -> SchemaRef {
305        Arc::clone(&self.inner.schema)
306    }
307
308    /// Returns the source name, if configured.
309    #[must_use]
310    pub fn name(&self) -> Option<&str> {
311        self.inner.name.as_deref()
312    }
313
314    /// Returns true if the sink has been dropped.
315    #[must_use]
316    pub fn is_closed(&self) -> bool {
317        self.inner.producer.is_closed()
318    }
319
320    /// Returns the number of pending items in the buffer.
321    #[must_use]
322    pub fn pending(&self) -> usize {
323        self.inner.producer.len()
324    }
325
326    /// Returns the buffer capacity.
327    #[must_use]
328    pub fn capacity(&self) -> usize {
329        self.inner.producer.capacity()
330    }
331
332    /// Returns the current sequence number (total successful pushes).
333    #[must_use]
334    pub fn sequence(&self) -> u64 {
335        self.inner.sequence.load(Ordering::Acquire)
336    }
337
338    /// Returns the shared sequence counter for checkpoint registration.
339    #[must_use]
340    pub fn sequence_counter(&self) -> Arc<AtomicU64> {
341        Arc::clone(&self.inner.sequence)
342    }
343
344    /// Returns the shared watermark atomic for checkpoint registration.
345    #[must_use]
346    pub fn watermark_atomic(&self) -> Arc<AtomicI64> {
347        self.inner.watermark.arc()
348    }
349
350    /// Declare which column in the source data represents event time.
351    ///
352    /// When set, `source.watermark()` enables late-row filtering
353    /// without a SQL `WATERMARK FOR` clause.
354    ///
355    /// Only the first call takes effect; subsequent calls are silently ignored.
356    pub fn set_event_time_column(&self, column: &str) {
357        let _ = self.inner.event_time_column.set(column.to_owned());
358    }
359
360    /// Returns the configured event-time column, if any.
361    #[must_use]
362    pub fn event_time_column(&self) -> Option<String> {
363        self.inner.event_time_column.get().cloned()
364    }
365
366    /// Set the max out-of-orderness bound for watermark generation.
367    ///
368    /// Only the first call takes effect; subsequent calls are silently ignored.
369    pub fn set_max_out_of_orderness(&self, dur: Duration) {
370        let _ = self.inner.max_out_of_orderness.set(dur);
371    }
372
373    /// Returns the configured max out-of-orderness, if any.
374    #[must_use]
375    pub fn max_out_of_orderness(&self) -> Option<Duration> {
376        self.inner.max_out_of_orderness.get().copied()
377    }
378}
379
380impl<T: Record> Clone for Source<T> {
381    fn clone(&self) -> Self {
382        let producer = self.inner.producer.clone();
383        let event_time_col = self.inner.event_time_column.get().cloned();
384        let event_time_column = OnceLock::new();
385        if let Some(col) = event_time_col {
386            let _ = event_time_column.set(col);
387        }
388        let max_ooo = self.inner.max_out_of_orderness.get().copied();
389        let max_out_of_orderness = OnceLock::new();
390        if let Some(dur) = max_ooo {
391            let _ = max_out_of_orderness.set(dur);
392        }
393        Self {
394            inner: Arc::new(SourceInner {
395                producer,
396                watermark: SourceWatermark::from_arc(self.inner.watermark.arc()),
397                schema: Arc::clone(&self.inner.schema),
398                name: self.inner.name.clone(),
399                sequence: Arc::clone(&self.inner.sequence),
400                event_time_column,
401                max_out_of_orderness,
402            }),
403        }
404    }
405}
406
407impl<T: Record + std::fmt::Debug> std::fmt::Debug for Source<T> {
408    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409        f.debug_struct("Source")
410            .field("name", &self.inner.name)
411            .field("pending", &self.pending())
412            .field("capacity", &self.capacity())
413            .field("watermark", &self.current_watermark())
414            .finish()
415    }
416}
417
418/// Creates a new Source/Sink pair with the given buffer size.
419#[must_use]
420pub fn create<T: Record>(buffer_size: usize) -> (Source<T>, Sink<T>) {
421    Source::new(SourceConfig::with_buffer_size(buffer_size))
422}
423
424/// Creates a new Source/Sink pair with custom configuration.
425#[must_use]
426pub fn create_with_config<T: Record>(config: SourceConfig) -> (Source<T>, Sink<T>) {
427    Source::new(config)
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433    use arrow::array::{Float64Array, Int64Array, StringArray};
434    use arrow::datatypes::{DataType, Field, Schema};
435    use std::sync::Arc;
436
437    // Test record type
438    #[derive(Clone, Debug)]
439    struct TestEvent {
440        id: i64,
441        value: f64,
442        timestamp: i64,
443    }
444
445    impl Record for TestEvent {
446        fn schema() -> SchemaRef {
447            Arc::new(Schema::new(vec![
448                Field::new("id", DataType::Int64, false),
449                Field::new("value", DataType::Float64, false),
450                Field::new("timestamp", DataType::Int64, false),
451            ]))
452        }
453
454        fn to_record_batch(&self) -> RecordBatch {
455            RecordBatch::try_new(
456                Self::schema(),
457                vec![
458                    Arc::new(Int64Array::from(vec![self.id])),
459                    Arc::new(Float64Array::from(vec![self.value])),
460                    Arc::new(Int64Array::from(vec![self.timestamp])),
461                ],
462            )
463            .unwrap()
464        }
465
466        fn event_time(&self) -> Option<i64> {
467            Some(self.timestamp)
468        }
469    }
470
471    #[tokio::test]
472    async fn test_create_source_sink() {
473        let (source, _sink) = create::<TestEvent>(1024);
474
475        assert!(!source.is_closed());
476        assert_eq!(source.pending(), 0);
477    }
478
479    #[tokio::test]
480    async fn test_push_single() {
481        let (source, _sink) = create::<TestEvent>(16);
482
483        let event = TestEvent {
484            id: 1,
485            value: 42.0,
486            timestamp: 1000,
487        };
488
489        assert!(source.push(event).is_ok());
490        assert_eq!(source.pending(), 1);
491    }
492
493    #[tokio::test]
494    async fn test_try_push() {
495        let (source, _sink) = create::<TestEvent>(16);
496
497        let event = TestEvent {
498            id: 1,
499            value: 42.0,
500            timestamp: 1000,
501        };
502
503        assert!(source.try_push(event).is_ok());
504    }
505
506    #[tokio::test]
507    async fn test_push_batch() {
508        let (source, _sink) = create::<TestEvent>(16);
509
510        let events = vec![
511            TestEvent {
512                id: 1,
513                value: 1.0,
514                timestamp: 1000,
515            },
516            TestEvent {
517                id: 2,
518                value: 2.0,
519                timestamp: 2000,
520            },
521            TestEvent {
522                id: 3,
523                value: 3.0,
524                timestamp: 3000,
525            },
526        ];
527
528        let count = source.push_batch(&events);
529        assert_eq!(count, 3);
530        assert_eq!(source.pending(), 3);
531    }
532
533    #[tokio::test]
534    async fn test_push_arrow() {
535        let (source, _sink) = create::<TestEvent>(16);
536
537        let batch = RecordBatch::try_new(
538            TestEvent::schema(),
539            vec![
540                Arc::new(Int64Array::from(vec![1, 2, 3])),
541                Arc::new(Float64Array::from(vec![1.0, 2.0, 3.0])),
542                Arc::new(Int64Array::from(vec![1000, 2000, 3000])),
543            ],
544        )
545        .unwrap();
546
547        assert!(source.push_arrow(batch).is_ok());
548    }
549
550    #[tokio::test]
551    async fn test_push_arrow_schema_mismatch() {
552        let (source, _sink) = create::<TestEvent>(16);
553
554        // Create batch with different schema
555        let wrong_schema = Arc::new(Schema::new(vec![Field::new(
556            "wrong",
557            DataType::Utf8,
558            false,
559        )]));
560
561        let batch = RecordBatch::try_new(
562            wrong_schema,
563            vec![Arc::new(StringArray::from(vec!["test"]))],
564        )
565        .unwrap();
566
567        let result = source.push_arrow(batch);
568        assert!(matches!(result, Err(StreamingError::SchemaMismatch { .. })));
569    }
570
571    #[tokio::test]
572    async fn test_watermark() {
573        let (source, _sink) = create::<TestEvent>(16);
574
575        assert_eq!(source.current_watermark(), i64::MIN);
576
577        source.watermark(1000);
578        assert_eq!(source.current_watermark(), 1000);
579
580        source.watermark(2000);
581        assert_eq!(source.current_watermark(), 2000);
582
583        // Watermark should not go backwards
584        source.watermark(1500);
585        assert_eq!(source.current_watermark(), 2000);
586    }
587
588    #[tokio::test]
589    async fn recovery_restore_lowers_shared_watermark_then_runtime_advances() {
590        let (source, _sink) = create::<TestEvent>(16);
591        let clone = source.clone();
592        source.watermark(2_000);
593
594        source.restore_watermark_for_recovery(500);
595        assert_eq!(source.current_watermark(), 500);
596        assert_eq!(clone.current_watermark(), 500);
597
598        clone.watermark(600);
599        assert_eq!(source.current_watermark(), 600);
600        clone.watermark(550);
601        assert_eq!(source.current_watermark(), 600);
602    }
603
604    #[tokio::test]
605    async fn test_watermark_from_event_time() {
606        let (source, _sink) = create::<TestEvent>(16);
607
608        let event = TestEvent {
609            id: 1,
610            value: 42.0,
611            timestamp: 5000,
612        };
613
614        source.push(event).unwrap();
615
616        // Watermark should be updated from event time
617        assert_eq!(source.current_watermark(), 5000);
618    }
619
620    #[tokio::test]
621    async fn test_clone_multi_producer() {
622        let (source, sink) = create::<TestEvent>(16);
623        let source2 = source.clone();
624        let mut sub = sink.subscribe(); // subscribe before push
625
626        source
627            .push(TestEvent {
628                id: 1,
629                value: 1.0,
630                timestamp: 1000,
631            })
632            .unwrap();
633        source2
634            .push(TestEvent {
635                id: 2,
636                value: 2.0,
637                timestamp: 2000,
638            })
639            .unwrap();
640
641        tokio::time::sleep(std::time::Duration::from_millis(10)).await;
642        assert!(sub.poll().is_some());
643        assert!(sub.poll().is_some());
644    }
645
646    #[tokio::test]
647    async fn test_schema() {
648        let (source, _sink) = create::<TestEvent>(16);
649
650        let schema = source.schema();
651        assert_eq!(schema.fields().len(), 3);
652        assert_eq!(schema.field(0).name(), "id");
653        assert_eq!(schema.field(1).name(), "value");
654        assert_eq!(schema.field(2).name(), "timestamp");
655    }
656
657    #[tokio::test]
658    async fn test_named_source() {
659        let config = SourceConfig::named("my_source");
660        let (source, _sink) = create_with_config::<TestEvent>(config);
661
662        assert_eq!(source.name(), Some("my_source"));
663    }
664
665    #[tokio::test]
666    async fn test_debug_format() {
667        let (source, _sink) = create::<TestEvent>(16);
668
669        let debug = format!("{source:?}");
670        assert!(debug.contains("Source"));
671    }
672
673    #[tokio::test]
674    async fn test_set_event_time_column() {
675        let (source, _sink) = create::<TestEvent>(16);
676
677        assert!(source.event_time_column().is_none());
678
679        source.set_event_time_column("timestamp");
680        assert_eq!(source.event_time_column(), Some("timestamp".to_string()));
681    }
682
683    #[tokio::test]
684    async fn test_event_time_column_preserved_on_clone() {
685        let (source, _sink) = create::<TestEvent>(16);
686        source.set_event_time_column("ts");
687
688        let source2 = source.clone();
689        assert_eq!(source2.event_time_column(), Some("ts".to_string()));
690    }
691}