1mod 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
34const PIN_ATTEMPTS: u32 = 4;
36const MAX_EXACT_WALK: usize = 64;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum CommittedRead {
42 Latest,
44 AtLeast(u64),
47 Exact(u64),
49 Recent {
53 at_least: u64,
55 },
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
60pub enum CommittedReadError {
61 #[error("committed reads need checkpointing")]
63 CheckpointingDisabled,
64 #[error("{}", not_committed(*requested, *latest))]
66 NotCommitted {
67 requested: Option<u64>,
69 latest: Option<u64>,
71 },
72 #[error("epoch {requested} is not retained")]
74 Retired {
75 requested: u64,
77 },
78 #[error("too many committed reads hold checkpoints; retry")]
80 PinCapacity,
81 #[error("the read of epoch {epoch} passed its deadline")]
83 DeadlineExceeded {
84 epoch: u64,
86 },
87 #[error("view '{view}' has no committed state at epoch {epoch}")]
89 ViewNotCommitted {
90 view: String,
92 epoch: u64,
94 },
95 #[error("committed reads scan materialized views only; '{table}' is live state")]
97 LiveTable {
98 table: String,
100 },
101 #[error(
103 "'{input}' cannot report how far its upstream has been written; read it without recency"
104 )]
105 RecencyUnsupported {
106 input: String,
108 },
109 #[error("source '{input}' could not report its upstream position: {reason}")]
112 RecencyProbe {
113 input: String,
115 reason: String,
117 },
118 #[error("{}", not_recent(*latest, uncovered))]
120 NotRecent {
121 latest: Option<u64>,
123 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#[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 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
180pub(crate) struct CommittedViews {
182 store: Arc<dyn CheckpointStore>,
183 decisions: Arc<CheckpointDecisionStore>,
184 cuts: CommittedCuts,
185 pins: Arc<ReadPins>,
186 manifests: parking_lot::Mutex<Option<(u64, Arc<[CheckpointManifest]>)>>,
188 artifacts: ArtifactCache,
189}
190
191pub(crate) struct PinnedCut {
193 pub(crate) epoch: u64,
194 pub(crate) checkpoint_id: u64,
195 #[cfg(feature = "cluster")]
196 reference: CommittedCheckpointRef,
197 pub(crate) taken_at_ms: u64,
199 manifests: Arc<[CheckpointManifest]>,
200 log_frontiers: LogFrontiers,
202 deadline: Instant,
203 _pin: ReadPin,
204}
205
206enum LogFrontiers {
208 Logged,
210 Committed(BTreeMap<String, i64>),
212}
213
214impl PinnedCut {
215 #[cfg(feature = "cluster")]
217 #[allow(clippy::used_underscore_binding)] 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 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 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 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 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 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 #[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 #[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 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 #[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 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}