Skip to main content

crabka_connect/
source.rs

1//! The [`Source`] SPI: pull records out of an external system, with a
2//! checkpointable read position.
3
4use async_trait::async_trait;
5
6use crate::error::ConnectError;
7use crate::record::{ConnectRecord, SourceOffset};
8
9/// A connector that pulls records out of an external system (a database change
10/// stream, a file tail, a queue) for production into Kafka.
11///
12/// This is the read side of the connector SPI and the template every CDC source
13/// builds on. It mirrors the streams runtime's `RecordFetcher`, but pulls one
14/// record at a time and owns its own read position rather than being told an
15/// offset on every call.
16///
17/// ## Polling
18///
19/// [`poll`](Source::poll) returns the next record, or `None` when the source is
20/// momentarily caught up (the runtime should back off and poll again). Each
21/// successful `poll` advances the source's internal position; the runtime never
22/// passes an offset in — it reads the position back with
23/// [`checkpoint`](Source::checkpoint).
24///
25/// ## Offset state
26///
27/// The runtime persists the [`SourceOffset`] returned by `checkpoint` after the
28/// records it covers have been durably produced, and restores it with
29/// [`seek`](Source::seek) before the first `poll` on restart. The offset is
30/// opaque to the runtime — only the source interprets it — so a source is free
31/// to encode a log sequence number, a byte offset, a GTID set, or whatever
32/// resume token its backend uses.
33///
34/// After the sink commit is durable and checkpoint persistence succeeds, the
35/// runtime calls [`acknowledge`](Source::acknowledge) with the persisted offset.
36/// Sources that hold backend resources such as non-advancing cursors or logical
37/// replication slots can use this hook to release data that is now safe to
38/// discard. The default implementation is a no-op for sources that need no
39/// explicit acknowledgement.
40#[async_trait]
41pub trait Source<K, V>: Send + Sync + 'static {
42    /// Pull the next record, advancing the source's read position.
43    ///
44    /// Returns `Ok(None)` when nothing is available yet — a non-fatal "caught
45    /// up" signal, not end-of-stream. Returns `Err` only on a real failure
46    /// (lost connection, malformed upstream data the source cannot skip).
47    ///
48    /// # Errors
49    ///
50    /// Returns [`ConnectError`] if reading from the external system fails.
51    async fn poll(&mut self) -> Result<Option<ConnectRecord<K, V>>, ConnectError>;
52
53    /// Snapshot the current read position so the runtime can persist it.
54    ///
55    /// Returns `None` before the source has a position to commit (e.g. nothing
56    /// has been polled and no prior offset was restored). The runtime commits a
57    /// checkpoint only after the records preceding it are durable, so on restart
58    /// [`seek`](Source::seek) resumes from the last fully-produced record.
59    fn checkpoint(&self) -> Option<SourceOffset>;
60
61    /// Restore the read position to a previously [`checkpoint`](Source::checkpoint)ed
62    /// offset. Called once, before the first [`poll`](Source::poll), on startup
63    /// and after a rebalance. A source with no stored offset is not sought and
64    /// starts from its configured default position.
65    ///
66    /// # Errors
67    ///
68    /// Returns [`ConnectError::Offset`] if `offset` does not name a position
69    /// this source can resume from (e.g. the upstream log has since been
70    /// truncated past it).
71    async fn seek(&mut self, offset: SourceOffset) -> Result<(), ConnectError>;
72
73    /// Acknowledge that `offset` is durable end-to-end.
74    ///
75    /// The runtime calls this only after a non-empty batch has been committed
76    /// to the sink and the same offset has been saved in the checkpoint store.
77    /// It is never called before checkpoint persistence, so a failing
78    /// checkpoint save prevents upstream acknowledgement. Sources may use this
79    /// to advance external cursors or release retained log segments.
80    ///
81    /// The default is a no-op, preserving existing source implementations.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`ConnectError`] if the upstream acknowledgement fails.
86    async fn acknowledge(&mut self, _offset: &SourceOffset) -> Result<(), ConnectError> {
87        Ok(())
88    }
89
90    /// Release any resources held by the source (connections, file handles).
91    /// The default is a no-op. After `close`, the source is not polled again.
92    ///
93    /// # Errors
94    ///
95    /// Returns [`ConnectError`] if cleanup fails.
96    async fn close(&mut self) -> Result<(), ConnectError> {
97        Ok(())
98    }
99}