Skip to main content

laminar_db/mv_store/committed/
mod.rs

1//! Reads of materialized views from committed checkpoints. A read resolves one committed cut,
2//! pins it against retention, and reads each view from the artifacts that cut lists across all
3//! of its participants: changelog views from their vnode frames, append views from their log
4//! segments.
5
6mod artifacts;
7mod cache;
8#[cfg(feature = "cluster")]
9mod changes;
10mod pruning;
11mod recency;
12#[cfg(test)]
13mod tests;
14
15use std::collections::BTreeMap;
16use std::sync::Arc;
17use std::time::Instant;
18
19use arrow::datatypes::SchemaRef;
20use laminar_core::checkpoint::{
21    CheckpointManifest, CheckpointScope, CheckpointStore, CommittedCheckpointRef, ViewLog,
22};
23use laminar_core::checkpoint_decision::CheckpointDecisionStore;
24
25use super::admission::MvLimits;
26use super::MvStorageMode;
27use crate::checkpoint_coordinator::committed_reads::{
28    CommittedCut, CommittedCuts, PinAcquire, ReadPin, ReadPins,
29};
30use crate::error::DbError;
31use cache::ArtifactCache;
32pub(crate) use recency::RecencyTargets;
33
34/// Attempts at pinning the newest cut while retention retires the ones a read resolved.
35const PIN_ATTEMPTS: u32 = 4;
36/// Committed indexes an exact read walks back through from the newest cut.
37const MAX_EXACT_WALK: usize = 64;
38
39/// Which committed cut a read observes.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum CommittedRead {
42    /// The newest committed cut.
43    Latest,
44    /// The newest committed cut at or after this epoch. The read waits for it until its
45    /// deadline.
46    AtLeast(u64),
47    /// Exactly this epoch, while retention keeps it.
48    Exact(u64),
49    /// The newest committed cut, at or after `at_least`, that holds every record each source
50    /// behind the read's views could see upstream when the read arrived; it may hold later
51    /// records too. The read waits for it until its deadline.
52    Recent {
53        /// The oldest epoch the read accepts; `0` for none.
54        at_least: u64,
55    },
56}
57
58/// Why a committed read could not be served.
59#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
60pub enum CommittedReadError {
61    /// The pipeline has no checkpoint store to read from.
62    #[error("committed reads need checkpointing")]
63    CheckpointingDisabled,
64    /// No committed cut satisfies the request before the read's deadline.
65    #[error("{}", not_committed(*requested, *latest))]
66    NotCommitted {
67        /// The epoch the read asked for; `None` for the newest cut.
68        requested: Option<u64>,
69        /// The newest committed epoch, if any.
70        latest: Option<u64>,
71    },
72    /// Retention has retired the requested epoch, or it never committed.
73    #[error("epoch {requested} is not retained")]
74    Retired {
75        /// The epoch the read asked for.
76        requested: u64,
77    },
78    /// Every pin slot is held by other reads.
79    #[error("too many committed reads hold checkpoints; retry")]
80    PinCapacity,
81    /// The read did not finish loading before its deadline.
82    #[error("the read of epoch {epoch} passed its deadline")]
83    DeadlineExceeded {
84        /// The epoch being read.
85        epoch: u64,
86    },
87    /// The view was created after the cut, so the cut holds none of its state.
88    #[error("view '{view}' has no committed state at epoch {epoch}")]
89    ViewNotCommitted {
90        /// The view.
91        view: String,
92        /// The epoch being read.
93        epoch: u64,
94    },
95    /// The statement scans something other than a materialized view.
96    #[error("committed reads scan materialized views only; '{table}' is live state")]
97    LiveTable {
98        /// The scanned table.
99        table: String,
100    },
101    /// An input behind the read's views cannot report how far its upstream has been written.
102    #[error(
103        "'{input}' cannot report how far its upstream has been written; read it without recency"
104    )]
105    RecencyUnsupported {
106        /// The source, or the input that is not a source.
107        input: String,
108    },
109    /// A source could not report how far its upstream has been written, or whether a cut holds
110    /// it.
111    #[error("source '{input}' could not report its upstream position: {reason}")]
112    RecencyProbe {
113        /// The source.
114        input: String,
115        /// What failed.
116        reason: String,
117    },
118    /// No committed cut held every source's upstream records before the read's deadline.
119    #[error("{}", not_recent(*latest, uncovered))]
120    NotRecent {
121        /// The newest committed epoch, if any.
122        latest: Option<u64>,
123        /// The sources no cut checked before the deadline held.
124        uncovered: Vec<String>,
125    },
126}
127
128fn not_committed(requested: Option<u64>, latest: Option<u64>) -> String {
129    let latest = latest.map_or_else(|| "none".to_owned(), |epoch| epoch.to_string());
130    match requested {
131        Some(epoch) => format!("epoch {epoch} has not committed; the newest is {latest}"),
132        None => "no checkpoint has committed yet".to_owned(),
133    }
134}
135
136fn not_recent(latest: Option<u64>, uncovered: &[String]) -> String {
137    let latest = latest.map_or_else(|| "none".to_owned(), |epoch| epoch.to_string());
138    format!(
139        "no committed cut held what sources [{}] had upstream when the read arrived; the newest \
140         is {latest}",
141        uncovered.join(", ")
142    )
143}
144
145impl From<CommittedReadError> for DbError {
146    fn from(error: CommittedReadError) -> Self {
147        Self::CommittedRead(error)
148    }
149}
150
151/// What a committed read needs of one view, captured under the store read guard.
152#[derive(Clone)]
153pub(crate) struct ViewShape {
154    pub(super) name: String,
155    pub(super) schema: SchemaRef,
156    pub(super) mode: MvStorageMode,
157    pub(super) limits: MvLimits,
158}
159
160impl ViewShape {
161    #[cfg(feature = "cluster")]
162    pub(crate) fn name(&self) -> &str {
163        &self.name
164    }
165}
166
167impl super::MvStore {
168    /// What a committed read of `name` needs, while the view is registered.
169    pub(crate) fn view_shape(&self, name: &str) -> Option<ViewShape> {
170        let entry = self.entries.get(name)?;
171        Some(ViewShape {
172            name: name.to_owned(),
173            schema: Arc::clone(&entry.schema),
174            mode: entry.mode.clone(),
175            limits: self.limits,
176        })
177    }
178}
179
180/// Reads views from committed checkpoints of this pipeline.
181pub(crate) struct CommittedViews {
182    store: Arc<dyn CheckpointStore>,
183    decisions: Arc<CheckpointDecisionStore>,
184    cuts: CommittedCuts,
185    pins: Arc<ReadPins>,
186    /// Manifests of the cut read last, by checkpoint ID.
187    manifests: parking_lot::Mutex<Option<(u64, Arc<[CheckpointManifest]>)>>,
188    artifacts: ArtifactCache,
189}
190
191/// One committed cut, held against retention until the read finishes or its deadline passes.
192pub(crate) struct PinnedCut {
193    pub(crate) epoch: u64,
194    pub(crate) checkpoint_id: u64,
195    #[cfg(feature = "cluster")]
196    reference: CommittedCheckpointRef,
197    /// Earliest participant capture time of the cut, in Unix milliseconds.
198    pub(crate) taken_at_ms: u64,
199    manifests: Arc<[CheckpointManifest]>,
200    /// The frontier each append view's retention follows at this cut.
201    log_frontiers: LogFrontiers,
202    deadline: Instant,
203    _pin: ReadPin,
204}
205
206/// Where a cut records the frontier an append view's retention follows.
207enum LogFrontiers {
208    /// A local cut: the frontier its one participant logged.
209    Logged,
210    /// A cluster cut: the committed frontier its index carries for every participant.
211    Committed(BTreeMap<String, i64>),
212}
213
214impl PinnedCut {
215    /// Hold this cut against retention until `deadline` instead.
216    #[cfg(feature = "cluster")]
217    #[allow(clippy::used_underscore_binding)] // the pin is held, never read
218    pub(crate) fn renew(
219        &mut self,
220        views: &CommittedViews,
221        deadline: Instant,
222    ) -> Result<(), DbError> {
223        match views.pins.acquire(&self.reference, deadline) {
224            PinAcquire::Acquired(pin) => {
225                self._pin = pin;
226                self.deadline = deadline;
227                Ok(())
228            }
229            PinAcquire::Pruned => Err(CommittedReadError::Retired {
230                requested: self.epoch,
231            }
232            .into()),
233            PinAcquire::Capacity => Err(CommittedReadError::PinCapacity.into()),
234        }
235    }
236
237    /// The frontier `log` of `view` keeps rows against at this cut.
238    pub(crate) fn log_frontier(&self, view: &str, log: &ViewLog) -> Option<i64> {
239        match &self.log_frontiers {
240            LogFrontiers::Logged => log.frontier,
241            LogFrontiers::Committed(frontiers) => frontiers.get(view).copied(),
242        }
243    }
244}
245
246impl CommittedViews {
247    pub(crate) fn new(
248        store: Arc<dyn CheckpointStore>,
249        decisions: Arc<CheckpointDecisionStore>,
250        (cuts, pins): (CommittedCuts, Arc<ReadPins>),
251        cache_bytes: usize,
252    ) -> Self {
253        Self {
254            store,
255            decisions,
256            cuts,
257            pins,
258            manifests: parking_lot::Mutex::new(None),
259            artifacts: ArtifactCache::new(cache_bytes),
260        }
261    }
262
263    pub(crate) fn latest(&self) -> Option<Arc<CommittedCut>> {
264        self.cuts.borrow().clone()
265    }
266
267    /// Resolve `at` to one committed cut and pin it until `deadline`.
268    pub(crate) async fn pin(
269        &self,
270        at: CommittedRead,
271        deadline: Instant,
272    ) -> Result<PinnedCut, DbError> {
273        let wait_for = match at {
274            CommittedRead::Latest => None,
275            CommittedRead::AtLeast(epoch) | CommittedRead::Exact(epoch) => Some(epoch),
276            CommittedRead::Recent { .. } => {
277                return Err(DbError::InvalidOperation(
278                    "a recent read needs the upstream positions of its sources".into(),
279                ))
280            }
281        };
282        let latest = self.await_epoch(wait_for, deadline).await?;
283        if let CommittedRead::Exact(epoch) = at {
284            if latest.index.epoch != epoch {
285                return self.pin_older(&latest, epoch, deadline).await;
286            }
287        }
288        let mut cut = latest;
289        for _ in 0..PIN_ATTEMPTS {
290            match self.pins.acquire(&cut.reference, deadline) {
291                PinAcquire::Acquired(pin) => return self.pinned(&cut, pin, deadline).await,
292                PinAcquire::Capacity => return Err(CommittedReadError::PinCapacity.into()),
293                PinAcquire::Pruned if matches!(at, CommittedRead::Exact(_)) => break,
294                PinAcquire::Pruned => {}
295            }
296            cut = self.latest().ok_or(CommittedReadError::NotCommitted {
297                requested: wait_for,
298                latest: None,
299            })?;
300        }
301        Err(CommittedReadError::Retired {
302            requested: cut.index.epoch,
303        }
304        .into())
305    }
306
307    /// The newest cut, once one at or after `epoch` has committed.
308    async fn await_epoch(
309        &self,
310        epoch: Option<u64>,
311        deadline: Instant,
312    ) -> Result<Arc<CommittedCut>, DbError> {
313        let not_committed = |latest: Option<&Arc<CommittedCut>>| CommittedReadError::NotCommitted {
314            requested: epoch,
315            latest: latest.map(|cut| cut.index.epoch),
316        };
317        let Some(epoch) = epoch else {
318            return self.latest().ok_or_else(|| not_committed(None).into());
319        };
320        let mut cuts = self.cuts.clone();
321        let waited = tokio::time::timeout_at(
322            tokio::time::Instant::from_std(deadline),
323            cuts.wait_for(|cut| cut.as_ref().is_some_and(|cut| cut.index.epoch >= epoch)),
324        )
325        .await;
326        match waited {
327            Ok(Ok(cut)) => cut.clone().ok_or_else(|| not_committed(None).into()),
328            Ok(Err(_)) | Err(_) => Err(not_committed(self.latest().as_ref()).into()),
329        }
330    }
331
332    /// Pin `epoch`, older than the newest cut, by walking back through committed predecessors.
333    async fn pin_older(
334        &self,
335        latest: &CommittedCut,
336        epoch: u64,
337        deadline: Instant,
338    ) -> Result<PinnedCut, DbError> {
339        let retired = || DbError::from(CommittedReadError::Retired { requested: epoch });
340        let mut predecessor = latest.index.predecessor.clone();
341        for _ in 0..MAX_EXACT_WALK {
342            let reference = predecessor.filter(|reference| reference.epoch >= epoch);
343            let reference = reference.ok_or_else(retired)?;
344            if reference.epoch == epoch {
345                return self.pin_reference(&reference, deadline).await;
346            }
347            predecessor = self
348                .load_index(&reference)
349                .await?
350                .ok_or_else(retired)?
351                .predecessor;
352        }
353        Err(retired())
354    }
355
356    /// Pin the committed cut `reference` names.
357    pub(crate) async fn pin_reference(
358        &self,
359        reference: &CommittedCheckpointRef,
360        deadline: Instant,
361    ) -> Result<PinnedCut, DbError> {
362        let retired = || {
363            DbError::from(CommittedReadError::Retired {
364                requested: reference.epoch,
365            })
366        };
367        let pin = match self.pins.acquire(reference, deadline) {
368            PinAcquire::Acquired(pin) => pin,
369            PinAcquire::Pruned => return Err(retired()),
370            PinAcquire::Capacity => return Err(CommittedReadError::PinCapacity.into()),
371        };
372        let index = self.load_index(reference).await?.ok_or_else(retired)?;
373        let cut = CommittedCut::new(index, reference.clone(), std::iter::empty());
374        self.pinned(&cut, pin, deadline).await
375    }
376
377    /// The committed cuts after `cut`, oldest first, found by walking back from the newest.
378    #[cfg(feature = "cluster")]
379    pub(crate) async fn successors(
380        &self,
381        cut: &PinnedCut,
382    ) -> Result<Vec<CommittedCheckpointRef>, DbError> {
383        let lost = || {
384            DbError::from(CommittedReadError::Retired {
385                requested: cut.epoch,
386            })
387        };
388        let Some(latest) = self
389            .latest()
390            .filter(|latest| latest.index.epoch >= cut.epoch)
391        else {
392            return Ok(Vec::new());
393        };
394        let mut successors = Vec::new();
395        let mut reference = latest.reference.clone();
396        let mut predecessor = latest.index.predecessor.clone();
397        while reference != cut.reference {
398            if successors.len() == MAX_EXACT_WALK || reference.epoch <= cut.epoch {
399                return Err(lost());
400            }
401            successors.push(reference);
402            reference = predecessor.take().ok_or_else(lost)?;
403            if reference != cut.reference {
404                predecessor = self
405                    .load_index(&reference)
406                    .await?
407                    .ok_or_else(lost)?
408                    .predecessor;
409            }
410        }
411        successors.reverse();
412        Ok(successors)
413    }
414
415    /// Committed cuts as this node learns of them.
416    #[cfg(feature = "cluster")]
417    pub(crate) fn cuts(&self) -> CommittedCuts {
418        self.cuts.clone()
419    }
420
421    async fn load_index(
422        &self,
423        reference: &CommittedCheckpointRef,
424    ) -> Result<Option<laminar_core::checkpoint::CommittedCheckpointIndex>, DbError> {
425        self.decisions
426            .load_committed_checkpoint_optional(reference)
427            .await
428            .map_err(|error| DbError::Checkpoint(format!("load committed checkpoint: {error}")))
429    }
430
431    /// Pin the cut whose segments a store read guard captured as checkpoint `checkpoint_id`.
432    /// `None` when retention has moved past it; the caller captures the view again.
433    pub(super) async fn pin_checkpoint(
434        &self,
435        checkpoint_id: u64,
436        deadline: Instant,
437    ) -> Result<Option<PinnedCut>, DbError> {
438        let Some(latest) = self.latest() else {
439            return Ok(None);
440        };
441        if latest.index.checkpoint_id == checkpoint_id {
442            return match self.pins.acquire(&latest.reference, deadline) {
443                PinAcquire::Acquired(pin) => self.pinned(&latest, pin, deadline).await.map(Some),
444                PinAcquire::Pruned => Ok(None),
445                PinAcquire::Capacity => Err(CommittedReadError::PinCapacity.into()),
446            };
447        }
448        let Some(reference) = latest
449            .index
450            .predecessor
451            .clone()
452            .filter(|reference| reference.checkpoint_id == checkpoint_id)
453        else {
454            return Ok(None);
455        };
456        let pin = match self.pins.acquire(&reference, deadline) {
457            PinAcquire::Acquired(pin) => pin,
458            PinAcquire::Pruned => return Ok(None),
459            PinAcquire::Capacity => return Err(CommittedReadError::PinCapacity.into()),
460        };
461        let Some(index) = self.load_index(&reference).await? else {
462            return Ok(None);
463        };
464        let cut = CommittedCut::new(index, reference, std::iter::empty());
465        self.pinned(&cut, pin, deadline).await.map(Some)
466    }
467
468    async fn pinned(
469        &self,
470        cut: &CommittedCut,
471        pin: ReadPin,
472        deadline: Instant,
473    ) -> Result<PinnedCut, DbError> {
474        let manifests = tokio::time::timeout_at(
475            tokio::time::Instant::from_std(deadline),
476            self.manifests(cut),
477        )
478        .await
479        .map_err(|_| CommittedReadError::DeadlineExceeded {
480            epoch: cut.index.epoch,
481        })??;
482        let taken_at_ms = manifests
483            .iter()
484            .map(|manifest| manifest.timestamp_ms)
485            .min()
486            .unwrap_or(cut.taken_at_ms);
487        let log_frontiers = match cut.index.scope {
488            CheckpointScope::Local => LogFrontiers::Logged,
489            CheckpointScope::Cluster => LogFrontiers::Committed(cut.index.view_frontiers.clone()),
490        };
491        Ok(PinnedCut {
492            epoch: cut.index.epoch,
493            checkpoint_id: cut.index.checkpoint_id,
494            #[cfg(feature = "cluster")]
495            reference: cut.reference.clone(),
496            taken_at_ms,
497            manifests,
498            log_frontiers,
499            deadline,
500            _pin: pin,
501        })
502    }
503
504    /// Every participant manifest of the newest committed cut, if one has committed.
505    #[cfg(feature = "cluster")]
506    pub(crate) async fn latest_manifests(
507        &self,
508    ) -> Result<Option<Arc<[CheckpointManifest]>>, DbError> {
509        match self.latest() {
510            Some(cut) => self.manifests(&cut).await.map(Some),
511            None => Ok(None),
512        }
513    }
514
515    /// Every participant manifest of `cut`, verified against its committed index.
516    async fn manifests(&self, cut: &CommittedCut) -> Result<Arc<[CheckpointManifest]>, DbError> {
517        let checkpoint_id = cut.index.checkpoint_id;
518        if let Some((_, manifests)) = self
519            .manifests
520            .lock()
521            .as_ref()
522            .filter(|(cached, _)| *cached == checkpoint_id)
523        {
524            return Ok(Arc::clone(manifests));
525        }
526        let manifests: Arc<[CheckpointManifest]> =
527            crate::checkpoint_coordinator::load_index_manifests(self.store.as_ref(), &cut.index)
528                .await?
529                .into();
530        *self.manifests.lock() = Some((checkpoint_id, Arc::clone(&manifests)));
531        Ok(manifests)
532    }
533}