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}