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    /// Open the connection and prepare to accept writes.
108    async fn open(&mut self, config: &ConnectorConfig) -> Result<(), ConnectorError>;
109
110    /// Implementations using [`ConnectorCancellationPolicy::CancelSafe`] must
111    /// remain valid when this future is dropped at a deadline. For
112    /// [`ConnectorCancellationPolicy::RetireConnector`], cancellation makes the
113    /// complete connector instance terminal before later work can be processed.
114    async fn write_batch(&mut self, batch: &RecordBatch) -> Result<WriteResult, ConnectorError>;
115
116    /// Expected Arrow schema of input batches.
117    fn schema(&self) -> SchemaRef;
118
119    /// Begin checkpoint-owned staging. Called only for an admitted
120    /// checkpoint-committable contract; weaker sinks use the no-op default.
121    async fn begin_epoch(&mut self, _epoch: u64) -> Result<(), ConnectorError> {
122        Ok(())
123    }
124
125    /// Return bounded recovery evidence before the runtime allows this epoch to write.
126    ///
127    /// The checkpoint store persists the result before [`begin_epoch`](Self::begin_epoch). It
128    /// must contain no credentials and must deterministically identify only artifacts owned by
129    /// this runtime context and epoch. `None` means the connector has no cleanup payload; it does
130    /// not prove that the connector created no external artifacts.
131    async fn checkpoint_artifact_intent(
132        &mut self,
133        _epoch: u64,
134    ) -> Result<Option<Vec<u8>>, ConnectorError> {
135        Ok(None)
136    }
137
138    /// Flush + prepare, but do not finalize externally. The runtime persists
139    /// the checkpoint decision before a designated committer finalizes the
140    /// collected descriptors; on failure it calls `rollback_epoch`.
141    ///
142    /// Returns an opaque commit descriptor for checkpoint-committable sinks (the
143    /// committables the designated committer will aggregate), else `None`.
144    /// Default delegates to `flush()` and returns `None`.
145    ///
146    /// # Errors
147    /// Returns `ConfigurationError` if the sink exposes a coordinated committer
148    /// yet relies on this default — it would finalize epochs with no external commit.
149    async fn pre_commit(&mut self, _epoch: u64) -> Result<Option<Vec<u8>>, ConnectorError> {
150        if self.as_coordinated_committer().is_some() {
151            return Err(ConnectorError::ConfigurationError(
152                "sink exposes a coordinated committer but does not override pre_commit".into(),
153            ));
154        }
155        self.flush().await?;
156        Ok(None)
157    }
158
159    /// Must be idempotent. The runtime calls this on every
160    /// checkpoint-committable sink after proving a pre-decision failure,
161    /// including sinks that never completed `pre_commit`.
162    async fn rollback_epoch(&mut self, _epoch: u64) -> Result<(), ConnectorError> {
163        Ok(())
164    }
165
166    /// Default per-call `write_batch` I/O timeout. Users can override this via
167    /// the `sink.write.timeout.ms` connector property.
168    fn suggested_write_timeout(&self) -> std::time::Duration;
169
170    /// Maximum residence time for a non-empty sink buffer before the runtime
171    /// invokes [`flush`](Self::flush). Checkpoint-committable sinks ignore the
172    /// periodic timer and flush only through their checkpoint protocol.
173    fn flush_interval(&self) -> std::time::Duration {
174        std::time::Duration::from_secs(5)
175    }
176
177    /// Must be internally bounded — the sink task's periodic timer
178    /// calls this on every tick. Thorough drains belong in `pre_commit`
179    /// / coordinated commit / `close`, not here.
180    async fn flush(&mut self) -> Result<(), ConnectorError> {
181        Ok(())
182    }
183
184    /// Close the sink and release resources.
185    async fn close(&mut self) -> Result<(), ConnectorError>;
186
187    /// Leader-side committer for a checkpoint-committable contract; `None`
188    /// for every weaker contract.
189    fn as_coordinated_committer(&self) -> Option<&dyn CoordinatedCommitter> {
190        None
191    }
192
193    /// Detached cleanup authority retained by recovery after the participant writer closes.
194    ///
195    /// `None` means this connector has no artifacts that require immediate cleanup after an
196    /// authoritative Abort. Files intentionally delegated to a retention-safe maintenance policy
197    /// do not require a cleaner here.
198    fn coordinated_abort_cleaner(&self) -> Option<Arc<dyn CoordinatedAbortCleaner>> {
199        None
200    }
201}