Skip to main content

laminar_connectors/connector/
contracts.rs

1//! Source and sink delivery, topology, and changelog contracts.
2
3use std::str::FromStr;
4
5/// Delivery guarantee level for the pipeline.
6///
7/// Configures the expected end-to-end delivery semantics. The pipeline
8/// validates at startup that all sources and sinks meet the requirements
9/// for the chosen guarantee level.
10#[derive(
11    Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize,
12)]
13#[serde(rename_all = "snake_case")]
14pub enum DeliveryGuarantee {
15    /// Best effort: no replay contract. Intended for bare-metal/embedded
16    /// low-latency pipelines that explicitly accept loss on failure.
17    #[default]
18    BestEffort,
19    /// At-least-once: records may be replayed on recovery. Requires
20    /// checkpointing and replayable sources.
21    AtLeastOnce,
22    /// Exactly-once: no duplicates or losses. Requires all sources to
23    /// support replay, all sinks to support exactly-once, and checkpoint
24    /// to be enabled.
25    ExactlyOnce,
26}
27
28impl std::fmt::Display for DeliveryGuarantee {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        match self {
31            DeliveryGuarantee::BestEffort => write!(f, "best-effort"),
32            DeliveryGuarantee::AtLeastOnce => write!(f, "at-least-once"),
33            DeliveryGuarantee::ExactlyOnce => write!(f, "exactly-once"),
34        }
35    }
36}
37
38impl FromStr for DeliveryGuarantee {
39    type Err = String;
40
41    fn from_str(s: &str) -> Result<Self, Self::Err> {
42        match s.to_lowercase().replace('-', "_").as_str() {
43            "best_effort" | "besteffort" | "none" => Ok(Self::BestEffort),
44            "at_least_once" | "atleastonce" => Ok(Self::AtLeastOnce),
45            "exactly_once" | "exactlyonce" => Ok(Self::ExactlyOnce),
46            other => Err(format!("unknown delivery guarantee: '{other}'")),
47        }
48    }
49}
50
51/// Recovery semantics provided by a source.
52///
53/// This is deliberately a small, ordered set of operational contracts rather
54/// than a collection of independent capability flags. A source must advertise
55/// the strongest contract its implementation can actually uphold.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize)]
57#[serde(rename_all = "snake_case")]
58pub enum SourceConsistency {
59    /// Events cannot be reconstructed after the runtime has accepted them.
60    #[default]
61    Ephemeral,
62    /// A persisted source position can be used to reproduce accepted events.
63    Replayable,
64    /// Replay is supported, and upstream progress/resources advance only when
65    /// the corresponding `LaminarDB` checkpoint is durably committed.
66    CommitCoupled,
67}
68
69impl SourceConsistency {
70    /// Whether a persisted source position can be replayed after recovery.
71    #[must_use]
72    pub const fn supports_replay(self) -> bool {
73        !matches!(self, Self::Ephemeral)
74    }
75
76    /// Whether checkpoint commits are required for safe upstream progress.
77    #[must_use]
78    pub const fn requires_checkpointing(self) -> bool {
79        matches!(self, Self::CommitCoupled)
80    }
81}
82
83/// How a source may be placed across runtime nodes.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize)]
85#[serde(rename_all = "snake_case")]
86pub enum SourceTopology {
87    /// Exactly one runtime instance owns the source.
88    #[default]
89    Singleton,
90    /// Input partitions can be assigned independently across runtime nodes.
91    Splittable,
92    /// Each runtime node receives a distinct, node-local input stream.
93    NodeLocalIngress,
94}
95
96/// Update model emitted by a configured source.
97#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize)]
98#[serde(rename_all = "snake_case")]
99pub enum SourceInputMode {
100    /// Every row is an insertion.
101    #[default]
102    AppendOnly,
103    /// Current row images and deletes are reconciled by the declared primary key.
104    KeyedUpsert,
105    /// Decoded rows carry a non-null, non-zero `Int64` `__weight` column.
106    FullChangelog,
107}
108
109/// Whether a source emits an ordered deterministic position for every decoded row.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize)]
111#[serde(rename_all = "snake_case")]
112pub enum SourceRowPositionCapability {
113    /// The source does not provide row positions.
114    #[default]
115    Unavailable,
116    /// Every emitted row carries a replay position. Within one source run, `(order_key,
117    /// sub_offset)` is nondecreasing per partition across batches; recovery may restart from an
118    /// earlier position. Replaying an equal position must produce the same logical row and
119    /// mutation. This does not define the merge order of independent partitions.
120    OrderedDeterministic,
121}
122
123/// Row order and batch boundaries reproduced when resuming a source cursor.
124///
125/// This is separate from row positions and delivery guarantees. The runtime must enforce
126/// the declared batch boundaries before relying on them for input/timer replay.
127#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize)]
128#[serde(rename_all = "snake_case")]
129pub enum SourceReplayOrder {
130    /// The source does not promise a replay order.
131    #[default]
132    Unspecified,
133    /// One physical input channel reproduces the same ordered suffix from a committed cursor,
134    /// including the order of different keys. Poll size and timing must not reorder rows, and
135    /// recovery must retain the channel identity. Independent-channel merging is excluded;
136    /// this raw row-order promise does not reproduce watermark cuts.
137    SingleChannel,
138    /// One global physical channel, with the same ordered rows in the same nonempty batches
139    /// after a committed cursor. Poll limits and timing must not change batch membership.
140    /// The channel may move between nodes under splittable placement; independent channels
141    /// are excluded. Deterministic row positions and bounded atomic batches are required.
142    ///
143    /// Process-function execution treats each batch as one input/watermark cut. The engine
144    /// derives event-time progress from that batch and excludes external and wall-clock
145    /// advancement. Transport batching may change within a cut without changing its end.
146    SingleChannelFixedBatches,
147}
148
149/// Complete source admission contract for a concrete connector configuration.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize)]
151pub struct SourceContract {
152    /// Recovery and external-progress semantics.
153    pub consistency: SourceConsistency,
154    /// Valid runtime placement model.
155    pub topology: SourceTopology,
156    /// Update model produced after connector decoding.
157    pub input_mode: SourceInputMode,
158    /// Deterministic per-row position support.
159    pub row_positions: SourceRowPositionCapability,
160    /// Order reproduced from a persisted cursor; unspecified unless explicitly declared.
161    pub replay_order: SourceReplayOrder,
162    exact_delivery_certified: bool,
163}
164
165impl SourceContract {
166    /// Construct a source contract from its recovery, placement, and update dimensions.
167    /// Exactly-once certification defaults to fail-closed.
168    #[must_use]
169    pub const fn new(
170        consistency: SourceConsistency,
171        topology: SourceTopology,
172        input_mode: SourceInputMode,
173    ) -> Self {
174        Self {
175            consistency,
176            topology,
177            input_mode,
178            row_positions: SourceRowPositionCapability::Unavailable,
179            replay_order: SourceReplayOrder::Unspecified,
180            exact_delivery_certified: false,
181        }
182    }
183
184    /// Declare the source's per-row position contract.
185    #[must_use]
186    pub const fn with_row_positions(mut self, capability: SourceRowPositionCapability) -> Self {
187        self.row_positions = capability;
188        self
189    }
190
191    /// Declare the row order reproduced from a committed cursor. Connector implementations
192    /// must uphold this for the exact configuration, independently of poll size and timing.
193    #[must_use]
194    pub const fn with_replay_order(mut self, order: SourceReplayOrder) -> Self {
195        self.replay_order = order;
196        self
197    }
198
199    /// Whether the declared source shape supports replay at fixed batch boundaries.
200    /// The runtime must also enforce one physical channel and reproduce its watermark cuts.
201    #[must_use]
202    pub const fn supports_fixed_batch_replay(self) -> bool {
203        self.supports_replay()
204            && matches!(
205                self.topology,
206                SourceTopology::Singleton | SourceTopology::Splittable
207            )
208            && matches!(self.input_mode, SourceInputMode::AppendOnly)
209            && matches!(
210                self.row_positions,
211                SourceRowPositionCapability::OrderedDeterministic
212            )
213            && matches!(
214                self.replay_order,
215                SourceReplayOrder::SingleChannelFixedBatches
216            )
217    }
218
219    /// Mark a built-in connector whose exact-delivery suite is an engine release gate.
220    #[must_use]
221    pub(crate) const fn with_exact_delivery_certification(mut self) -> Self {
222        self.exact_delivery_certified = true;
223        self
224    }
225
226    /// Whether this source is certified for exactly-once delivery.
227    #[doc(hidden)]
228    #[must_use]
229    pub const fn is_exact_delivery_certified(self) -> bool {
230        self.exact_delivery_certified
231    }
232
233    /// Whether a persisted source position can be replayed after recovery.
234    #[must_use]
235    pub const fn supports_replay(self) -> bool {
236        self.consistency.supports_replay()
237    }
238
239    /// Whether checkpoint commits are required for safe upstream progress.
240    #[must_use]
241    pub const fn requires_checkpointing(self) -> bool {
242        self.consistency.requires_checkpointing()
243    }
244}
245
246/// Durability protocol provided by a sink.
247///
248/// This describes externally observable behaviour, not an implementation
249/// detail such as whether the client library buffers or retries writes.
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize)]
251#[serde(rename_all = "snake_case")]
252pub enum SinkConsistency {
253    /// An accepted write can be lost when the connector or peer fails.
254    #[default]
255    Ephemeral,
256    /// Successful writes are durably acknowledged, but replay may duplicate
257    /// them because visibility is not coupled to a `LaminarDB` checkpoint.
258    DurableAtLeastOnce,
259    /// Output can be staged and made visible by the checkpoint commit
260    /// protocol. This is necessary, but not sufficient, for exactly-once
261    /// certification: namespaces and recovery cursors must also be fenced.
262    CheckpointCommittable,
263}
264
265/// How a sink may be placed across runtime nodes.
266#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize)]
267#[serde(rename_all = "snake_case")]
268pub enum SinkTopology {
269    /// Only one fenced runtime writer may target the configured destination.
270    #[default]
271    Singleton,
272    /// Independent runtime writers can safely target the destination.
273    MultiWriter,
274    /// Each runtime node owns a distinct local egress endpoint or audience.
275    NodeLocalEgress,
276}
277
278/// The strongest input update model a configured sink understands.
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize)]
280#[serde(rename_all = "snake_case")]
281pub enum SinkInputMode {
282    /// Inserts only; retractions or deletes would be lost.
283    #[default]
284    AppendOnly,
285    /// Rows are reconciled by a configured key, but the connector does not
286    /// consume a native full-changelog envelope.
287    KeyedUpsert,
288    /// Inserts, updates, and deletes/retractions are represented faithfully.
289    FullChangelog,
290}
291
292impl SinkInputMode {
293    /// Whether this mode can faithfully consume a full Z-set changelog,
294    /// including deletes/retractions.
295    #[must_use]
296    pub const fn accepts_full_changelog(self) -> bool {
297        matches!(self, Self::FullChangelog)
298    }
299}
300
301/// Complete sink admission contract for a concrete connector configuration.
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, serde::Serialize)]
303pub struct SinkContract {
304    /// Durability and checkpoint-commit semantics.
305    pub consistency: SinkConsistency,
306    /// Valid runtime placement model.
307    pub topology: SinkTopology,
308    /// Strongest supported input update model.
309    pub input_mode: SinkInputMode,
310    /// True only for a built-in sink whose immutable phase-one and fenced
311    /// external cursor protocol is certified for multi-node exact delivery.
312    cluster_exact_delivery_certified: bool,
313}
314
315impl SinkContract {
316    /// Construct a sink contract from its three explicit dimensions.
317    #[must_use]
318    pub const fn new(
319        consistency: SinkConsistency,
320        topology: SinkTopology,
321        input_mode: SinkInputMode,
322    ) -> Self {
323        Self {
324            consistency,
325            topology,
326            input_mode,
327            cluster_exact_delivery_certified: false,
328        }
329    }
330
331    /// Mark a built-in sink whose cluster exact-delivery protocol is a release gate.
332    #[must_use]
333    pub(crate) const fn with_cluster_exact_delivery_certification(mut self) -> Self {
334        self.cluster_exact_delivery_certified = true;
335        self
336    }
337
338    /// Whether this sink's complete multi-node exact-delivery protocol is certified.
339    #[doc(hidden)]
340    #[must_use]
341    pub const fn is_cluster_exact_delivery_certified(self) -> bool {
342        self.cluster_exact_delivery_certified
343    }
344
345    /// Whether this contract participates in checkpoint-owned external commit.
346    #[must_use]
347    pub const fn is_checkpoint_committable(self) -> bool {
348        matches!(self.consistency, SinkConsistency::CheckpointCommittable)
349    }
350
351    /// Whether this contract faithfully consumes inserts, updates, and retractions.
352    #[must_use]
353    pub const fn accepts_full_changelog(self) -> bool {
354        self.input_mode.accepts_full_changelog()
355    }
356}