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