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}