Skip to main content

laminar_core/streaming/source/
mod.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;