mod backfill;
mod index_build;
mod index_lookup;
mod index_probe_cache;
mod index_read_scope;
mod inline_scan;
mod maintenance;
mod row_location;
mod row_lookup;
mod table_warm;
#[cfg(test)]
mod tests;
use std::{
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
time::{Duration, Instant},
};
pub use index_read_scope::{IndexReadIdentity, IndexReadScope};
pub(crate) use inline_scan::InlineScan;
pub use maintenance::{
MaintenanceReport, MaintenanceRequest, MaintenanceStatusPass, MaintenanceStatusStep,
};
use object_store::ObjectStore;
pub use row_location::{
DeleteFileRegistration, ExcludedPositions, ExistingDeleteFile, LocatedDeletion,
LocatedPositions, LocatedRowScan, 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, fold_replayed_writes, held_head_view,
install_head_view_at, install_shared_current_entities, shared_current_entities,
view_head,
},
},
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,
read_transactions: 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,
pacer: Arc<commit::FlushPacer>,
},
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 { pacer, .. } => commit::CommitDurability::Paced(Arc::clone(pacer)),
Self::Reader(_) => commit::CommitDurability::OnFlushInterval,
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
#[allow(clippy::struct_excessive_bools)]
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_identity: Option<crate::CacheIdentity>,
pub cache_size: Option<u64>,
pub cache_memory: Option<u64>,
pub cache_preload: Option<CachePreload>,
pub cache_puts: bool,
pub cache_compaction_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_identity: None,
cache_size: None,
cache_memory: None,
cache_preload: None,
cache_puts: true,
cache_compaction_puts: true,
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 warn_if_block_cache_cannot_hold_index(path: &str, index_bytes: Option<u64>) {
let Some(index_bytes) = index_bytes else {
return;
};
let status = cache_status();
let attached_stores = status.attached_stores.max(1);
let share_bytes = status.block_capacity_bytes / attached_stores;
if let Some(shortfall) = crate::store::cache::index_share_shortfall(
index_bytes,
status.block_capacity_bytes,
attached_stores,
) {
warn!(
path,
index_bytes,
block_capacity_bytes = status.block_capacity_bytes,
attached_stores,
share_bytes,
shortfall,
"this store's share of the block cache cannot hold its index subspace, so probes \
will read index blocks from the disk tier or object storage on every statement. \
Raise the cache memory budget on the first attach in the process."
);
}
}
const SLOW_OPEN: Duration = Duration::from_millis(250);
pub(super) fn slow_open_without_cache(
cache_dir: Option<&std::path::Path>,
elapsed: Duration,
) -> bool {
cache_dir.is_none() && elapsed >= SLOW_OPEN
}
fn warn_if_slow_open_without_cache(options: &CatalogOptions, elapsed: Duration) {
if !slow_open_without_cache(options.cache_dir.as_deref(), elapsed) {
return;
}
warn!(
path = options.path,
elapsed_ms = crate::telemetry::milliseconds(elapsed),
"opening this store read what no cache directory kept, and every process that opens it reads the same bytes again. Set `CACHE_DIR` to a local directory to keep them on disk across restarts."
);
}
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 {
pinned: Option<Arc<index_read_scope::PinnedRead>>,
store: Arc<Store>,
reads: Arc<ReadTally>,
cache: Arc<CacheCounters>,
data_reads: Arc<data_file::DataStoreCounters>,
row_lookups: Arc<row_lookup::RowLookupCache>,
index_probes: Arc<index_probe_cache::IndexProbeCache>,
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 fn forget_head_view(&self) {
crate::catalog::projection::invalidate_head_view(self.projections());
}
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()
.saturating_add(self.row_lookups.estimated_bytes())
.saturating_add(self.index_probes.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)
}
#[cfg(test)]
pub(crate) fn read_transactions(&self) -> u64 {
self.reads.read_transactions.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 let Some(pinned) = &self.pinned {
return Some(pinned.snapshot.clone());
}
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(crate) fn warm_writer_read(
&self,
) -> Result<Option<(Arc<CatalogSnapshot>, ReadHandle<'_>)>> {
if let Some(pinned) = &self.pinned {
self.refuse_if_closed()?;
return Ok(Some((
pinned.snapshot.clone(),
ReadHandle::Tx(&pinned.transaction),
)));
}
let (Some(view), Some(db)) = (self.writer_head_view(), self.store.writer_db()) else {
return Ok(None);
};
self.refuse_if_closed()?;
self.reads.cache_hits.fetch_add(1, Ordering::Relaxed);
Ok(Some((view, ReadHandle::Writer(db))))
}
pub(crate) fn still_holds(&self, view: &Arc<CatalogSnapshot>) -> bool {
self.writer_head_view()
.is_some_and(|held| Arc::ptr_eq(&held, view))
}
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>> {
let session = self.begin_read().await?;
let outcome = self.scan_recent_row(&session, table, row_id).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(), epoch).await;
session.finish();
view
}
async fn head_view(&self, handle: ReadHandle<'_>, epoch: u64) -> Result<Arc<CatalogSnapshot>> {
if let Some(pinned) = &self.pinned {
return Ok(pinned.snapshot.clone());
}
let view = self.load_head_view(handle).await?;
install_head_view_at(&self.projections, epoch, Arc::clone(&view));
Ok(view)
}
async fn load_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, writes)) = commit::refresh_with_writes(handle, &behind).await?
{
self.reads.refreshes.fetch_add(1, Ordering::Relaxed);
fold_replayed_writes(
&self.projections,
&view_head(&behind),
view_head(&refreshed),
&writes,
);
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> {
if let Some(pinned) = &self.pinned {
self.refuse_if_closed()?;
return Ok(ReadSession::Pinned(pinned.transaction.clone()));
}
let session = match self.store.as_ref() {
Store::Writer { db, .. } => {
self.reads.read_transactions.fetch_add(1, Ordering::Relaxed);
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()?,
&self.store.commit_durability(),
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 started = Instant::now();
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.as_ref());
let store = StoreBuilder::new(&options.path, object_store)
.without_flush_timer()
.cache_dir(options.cache_dir.clone())
.cache_identity(options.cache_identity)
.cache_size(options.cache_size)
.cache_memory(options.cache_memory)
.cache_preload(options.cache_preload)
.warm_segments(
manifest
.as_ref()
.map(|manifest| manifest.segments.clone())
.unwrap_or_default(),
)
.cache_puts(options.cache_puts)
.cache_compaction_puts(options.cache_compaction_puts);
let flush_spacing = if options.flush_on_commit {
Duration::ZERO
} else {
options.flush_interval
};
let (db, cache, format, pacer) = commit::open_initialized(
store,
options.encrypted,
options.data_path.as_deref(),
flush_spacing,
)
.await?;
warn_if_metadata_cache_cannot_hold(
&options.path,
manifest.as_ref().map(|manifest| manifest.metadata_bytes),
);
warn_if_block_cache_cannot_hold_index(
&options.path,
manifest.as_ref().and_then(|manifest| {
let index = crate::store::key::subspace_prefix(crate::store::key::Subspace::Index);
manifest
.segments
.iter()
.find(|segment| segment.prefix == index)
.map(|segment| segment.bytes)
}),
);
let elapsed = started.elapsed();
info!(
path = options.path,
flush_interval_ms = options.flush_interval.as_millis(),
flush_on_commit = options.flush_on_commit,
elapsed_ms = crate::telemetry::milliseconds(elapsed),
"opened catalog read-write"
);
warn_if_slow_open_without_cache(&options, elapsed);
let projections = Arc::new(std::sync::RwLock::new(ProjectionCache::empty()));
crate::catalog::projection::raise_format_floor(&projections, format);
let durability = commit::CommitDurability::Paced(Arc::clone(&pacer));
Ok(Self {
inner: ReadOnlyCatalog {
pinned: None,
writer_status: Some(db.subscribe()),
store: Arc::new(Store::Writer { db, pacer }),
location: Arc::new(StoreLocation {
path: options.path,
object_store: located,
}),
reads: Arc::new(ReadTally::default()),
cache,
data_reads: Arc::default(),
row_lookups: Arc::default(),
index_probes: 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 started = Instant::now();
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.as_ref());
let store = StoreBuilder::new(&options.path, object_store)
.cache_dir(options.cache_dir.clone())
.cache_identity(options.cache_identity)
.cache_size(options.cache_size)
.cache_memory(options.cache_memory)
.cache_preload(options.cache_preload)
.warm_segments(
manifest
.as_ref()
.map(|manifest| manifest.segments.clone())
.unwrap_or_default(),
)
.cache_puts(options.cache_puts)
.cache_compaction_puts(options.cache_compaction_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.as_ref().map(|manifest| manifest.metadata_bytes),
);
warn_if_block_cache_cannot_hold_index(
&options.path,
manifest.as_ref().and_then(|manifest| {
let index = crate::store::key::subspace_prefix(crate::store::key::Subspace::Index);
manifest
.segments
.iter()
.find(|segment| segment.prefix == index)
.map(|segment| segment.bytes)
}),
);
let elapsed = started.elapsed();
info!(
path = options.path,
checkpoint = options.checkpoint,
elapsed_ms = crate::telemetry::milliseconds(elapsed),
"opened catalog read-only"
);
warn_if_slow_open_without_cache(&options, elapsed);
let projections = Arc::new(std::sync::RwLock::new(ProjectionCache::empty()));
crate::catalog::projection::raise_format_floor(&projections, format);
Ok(ReadOnlyCatalog {
pinned: None,
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(),
row_lookups: Arc::default(),
index_probes: 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_identity(options.cache_identity)
.cache_size(options.cache_size)
.cache_memory(options.cache_memory)
.cache_preload(options.cache_preload)
.cache_puts(options.cache_puts)
.cache_compaction_puts(options.cache_compaction_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)
}
#[cfg(test)]
pub(crate) async fn flush_memtable(&self) -> Result<()> {
use slatedb::config::{FlushOptions, FlushType};
self.writer()?
.flush_with_options(FlushOptions {
flush_type: FlushType::MemTable,
})
.await
.map_err(Error::from)
}
#[cfg(test)]
pub(crate) async fn commit_write_tx(&self, tx: DbTransaction) -> Result<()> {
commit::commit_durable(
tx,
"test",
crate::store::StagedBytes::default(),
&self.store.commit_durability(),
)
.await
.map(|_| ())
.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
}
}