Skip to main content

laminar_core/checkpoint/barrier/
mod.rs

1//! Checkpoint barrier protocol.
2//!
3//! The coordinator injects barriers into sources via [`CheckpointBarrierInjector`].
4//! Sources deliver barriers alongside events. The fast path (no pending barrier)
5//! is a single `AtomicU64` load (~10ns).
6
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::sync::Arc;
9
10/// Barrier flags — packed into the `flags` field.
11pub mod flags {
12    /// No special behavior.
13    pub const NONE: u64 = 0;
14    /// This barrier requires a full snapshot (not incremental).
15    pub const FULL_SNAPSHOT: u64 = 1 << 0;
16    /// This barrier is the final barrier before shutdown.
17    pub const DRAIN: u64 = 1 << 1;
18    /// Cancel any in-progress checkpoint with this ID.
19    pub const CANCEL: u64 = 1 << 2;
20    /// This checkpoint participates in an assignment handoff.
21    pub const HANDOFF: u64 = 1 << 3;
22    /// Exact old-topology cut for a reserved logical migration. Intake and sink succession stay held.
23    pub const TOPOLOGY_CUT: u64 = 1 << 4;
24}
25
26/// Internal flag layout used only by the clustered shuffle fixed-point flush.
27///
28/// These bits never appear in a source barrier, checkpoint request, or control-plane
29/// announcement. Keeping the tag separate from the wave ordinal lets a receiver reject an
30/// untagged or partially encoded marker instead of accidentally treating it as wave zero.
31const SHUFFLE_FLUSH_TAG: u64 = 1 << 31;
32const SHUFFLE_FLUSH_WAVE_SHIFT: u32 = 32;
33const SHUFFLE_FLUSH_WAVE_MASK: u64 = 0x7fff_ffff << SHUFFLE_FLUSH_WAVE_SHIFT;
34const SHUFFLE_FLUSH_ACTIVITY: u64 = 1 << 63;
35const SHUFFLE_FLUSH_INTERNAL_MASK: u64 =
36    SHUFFLE_FLUSH_TAG | SHUFFLE_FLUSH_WAVE_MASK | SHUFFLE_FLUSH_ACTIVITY;
37
38/// Largest shuffle flush wave representable in a checkpoint barrier.
39pub const MAX_SHUFFLE_FLUSH_WAVE: u64 = 0x7fff_ffff;
40
41/// Encode one internal clustered-shuffle flush marker.
42///
43/// The returned value is valid only in the `flags` field of an in-band shuffle barrier. It must
44/// not be copied into checkpoint metadata or cluster control traffic.
45///
46/// # Errors
47/// Returns an error instead of truncating a wave ordinal that does not fit the reserved field.
48pub fn encode_shuffle_flush_flags(wave: u64, activity: bool) -> Result<u64, &'static str> {
49    if wave > MAX_SHUFFLE_FLUSH_WAVE {
50        return Err("shuffle flush wave exceeds its reserved checkpoint-barrier field");
51    }
52    Ok(SHUFFLE_FLUSH_TAG
53        | (wave << SHUFFLE_FLUSH_WAVE_SHIFT)
54        | if activity { SHUFFLE_FLUSH_ACTIVITY } else { 0 })
55}
56
57/// Decode one internal clustered-shuffle flush marker into `(wave, sender_activity)`.
58///
59/// # Errors
60/// Rejects an untagged marker and every bit outside the internal layout. This is deliberately
61/// stricter than the general-purpose checkpoint barrier flags because shuffle-wave metadata must
62/// never be confused with source/control-plane behavior flags.
63pub fn decode_shuffle_flush_flags(flags: u64) -> Result<(u64, bool), &'static str> {
64    if flags & SHUFFLE_FLUSH_TAG == 0 {
65        return Err("shuffle flush marker is missing its internal tag");
66    }
67    if flags & !SHUFFLE_FLUSH_INTERNAL_MASK != 0 {
68        return Err("shuffle flush marker uses reserved or checkpoint behavior flags");
69    }
70    Ok((
71        (flags & SHUFFLE_FLUSH_WAVE_MASK) >> SHUFFLE_FLUSH_WAVE_SHIFT,
72        flags & SHUFFLE_FLUSH_ACTIVITY != 0,
73    ))
74}
75
76/// A checkpoint barrier that flows through the dataflow graph.
77///
78/// This is a 24-byte `#[repr(C)]` value type that can be cheaply copied
79/// and embedded in channel messages. It carries the checkpoint identity
80/// and behavior flags.
81///
82/// ## Layout (24 bytes)
83///
84/// | Field          | Offset | Size | Description                |
85/// |----------------|--------|------|----------------------------|
86/// | checkpoint_id  | 0      | 8    | Unique checkpoint ID       |
87/// | epoch          | 8      | 8    | Monotonic epoch number     |
88/// | flags          | 16     | 8    | Behavior flags (see [`flags`]) |
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90#[repr(C)]
91pub struct CheckpointBarrier {
92    /// Unique identifier for this checkpoint.
93    pub checkpoint_id: u64,
94    /// Monotonically increasing epoch number.
95    pub epoch: u64,
96    /// Behavior flags (see [`flags`] module constants).
97    pub flags: u64,
98}
99
100// Verify the struct is exactly 24 bytes as promised.
101const _: () = assert!(std::mem::size_of::<CheckpointBarrier>() == 24);
102
103impl CheckpointBarrier {
104    /// Create a new barrier with the given checkpoint ID and epoch.
105    #[must_use]
106    pub const fn new(checkpoint_id: u64, epoch: u64) -> Self {
107        Self {
108            checkpoint_id,
109            epoch,
110            flags: flags::NONE,
111        }
112    }
113
114    /// Create a barrier that requests a full snapshot.
115    #[must_use]
116    pub const fn full_snapshot(checkpoint_id: u64, epoch: u64) -> Self {
117        Self {
118            checkpoint_id,
119            epoch,
120            flags: flags::FULL_SNAPSHOT,
121        }
122    }
123
124    /// Whether the duplicated wire fields represent one nonzero durable checkpoint identity.
125    #[must_use]
126    pub const fn is_canonical(self) -> bool {
127        self.checkpoint_id != 0 && self.epoch == self.checkpoint_id
128    }
129
130    /// Check whether this barrier requests a full (non-incremental) snapshot.
131    #[must_use]
132    pub const fn is_full_snapshot(&self) -> bool {
133        self.flags & flags::FULL_SNAPSHOT != 0
134    }
135
136    /// Check whether this barrier signals drain/shutdown.
137    #[must_use]
138    pub const fn is_drain(&self) -> bool {
139        self.flags & flags::DRAIN != 0
140    }
141
142    /// Check whether this barrier cancels an in-progress checkpoint.
143    #[must_use]
144    pub const fn is_cancel(&self) -> bool {
145        self.flags & flags::CANCEL != 0
146    }
147}
148
149/// A message that flows through streaming channels.
150///
151/// Wraps user events with control messages (watermarks and barriers)
152/// in a single enum. Operators pattern-match on this to handle
153/// data vs. control flow.
154///
155/// ## Generic Parameter
156///
157/// `T` is the event payload type — typically `RecordBatch` or a
158/// domain-specific event struct.
159#[derive(Debug, Clone, PartialEq)]
160pub enum StreamMessage<T> {
161    /// A user data event.
162    Event(T),
163    /// A watermark indicating event-time progress (millis since epoch).
164    Watermark(i64),
165    /// A checkpoint barrier for consistent snapshots.
166    Barrier(CheckpointBarrier),
167}
168
169impl<T> StreamMessage<T> {
170    /// Returns `true` if this is a barrier message.
171    #[must_use]
172    pub const fn is_barrier(&self) -> bool {
173        matches!(self, Self::Barrier(_))
174    }
175
176    /// Returns `true` if this is a watermark message.
177    #[must_use]
178    pub const fn is_watermark(&self) -> bool {
179        matches!(self, Self::Watermark(_))
180    }
181
182    /// Returns `true` if this is a data event.
183    #[must_use]
184    pub const fn is_event(&self) -> bool {
185        matches!(self, Self::Event(_))
186    }
187
188    /// Extracts the barrier if this is a [`StreamMessage::Barrier`].
189    #[must_use]
190    pub const fn as_barrier(&self) -> Option<&CheckpointBarrier> {
191        match self {
192            Self::Barrier(b) => Some(b),
193            _ => None,
194        }
195    }
196}
197
198/// Cross-thread barrier injector for source operators.
199///
200/// The coordinator thread publishes a barrier command via
201/// [`trigger`](Self::trigger). Source operators poll via
202/// [`BarrierPollHandle::poll`] on each iteration of their event loop.
203///
204/// ## Fast Path
205///
206/// The poll path is a single `AtomicU64::load(Relaxed)` — typically < 10ns.
207/// Only when a barrier is pending does the source perform a compare-exchange
208/// to claim it.
209#[derive(Debug)]
210pub struct CheckpointBarrierInjector {
211    /// Command lifecycle. Payload fields are published before `PENDING`.
212    state: Arc<AtomicU64>,
213    /// Full-width checkpoint ID for the pending command.
214    checkpoint_id: Arc<AtomicU64>,
215    /// Full-width epoch for the pending command.
216    epoch: Arc<AtomicU64>,
217    /// Full-width flags for the pending command.
218    flags: Arc<AtomicU64>,
219}
220
221const STATE_IDLE: u64 = 0;
222const STATE_WRITING: u64 = 1;
223const STATE_PENDING: u64 = 2;
224const STATE_CONSUMING: u64 = 3;
225
226impl CheckpointBarrierInjector {
227    /// Create a new injector with no pending barrier.
228    #[must_use]
229    pub fn new() -> Self {
230        Self {
231            state: Arc::new(AtomicU64::new(STATE_IDLE)),
232            checkpoint_id: Arc::new(AtomicU64::new(0)),
233            epoch: Arc::new(AtomicU64::new(0)),
234            flags: Arc::new(AtomicU64::new(0)),
235        }
236    }
237
238    /// Get a handle that source operators use to poll for barriers.
239    #[must_use]
240    pub fn handle(&self) -> BarrierPollHandle {
241        BarrierPollHandle {
242            state: Arc::clone(&self.state),
243            checkpoint_id: Arc::clone(&self.checkpoint_id),
244            epoch: Arc::clone(&self.epoch),
245            flags: Arc::clone(&self.flags),
246        }
247    }
248
249    /// Whether no barrier command is currently being published, pending, or consumed.
250    /// The coordinator uses this as an all-source preflight before fan-out.
251    #[must_use]
252    pub fn can_trigger(&self) -> bool {
253        self.state.load(Ordering::Acquire) == STATE_IDLE
254    }
255
256    /// Trigger a new checkpoint barrier.
257    ///
258    /// The next [`BarrierPollHandle::poll`] call on any source will
259    /// observe this barrier and return it. Returns `false` without
260    /// modifying the pending command if the identity is zero or noncanonical, or
261    /// if another trigger is being published, is pending, or is being consumed.
262    ///
263    /// # Arguments
264    ///
265    /// * `barrier` - Exact barrier command to publish. `checkpoint_id` and `epoch`
266    ///   must name the same nonzero durable checkpoint.
267    #[must_use = "a rejected trigger leaves the existing barrier pending"]
268    pub fn trigger(&self, barrier: CheckpointBarrier) -> bool {
269        if !barrier.is_canonical()
270            || self
271                .state
272                .compare_exchange(
273                    STATE_IDLE,
274                    STATE_WRITING,
275                    Ordering::Acquire,
276                    Ordering::Relaxed,
277                )
278                .is_err()
279        {
280            return false;
281        }
282
283        // Writers own the payload while state is WRITING. A release-publish of
284        // PENDING makes every full-width field visible to the claiming poller.
285        self.checkpoint_id
286            .store(barrier.checkpoint_id, Ordering::Relaxed);
287        self.epoch.store(barrier.epoch, Ordering::Relaxed);
288        self.flags.store(barrier.flags, Ordering::Relaxed);
289        self.state.store(STATE_PENDING, Ordering::Release);
290        true
291    }
292
293    /// Cancel the pending barrier with this exact checkpoint identity.
294    ///
295    /// Returns `true` only when this call claims and cancels a pending command
296    /// whose checkpoint ID and epoch both match. A stale identity, an idle
297    /// injector, or a barrier already claimed by a poller returns `false`.
298    /// Mismatched pending commands remain available to pollers unchanged.
299    #[must_use = "a false result means the exact pending barrier was not cancelled"]
300    pub fn cancel_exact(&self, barrier: CheckpointBarrier) -> bool {
301        if !barrier.is_canonical()
302            || self.state.load(Ordering::Acquire) != STATE_PENDING
303            || self.checkpoint_id.load(Ordering::Relaxed) != barrier.checkpoint_id
304            || self.epoch.load(Ordering::Relaxed) != barrier.epoch
305        {
306            return false;
307        }
308
309        if self
310            .state
311            .compare_exchange(
312                STATE_PENDING,
313                STATE_CONSUMING,
314                Ordering::Acquire,
315                Ordering::Relaxed,
316            )
317            .is_err()
318        {
319            return false;
320        }
321
322        // Revalidate under exclusive ownership: a poll and a subsequent
323        // trigger may have changed the payload after the optimistic check.
324        let matches = self.checkpoint_id.load(Ordering::Relaxed) == barrier.checkpoint_id
325            && self.epoch.load(Ordering::Relaxed) == barrier.epoch;
326        self.state.store(
327            if matches { STATE_IDLE } else { STATE_PENDING },
328            Ordering::Release,
329        );
330        matches
331    }
332}
333
334impl Default for CheckpointBarrierInjector {
335    fn default() -> Self {
336        Self::new()
337    }
338}
339
340impl Clone for CheckpointBarrierInjector {
341    fn clone(&self) -> Self {
342        Self {
343            state: Arc::clone(&self.state),
344            checkpoint_id: Arc::clone(&self.checkpoint_id),
345            epoch: Arc::clone(&self.epoch),
346            flags: Arc::clone(&self.flags),
347        }
348    }
349}
350
351/// Handle used by source operators to poll for pending barriers.
352///
353/// Cloned from [`CheckpointBarrierInjector::handle`] and stored in the
354/// source operator. The fast path is a single atomic load.
355#[derive(Debug, Clone)]
356pub struct BarrierPollHandle {
357    /// Shared command lifecycle.
358    state: Arc<AtomicU64>,
359    /// Full-width checkpoint ID payload.
360    checkpoint_id: Arc<AtomicU64>,
361    /// Full-width epoch payload.
362    epoch: Arc<AtomicU64>,
363    /// Full-width flags payload.
364    flags: Arc<AtomicU64>,
365}
366
367impl BarrierPollHandle {
368    /// Poll for a pending barrier.
369    ///
370    /// Returns `Some(CheckpointBarrier)` if a barrier is pending and
371    /// this call successfully claimed it (exactly-once delivery across
372    /// handles sharing the same injector). Returns `None` if no barrier
373    /// is pending or another handle already claimed it.
374    ///
375    /// ## Performance
376    ///
377    /// Fast path (no barrier): single `load(Relaxed)` — < 10ns.
378    /// Slow path (barrier pending): one `compare_exchange`.
379    #[must_use]
380    pub fn poll(&self) -> Option<CheckpointBarrier> {
381        // Fast path: no barrier pending
382        if self.state.load(Ordering::Relaxed) != STATE_PENDING {
383            return None;
384        }
385
386        // Claim before reading. CONSUMING prevents the next trigger from
387        // overwriting payload fields until this poller has copied both.
388        if self
389            .state
390            .compare_exchange(
391                STATE_PENDING,
392                STATE_CONSUMING,
393                Ordering::Acquire,
394                Ordering::Relaxed,
395            )
396            .is_ok()
397        {
398            let barrier = CheckpointBarrier {
399                checkpoint_id: self.checkpoint_id.load(Ordering::Relaxed),
400                epoch: self.epoch.load(Ordering::Relaxed),
401                flags: self.flags.load(Ordering::Relaxed),
402            };
403            self.state.store(STATE_IDLE, Ordering::Release);
404            Some(barrier)
405        } else {
406            // Another thread claimed it first
407            None
408        }
409    }
410}
411
412#[cfg(test)]
413mod tests;