mod backfill;
mod index_build;
mod index_lookup;
mod inline_scan;
mod maintenance;
mod row_location;
mod table_warm;
#[cfg(test)]
mod tests;
use std::{
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
time::{Duration, Instant},
};
pub use maintenance::{
MaintenanceReport, MaintenanceRequest, MaintenanceStatusPass, MaintenanceStatusStep,
};
use object_store::ObjectStore;
pub use row_location::{
DeleteFileRegistration, ExistingDeleteFile, LocatedDeletion, LocatedPositions, RowSummaryWarmth,
};
use slatedb::{CloseReason, Db, DbReader, DbStatus, DbTransaction, IsolationLevel};
use tokio::sync::watch;
use tracing::{info, warn};
use crate::{
catalog::{
CatalogSnapshot, RecentRow, SnapshotId, TableId,
projection::{
ProjectionCache, cache_epoch, cached_head_view, held_head_view, install_head_view_at,
install_shared_current_entities, shared_current_entities,
},
},
data_file,
error::{Error, Result},
store::{
cache::{
CacheCounters, CacheTally, MemoryTally, ObjectStoreTally, cache_status,
metadata_shortfall,
},
census,
handle::{ReadHandle, ReadSession},
open::{self, StoreBuilder},
proto::HeadValue,
},
transaction::{MigrationReport, Transaction, commit, migration},
};
pub(crate) const WARM_TABLE_CONCURRENCY: usize = 32;
pub(crate) const SUMMARY_READ_CONCURRENCY: usize = 64;
pub(crate) const BACKFILL_FILE_READ_CONCURRENCY: usize = 8;
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct MigrationRequest {
pub checkpoint: bool,
}
#[derive(Debug, Default)]
struct ReadTally {
materializations: AtomicU64,
refreshes: AtomicU64,
cache_hits: AtomicU64,
head_reads: AtomicU64,
materialize_micros: AtomicU64,
current_scans: AtomicU64,
history_scans: AtomicU64,
}
impl ReadTally {
fn materialized(&self, elapsed: Duration) {
let count = self.materializations.fetch_add(1, Ordering::Relaxed) + 1;
let micros = u64::try_from(elapsed.as_micros()).unwrap_or(u64::MAX);
let total = self.materialize_micros.fetch_add(micros, Ordering::Relaxed) + micros;
info!(
elapsed_ms = crate::telemetry::milliseconds(elapsed),
materializations = count,
total_ms = crate::telemetry::milliseconds(Duration::from_micros(total)),
cache_hits = self.cache_hits.load(Ordering::Relaxed),
refreshes = self.refreshes.load(Ordering::Relaxed),
head_reads = self.head_reads.load(Ordering::Relaxed),
"materialized the catalog from `current`"
);
}
}
struct StoreLocation {
path: String,
object_store: Arc<dyn ObjectStore>,
}
pub(crate) enum Store {
Writer { db: Db, flush_on_commit: bool },
Reader(Arc<DbReader>),
}
impl Store {
pub(crate) fn writer_db(&self) -> Option<&Db> {
match self {
Self::Writer { db, .. } => Some(db),
Self::Reader(_) => None,
}
}
pub(crate) fn commit_durability(&self) -> commit::CommitDurability {
match self {
Self::Writer {
db,
flush_on_commit,
} => commit::writer_durability(db, *flush_on_commit),
Self::Reader(_) => commit::CommitDurability::OnFlushInterval,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CatalogOptions {
pub path: String,
pub encrypted: bool,
pub flush_interval: Duration,
pub flush_on_commit: bool,
pub cache_dir: Option<std::path::PathBuf>,
pub cache_size: Option<u64>,
pub cache_memory: Option<u64>,
pub cache_preload: Option<CachePreload>,
pub cache_puts: bool,
pub data_path: Option<String>,
pub reader_poll_interval: Duration,
pub checkpoint: Option<String>,
}
impl Default for CatalogOptions {
fn default() -> Self {
Self {
path: String::new(),
encrypted: false,
flush_interval: Duration::from_millis(100),
flush_on_commit: false,
cache_dir: None,
cache_size: None,
cache_memory: None,
cache_preload: None,
cache_puts: false,
data_path: None,
reader_poll_interval: Duration::from_secs(10),
checkpoint: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CachePreload {
L0,
All,
}
fn warn_if_preload_cannot_fit(options: &CatalogOptions, manifest: Option<census::ManifestBytes>) {
if options.cache_preload != Some(CachePreload::All) || options.cache_dir.is_none() {
return;
}
let Some(store_bytes) = manifest.map(|manifest| manifest.store_bytes) else {
return;
};
if let Some(shortfall) = open::preload_shortfall(store_bytes, options.cache_size) {
warn!(
path = options.path,
store_bytes,
cache_size = options.cache_size,
shortfall,
"preload cannot hold this store: the load stops once the cache is full, leaving \
the rest to be fetched on demand"
);
}
}
fn warn_if_metadata_cache_cannot_hold(path: &str, metadata_bytes: Option<u64>) {
let Some(metadata_bytes) = metadata_bytes else {
return;
};
let capacity = cache_status().metadata_capacity_bytes;
if let Some(shortfall) = metadata_shortfall(metadata_bytes, capacity) {
warn!(
path,
metadata_bytes,
metadata_capacity_bytes = capacity,
shortfall,
"the metadata cache cannot hold this store's SST filters and indexes, so probes \
will fetch them from object storage. Raise the cache memory budget on the first \
attach in the process."
);
}
}
fn parse_checkpoint(checkpoint: Option<&str>) -> Result<Option<uuid::Uuid>> {
checkpoint
.map(|id| {
uuid::Uuid::parse_str(id).map_err(|err| {
Error::Configuration(format!("checkpoint `{id}` is not a valid id: {err}"))
})
})
.transpose()
}
pub type CommitMember<'a> = &'a (dyn Fn(&mut Transaction) -> Result<()> + Sync);
#[derive(Clone)]
pub struct ReadOnlyCatalog {
store: Arc<Store>,
reads: Arc<ReadTally>,
cache: Arc<CacheCounters>,
data_reads: Arc<data_file::DataStoreCounters>,
location: Arc<StoreLocation>,
projections: Arc<std::sync::RwLock<ProjectionCache>>,
commits: Arc<commit::Coalescer>,
writer_status: Option<watch::Receiver<DbStatus>>,
}
impl std::fmt::Debug for ReadOnlyCatalog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ReadOnlyCatalog").finish_non_exhaustive()
}
}
#[derive(Clone)]
pub struct Catalog {
inner: ReadOnlyCatalog,
}
impl std::fmt::Debug for Catalog {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Catalog").finish_non_exhaustive()
}
}
impl std::ops::Deref for Catalog {
type Target = ReadOnlyCatalog;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl ReadOnlyCatalog {
pub(crate) fn projections(&self) -> &Arc<std::sync::RwLock<ProjectionCache>> {
&self.projections
}
#[must_use]
pub fn cache_tally(&self) -> CacheTally {
self.cache.tally()
}
#[must_use]
pub fn projection_bytes(&self) -> u64 {
self.projections
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.estimated_bytes()
}
#[must_use]
pub fn memory_tally(&self) -> MemoryTally {
let status = cache_status();
let (last_commit_index_entries, last_commit_staged_bytes) = self.cache.last_commit_memory();
MemoryTally {
slatedb_unflushed_bytes: self.cache.slatedb_unflushed_bytes(),
projection_bytes: self.projection_bytes(),
cache_metadata_bytes: status.metadata_occupancy_bytes,
cache_block_bytes: status.block_occupancy_bytes,
auxiliary_metadata_bytes: status.auxiliary_metadata_occupancy_bytes,
last_commit_index_entries,
last_commit_staged_bytes,
}
}
pub(crate) fn cache_counters(&self) -> Arc<CacheCounters> {
Arc::clone(&self.cache)
}
#[must_use]
pub fn object_store_tally(&self) -> ObjectStoreTally {
let mut tally = self.cache.object_store_tally();
tally.data_gets = self.data_reads.gets();
tally.data_bytes = self.data_reads.bytes();
tally
}
pub(crate) fn data_reads(&self) -> Arc<data_file::DataStoreCounters> {
Arc::clone(&self.data_reads)
}
pub(crate) fn data_read_metrics(&self) -> Arc<data_file::ScopedReadMetrics> {
Arc::new(data_file::ScopedReadMetrics::reporting_to(Arc::clone(
&self.data_reads,
)))
}
pub(crate) fn tally_current_scan(&self) {
self.reads.current_scans.fetch_add(1, Ordering::Relaxed);
}
pub(crate) fn tally_history_scan(&self) {
self.reads.history_scans.fetch_add(1, Ordering::Relaxed);
}
#[cfg(test)]
pub(crate) fn entity_scan_tallies(&self) -> (u64, u64) {
(
self.reads.current_scans.load(Ordering::Relaxed),
self.reads.history_scans.load(Ordering::Relaxed),
)
}
pub(crate) fn note_head_read(&self) {
self.reads.head_reads.fetch_add(1, Ordering::Relaxed);
}
#[cfg(test)]
pub(crate) fn head_reads(&self) -> u64 {
self.reads.head_reads.load(Ordering::Relaxed)
}
pub(crate) fn refuse_if_closed(&self) -> Result<()> {
let Some(status) = &self.writer_status else {
return Ok(());
};
match status.borrow().close_reason {
None => Ok(()),
Some(CloseReason::Fenced) => Err(crate::error::fenced()),
Some(reason) => Err(Error::Interrupted(format!(
"this catalog's store handle has closed ({reason:?}); re-attach to use it"
))),
}
}
pub(crate) fn writer_head_view(&self) -> Option<Arc<CatalogSnapshot>> {
if !self.holds_the_writer() {
return None;
}
held_head_view(&self.projections)
}
pub(crate) fn holds_the_writer(&self) -> bool {
matches!(self.store.as_ref(), Store::Writer { .. })
}
pub async fn snapshot(&self) -> Result<Arc<CatalogSnapshot>> {
self.view(None).await
}
pub async fn snapshot_at(&self, snapshot: SnapshotId) -> Result<Arc<CatalogSnapshot>> {
self.view(Some(snapshot.get())).await
}
pub async fn recent_rows(&self, table: TableId) -> Result<Vec<RecentRow>> {
let session = self.begin_read().await?;
let outcome = self.scan_recent_rows(&session, table, None).await;
session.finish();
outcome
}
pub async fn recent_rows_at(
&self,
table: TableId,
snapshot: SnapshotId,
) -> Result<Vec<RecentRow>> {
let session = self.begin_read().await?;
let outcome = self
.scan_recent_rows(&session, table, Some(snapshot.get()))
.await;
session.finish();
outcome
}
pub async fn recent_row(&self, table: TableId, row_id: u64) -> Result<Option<RecentRow>> {
Ok(self
.recent_rows(table)
.await?
.into_iter()
.find(|row| row.row_id == row_id))
}
pub(crate) async fn live_inline_row_ids(&self, table: TableId) -> Result<Vec<u64>> {
let session = self.begin_read().await?;
let outcome = self.scan_live_inline_row_ids(&session, table).await;
session.finish();
outcome
}
async fn view(&self, at: Option<u64>) -> Result<Arc<CatalogSnapshot>> {
if at.is_some() {
let session = self.begin_read().await?;
let started = Instant::now();
let view = commit::materialize(session.handle(), at).await;
session.finish();
if view.is_ok() {
self.reads.materialized(started.elapsed());
}
return view.map(Arc::new);
}
if let Some(view) = self.writer_head_view() {
self.refuse_if_closed()?;
self.reads.cache_hits.fetch_add(1, Ordering::Relaxed);
return Ok(view);
}
let epoch = cache_epoch(&self.projections);
let session = self.begin_read().await?;
let view = self.head_view(session.handle()).await;
session.finish();
let view = view?;
install_head_view_at(&self.projections, epoch, Arc::clone(&view));
Ok(view)
}
async fn head_view(&self, handle: ReadHandle<'_>) -> Result<Arc<CatalogSnapshot>> {
self.note_head_read();
let head = commit::read_head_value(handle).await?;
if let Some(cached) = cached_head_view(&self.projections, &head) {
self.reads.cache_hits.fetch_add(1, Ordering::Relaxed);
return Ok(cached);
}
if let Some(behind) = held_head_view(&self.projections)
&& let Some(refreshed) = commit::refresh(handle, &behind).await?
{
self.reads.refreshes.fetch_add(1, Ordering::Relaxed);
return Ok(Arc::new(refreshed));
}
if let Some(current) = shared_current_entities(&self.projections, &head)
&& let Some(view) = commit::materialize_from(handle, &head, ¤t).await?
{
self.reads.cache_hits.fetch_add(1, Ordering::Relaxed);
return Ok(Arc::new(view));
}
let started = Instant::now();
let (view, records) = commit::materialize_capturing(handle).await?;
self.reads.materialized(started.elapsed());
self.tally_current_scan();
install_shared_current_entities(
&self.projections,
HeadValue {
snapshot_id: view.snapshot.snapshot_id,
batch_seq: view.batch_seq,
},
records,
);
Ok(Arc::new(view))
}
pub(crate) async fn begin_read(&self) -> Result<ReadSession> {
let session = match self.store.as_ref() {
Store::Writer { db, .. } => ReadSession::Tx(
db.begin(IsolationLevel::Snapshot)
.await
.map_err(Error::from)?,
),
Store::Reader(reader) => ReadSession::Reader(reader.clone()),
};
let refused = async {
let head = commit::read_head_value(session.handle()).await?;
commit::refuse_mid_migration_at(session.handle(), &self.projections, &head).await
};
if let Err(error) = refused.await {
session.finish();
return Err(error);
}
Ok(session)
}
pub async fn close(&self) -> Result<()> {
match self.store.as_ref() {
Store::Writer { db, .. } => db.close().await.map_err(Error::from),
Store::Reader(reader) => reader.close().await.map_err(Error::from),
}
}
}
impl Catalog {
fn writer(&self) -> Result<&Db> {
match self.store.as_ref() {
Store::Writer { db, .. } => Ok(db),
Store::Reader(_) => Err(Error::Constraint(
"catalog opened read-only; writes are unavailable".to_string(),
)),
}
}
pub async fn raise_format(
&self,
dry_run: bool,
) -> Result<crate::transaction::migration::FormatRaise> {
let raised = crate::transaction::migration::raise_format(self.writer()?, dry_run).await?;
if !dry_run {
crate::catalog::projection::raise_format_floor(self.projections(), raised.to_format);
}
Ok(raised)
}
pub async fn open(object_store: Arc<dyn ObjectStore>, options: CatalogOptions) -> Result<Self> {
if options.checkpoint.is_some() {
return Err(Error::Configuration(
"a checkpoint pins a read-only catalog to a fixed cut; a writer commits new \
state and cannot be opened against one"
.to_string(),
));
}
let located = Arc::clone(&object_store);
let manifest = census::manifest_bytes(&options.path, Arc::clone(&located))
.await
.ok();
warn_if_preload_cannot_fit(&options, manifest);
let store = StoreBuilder::new(&options.path, object_store)
.flush_interval(options.flush_interval)
.cache_dir(options.cache_dir.clone())
.cache_size(options.cache_size)
.cache_memory(options.cache_memory)
.cache_preload(options.cache_preload)
.cache_puts(options.cache_puts);
let (db, cache, format) = commit::open_initialized(
store,
options.encrypted,
options.data_path.as_deref(),
options.flush_on_commit,
)
.await?;
warn_if_metadata_cache_cannot_hold(
&options.path,
manifest.map(|manifest| manifest.metadata_bytes),
);
info!(
path = options.path,
flush_interval_ms = options.flush_interval.as_millis(),
flush_on_commit = options.flush_on_commit,
"opened catalog read-write"
);
let projections = Arc::new(std::sync::RwLock::new(ProjectionCache::empty()));
crate::catalog::projection::raise_format_floor(&projections, format);
let durability = commit::writer_durability(&db, options.flush_on_commit);
Ok(Self {
inner: ReadOnlyCatalog {
writer_status: Some(db.subscribe()),
store: Arc::new(Store::Writer {
db,
flush_on_commit: options.flush_on_commit,
}),
location: Arc::new(StoreLocation {
path: options.path,
object_store: located,
}),
reads: Arc::new(ReadTally::default()),
cache,
data_reads: Arc::default(),
commits: Arc::new(commit::Coalescer::new(Arc::clone(&projections), durability)),
projections,
},
})
}
pub async fn open_read_only(
object_store: Arc<dyn ObjectStore>,
options: CatalogOptions,
) -> Result<ReadOnlyCatalog> {
let checkpoint = parse_checkpoint(options.checkpoint.as_deref())?;
let located = Arc::clone(&object_store);
let manifest = census::manifest_bytes(&options.path, Arc::clone(&located))
.await
.ok();
warn_if_preload_cannot_fit(&options, manifest);
let store = StoreBuilder::new(&options.path, object_store)
.cache_dir(options.cache_dir.clone())
.cache_size(options.cache_size)
.cache_memory(options.cache_memory)
.cache_preload(options.cache_preload)
.cache_puts(options.cache_puts)
.poll_interval(options.reader_poll_interval)
.checkpoint(checkpoint);
let (reader, cache, format) = commit::open_reader_initialized(store).await?;
warn_if_metadata_cache_cannot_hold(
&options.path,
manifest.map(|manifest| manifest.metadata_bytes),
);
info!(
path = options.path,
checkpoint = options.checkpoint,
"opened catalog read-only"
);
let projections = Arc::new(std::sync::RwLock::new(ProjectionCache::empty()));
crate::catalog::projection::raise_format_floor(&projections, format);
Ok(ReadOnlyCatalog {
writer_status: None,
store: Arc::new(Store::Reader(Arc::new(reader))),
location: Arc::new(StoreLocation {
path: options.path,
object_store: located,
}),
reads: Arc::new(ReadTally::default()),
cache,
data_reads: Arc::default(),
commits: Arc::new(commit::Coalescer::new(
Arc::clone(&projections),
commit::CommitDurability::OnFlushInterval,
)),
projections,
})
}
pub async fn migrate(
object_store: Arc<dyn ObjectStore>,
options: CatalogOptions,
request: MigrationRequest,
) -> Result<MigrationReport> {
let (db, _cache) = StoreBuilder::new(&options.path, object_store.clone())
.flush_interval(options.flush_interval)
.cache_dir(options.cache_dir.clone())
.cache_size(options.cache_size)
.cache_memory(options.cache_memory)
.cache_preload(options.cache_preload)
.cache_puts(options.cache_puts)
.open_writer()
.await?;
let checkpoint = if request.checkpoint {
let taken = open::create_checkpoint(&db, None).await?;
info!(checkpoint = %taken, "took a pre-migration checkpoint");
Some(taken)
} else {
None
};
let report = migration::run(&db).await;
let closed = db.close().await.map_err(Error::from);
let report = report.and_then(|report| closed.map(|()| report))?;
if let Some(checkpoint) = checkpoint {
StoreBuilder::new(&options.path, object_store)
.delete_checkpoint(checkpoint)
.await?;
}
Ok(report)
}
pub async fn create_checkpoint(&self, lifetime: Option<Duration>) -> Result<String> {
let id = open::create_checkpoint(self.writer()?, lifetime).await?;
info!(checkpoint = %id, "created a checkpoint");
Ok(id.to_string())
}
pub async fn delete_checkpoint(
object_store: Arc<dyn ObjectStore>,
options: CatalogOptions,
checkpoint: &str,
) -> Result<()> {
let id = parse_checkpoint(Some(checkpoint))?
.ok_or_else(|| Error::Configuration("no checkpoint given".to_string()))?;
StoreBuilder::new(&options.path, object_store)
.delete_checkpoint(id)
.await
}
pub async fn checkpoints(
object_store: Arc<dyn ObjectStore>,
options: CatalogOptions,
) -> Result<Vec<String>> {
let ids = StoreBuilder::new(&options.path, object_store)
.list_checkpoints()
.await?;
Ok(ids.iter().map(uuid::Uuid::to_string).collect())
}
pub(crate) fn store(&self) -> Arc<Store> {
Arc::clone(&self.store)
}
pub(crate) async fn begin_write_tx(&self) -> Result<DbTransaction> {
self.writer()?
.begin(IsolationLevel::Snapshot)
.await
.map_err(Error::from)
}
pub async fn commit<F>(&self, f: F) -> Result<SnapshotId>
where
F: Fn(&mut Transaction) -> Result<()>,
{
let ids =
commit::commit_cycle(self.writer()?, std::slice::from_ref(&f), &self.commits).await?;
ids.first().copied().ok_or_else(|| {
Error::Corruption("a commit of one member reported no snapshot".to_string())
})
}
pub async fn commit_group(&self, members: &[CommitMember<'_>]) -> Result<Vec<SnapshotId>> {
commit::commit_cycle(self.writer()?, members, &self.commits).await
}
}