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}