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