Skip to main content

laminar_connectors/connector/
sink.rs

1//! Sink lifecycle, write, flush, rollback, and coordinated-commit protocol.
2
3use std::sync::Arc;
4
5use arrow_array::RecordBatch;
6use arrow_schema::SchemaRef;
7use async_trait::async_trait;
8
9use crate::config::ConnectorConfig;
10use crate::error::ConnectorError;
11
12use super::{
13    ConnectorCancellationPolicy, ConnectorTaskTracker, CoordinatedAbortCleaner,
14    CoordinatedCommitter, SinkContract,
15};
16
17/// Durable runtime identity bound to checkpoint-committable sink staging.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct SinkRuntimeContext {
20    /// Create-once deployment UUID shared by checkpoint recovery.
21    pub deployment_id: String,
22    /// Stable sink registration name within the pipeline.
23    pub sink_id: String,
24    /// Stable nonzero checkpoint participant identifier.
25    pub participant_id: u64,
26}
27
28/// Summary of a successful `write_batch` call.
29#[derive(Debug, Clone)]
30pub struct WriteResult {
31    /// Records accepted by the sink.
32    pub records_written: usize,
33    /// Bytes written to the underlying transport (may be estimated).
34    pub bytes_written: u64,
35}
36
37impl WriteResult {
38    /// Construct with raw counts.
39    #[must_use]
40    pub fn new(records_written: usize, bytes_written: u64) -> Self {
41        Self {
42            records_written,
43            bytes_written,
44        }
45    }
46}
47
48/// Trait for sink connectors that write data to external systems.
49///
50/// Sink connectors operate in Ring 1, receiving data from Ring 0 and
51/// writing to external systems. Implementations whose contract is
52/// [`crate::connector::SinkConsistency::CheckpointCommittable`] prepare checkpoint-owned
53/// committables with `begin_epoch`/`pre_commit`, expose a
54/// [`CoordinatedCommitter`] for the single external commit, and implement
55/// `rollback_epoch`; the runtime drives them via the checkpoint coordinator.
56///
57/// All sinks follow `open()` → `write_batch()`/`flush()` → `close()`.
58/// Checkpoint-committable sinks additionally loop over `begin_epoch()`, staged
59/// writes, `pre_commit()`, and coordinated commit (or `rollback_epoch()` on a
60/// proven pre-decision failure).
61#[async_trait]
62pub trait SinkConnector: Send {
63    /// Deadline behavior required by the underlying client implementation.
64    ///
65    /// Retirement is the conservative default: a new connector must not be
66    /// reused after cancellation until every lifecycle future has been audited.
67    fn cancellation_policy(&self) -> ConnectorCancellationPolicy {
68        ConnectorCancellationPolicy::RetireConnector
69    }
70
71    /// Bind the checkpoint runtime identity before `open`.
72    ///
73    /// The default is a no-op for sinks whose object naming does not depend on
74    /// checkpoint identity.
75    ///
76    /// # Errors
77    ///
78    /// Implementations return an error when the supplied identity is invalid
79    /// or cannot be applied in the connector's current lifecycle state.
80    fn bind_runtime_context(&mut self, _context: SinkRuntimeContext) -> Result<(), ConnectorError> {
81        Ok(())
82    }
83
84    /// Observe detached tasks whose lifetime may outlast this connector value.
85    ///
86    /// A connector that spawns detached work must retain the matching
87    /// [`crate::connector::ConnectorTaskOwner`] and move a guard into every task. The runtime can
88    /// then wait for true terminal completion after dropping the connector.
89    fn terminal_task_tracker(&self) -> Option<ConnectorTaskTracker> {
90        None
91    }
92
93    /// Declare durability, placement, and input semantics for this exact
94    /// configuration without opening files, sockets, clients, or transactions.
95    ///
96    /// The fail-closed default is an ephemeral append-only singleton. Durable
97    /// or distributed behaviour must be opted into explicitly.
98    ///
99    /// # Errors
100    ///
101    /// Returns an error when the concrete configuration cannot provide a valid
102    /// durability, placement, or input contract. The default implementation never fails.
103    fn contract(&self, _config: &ConnectorConfig) -> Result<SinkContract, ConnectorError> {
104        Ok(SinkContract::default())
105    }
106
107    /// Key columns by which this configuration applies keyed mutations: rows whose trailing
108    /// Utf8 `_op` is `U` replace the row with that key and rows whose `_op` is `D` delete it,
109    /// reading only the key columns. Last writer wins per key within one flush.
110    ///
111    /// `None` (the default) means the configuration cannot faithfully apply key-only deletes,
112    /// so keyed-mutation sources cannot feed it directly.
113    ///
114    /// # Errors
115    ///
116    /// Returns an error when the configuration itself is invalid.
117    fn keyed_mutation_key(
118        &self,
119        _config: &ConnectorConfig,
120    ) -> Result<Option<Vec<String>>, ConnectorError> {
121        Ok(None)
122    }
123
124    /// Resolve a writer contract from already-bound query output before activation.
125    /// Destinations with authoritative schemas override this read-only hook.
126    async fn resolve_schema(
127        &mut self,
128        config: &ConnectorConfig,
129        input: SchemaRef,
130    ) -> Result<crate::schema::resolution::SchemaBinding, ConnectorError> {
131        crate::schema::resolution::logical_binding(
132            config,
133            crate::schema::resolution::SchemaDirection::Sink,
134            crate::schema::resolution::SchemaOrigin::Query,
135            &input,
136        )
137    }
138
139    /// Perform only explicitly configured, idempotent external preparation after
140    /// read-only resolution and deployment/delivery admission. The returned binding
141    /// must contain the concrete identity used during subsequent activation/replay.
142    async fn prepare_schema(
143        &mut self,
144        _config: &ConnectorConfig,
145        _binding: &mut crate::schema::resolution::SchemaBinding,
146    ) -> Result<(), ConnectorError> {
147        Ok(())
148    }
149
150    /// Open the connection and prepare to accept writes.
151    async fn open(&mut self, config: &ConnectorConfig) -> Result<(), ConnectorError>;
152
153    /// Implementations using [`ConnectorCancellationPolicy::CancelSafe`] must
154    /// remain valid when this future is dropped at a deadline. For
155    /// [`ConnectorCancellationPolicy::RetireConnector`], cancellation makes the
156    /// complete connector instance terminal before later work can be processed.
157    async fn write_batch(&mut self, batch: &RecordBatch) -> Result<WriteResult, ConnectorError>;
158
159    /// Expected Arrow schema of input batches.
160    fn schema(&self) -> SchemaRef;
161
162    /// Begin checkpoint-owned staging. Called only for an admitted
163    /// checkpoint-committable contract; weaker sinks use the no-op default.
164    async fn begin_epoch(&mut self, _epoch: u64) -> Result<(), ConnectorError> {
165        Ok(())
166    }
167
168    /// Return bounded recovery evidence before the runtime allows this epoch to write.
169    ///
170    /// The checkpoint store persists the result before [`begin_epoch`](Self::begin_epoch). It
171    /// must contain no credentials and must deterministically identify only artifacts owned by
172    /// this runtime context and epoch. `None` means the connector has no cleanup payload; it does
173    /// not prove that the connector created no external artifacts.
174    async fn checkpoint_artifact_intent(
175        &mut self,
176        _epoch: u64,
177    ) -> Result<Option<Vec<u8>>, ConnectorError> {
178        Ok(None)
179    }
180
181    /// Flush + prepare, but do not finalize externally. The runtime persists
182    /// the checkpoint decision before a designated committer finalizes the
183    /// collected descriptors; on failure it calls `rollback_epoch`.
184    ///
185    /// Returns an opaque commit descriptor for checkpoint-committable sinks (the
186    /// committables the designated committer will aggregate), else `None`.
187    /// Default delegates to `flush()` and returns `None`.
188    ///
189    /// # Errors
190    /// Returns `ConfigurationError` if the sink exposes a coordinated committer
191    /// yet relies on this default — it would finalize epochs with no external commit.
192    async fn pre_commit(&mut self, _epoch: u64) -> Result<Option<Vec<u8>>, ConnectorError> {
193        if self.as_coordinated_committer().is_some() {
194            return Err(ConnectorError::ConfigurationError(
195                "sink exposes a coordinated committer but does not override pre_commit".into(),
196            ));
197        }
198        self.flush().await?;
199        Ok(None)
200    }
201
202    /// Must be idempotent. The runtime calls this on every
203    /// checkpoint-committable sink after proving a pre-decision failure,
204    /// including sinks that never completed `pre_commit`.
205    async fn rollback_epoch(&mut self, _epoch: u64) -> Result<(), ConnectorError> {
206        Ok(())
207    }
208
209    /// Default per-call `write_batch` I/O timeout. Users can override this via
210    /// the `sink.write.timeout.ms` connector property.
211    fn suggested_write_timeout(&self) -> std::time::Duration;
212
213    /// Maximum residence time for a non-empty sink buffer before the runtime
214    /// invokes [`flush`](Self::flush). Checkpoint-committable sinks ignore the
215    /// periodic timer and flush only through their checkpoint protocol.
216    fn flush_interval(&self) -> std::time::Duration {
217        std::time::Duration::from_secs(5)
218    }
219
220    /// Must be internally bounded — the sink task's periodic timer
221    /// calls this on every tick. Thorough drains belong in `pre_commit`
222    /// / coordinated commit / `close`, not here.
223    async fn flush(&mut self) -> Result<(), ConnectorError> {
224        Ok(())
225    }
226
227    /// Close the sink and release resources.
228    async fn close(&mut self) -> Result<(), ConnectorError>;
229
230    /// Leader-side committer for a checkpoint-committable contract; `None`
231    /// for every weaker contract.
232    fn as_coordinated_committer(&self) -> Option<&dyn CoordinatedCommitter> {
233        None
234    }
235
236    /// Detached cleanup authority retained by recovery after the participant writer closes.
237    ///
238    /// `None` means this connector has no artifacts that require immediate cleanup after an
239    /// authoritative Abort. Files intentionally delegated to a retention-safe maintenance policy
240    /// do not require a cleaner here.
241    fn coordinated_abort_cleaner(&self) -> Option<Arc<dyn CoordinatedAbortCleaner>> {
242        None
243    }
244}