#![cfg(feature = "kv-rocksdb")]
#![deny(clippy::await_holding_lock)]
mod background_flusher;
mod cnf;
mod commit_coordinator;
mod comparator;
mod disk_space_manager;
mod garbage_collector;
mod inline_guard;
mod memory_manager;
mod prefix_extractor;
mod range_shard;
mod scan_cursor;
#[cfg(test)]
mod tests;
use std::ops::Range;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
use std::time::Duration;
use background_flusher::BackgroundFlusher;
pub use cnf::RocksDbConfig;
use commit_coordinator::CommitCoordinator;
use disk_space_manager::{DiskSpaceManager, DiskSpaceState, TransactionState};
use garbage_collector::GarbageCollector;
use inline_guard::InlineGuard;
use memory_manager::MemoryManager;
use range_shard::{COUNT_PARALLEL_MAX_SHARDS, shard_range};
use rocksdb::{
BottommostLevelCompaction, ColumnFamilyDescriptor, CompactOptions, DBCompactionStyle,
DBCompressionType, DBRawIteratorWithThreadMode, FlushOptions, LogLevel,
OptimisticTransactionDB, OptimisticTransactionOptions, Options, ReadOptions,
SnapshotWithThreadMode, UniversalCompactOptions, UniversalCompactionStopStyle,
WaitForCompactOptions, WriteOptions, properties,
};
use scan_cursor::{
AliveGuard, RocksDbKeysCursor, RocksDbValsCursor, ScanIter, ScanStateKeys, ScanStateVals,
};
use tokio::sync::{Mutex, MutexGuard};
use web_time::Instant;
use super::api::{
BoxFut, GetMultiResult, KeySpan, KeyValSpan, KeyVisitor, KeysBatch, KeysResult, ScanChunkStats,
ScanCursorKeys, ScanCursorVals, ScanResult, ValVisitor, ValsBatch,
};
use super::config::SyncMode;
use super::err::{Error, Result};
use super::{Direction, ESTIMATED_BYTES_PER_KEY, ESTIMATED_BYTES_PER_KV};
use crate::key::debug::Sprintable;
use crate::kvs::api::Transactable;
use crate::kvs::ds::{Metric, Metrics};
use crate::kvs::timestamp::HlcTimeStamp;
use crate::kvs::{Key, Val};
const TARGET: &str = "surrealdb::core::kvs::rocksdb";
pub struct Datastore {
db: Pin<Arc<OptimisticTransactionDB>>,
versioned: bool,
memory_manager: Arc<MemoryManager>,
disk_space_manager: Option<Arc<DiskSpaceManager>>,
commit_coordinator: Option<Arc<CommitCoordinator>>,
background_flusher: Option<Arc<BackgroundFlusher>>,
garbage_collector: Option<Arc<GarbageCollector>>,
prefix_extractor_enabled: bool,
scan_verify_checksums: bool,
compact_on_shutdown: bool,
shutdown_wait_for_compact_seconds: u64,
inline_guard: Arc<InlineGuard>,
}
pub struct Transaction {
pub(super) cursors_alive: AtomicUsize,
done: AtomicBool,
write: bool,
versioned: bool,
read_options: ReadOptions,
inner: Mutex<Option<TransactionInner>>,
transaction_state: Arc<AtomicU8>,
disk_space_manager: Option<Arc<DiskSpaceManager>>,
commit_coordinator: Option<Arc<CommitCoordinator>>,
db: Pin<Arc<OptimisticTransactionDB>>,
prefix_extractor_enabled: bool,
scan_verify_checksums: bool,
inline_guard: Arc<InlineGuard>,
}
impl Transaction {
fn ensure_versioned(&self, version: Option<u64>) -> Result<()> {
if !self.versioned && version.is_some() {
return Err(Error::UnsupportedVersionedQueries);
}
Ok(())
}
}
struct TransactionInner {
snapshot:
SnapshotWithThreadMode<'static, rocksdb::Transaction<'static, OptimisticTransactionDB>>,
tx: Box<rocksdb::Transaction<'static, OptimisticTransactionDB>>,
}
fn apply_cf_level_options(target: &mut Options, config: &RocksDbConfig) {
info!(target: TARGET, "Target file size for compaction: {}", config.target_file_size_base);
target.set_target_file_size_base(config.target_file_size_base);
let size_multiplier = config.target_file_size_multiplier.min(i32::MAX as usize) as i32;
info!(target: TARGET, "Target file size compaction multiplier: {size_multiplier}");
target.set_target_file_size_multiplier(size_multiplier);
let compaction_trigger = config.file_compaction_trigger.min(i32::MAX as usize) as i32;
info!(target: TARGET, "Number of files to trigger compaction: {compaction_trigger}");
target.set_level_zero_file_num_compaction_trigger(compaction_trigger);
info!(target: TARGET, "Level-0 slowdown writes trigger: {}", config.level0_slowdown_writes_trigger);
target.set_level_zero_slowdown_writes_trigger(config.level0_slowdown_writes_trigger);
info!(target: TARGET, "Level-0 stop writes trigger: {}", config.level0_stop_writes_trigger);
target.set_level_zero_stop_writes_trigger(config.level0_stop_writes_trigger);
if config.periodic_compaction_seconds > 0 {
info!(target: TARGET, "Periodic compaction seconds: {}", config.periodic_compaction_seconds);
target.set_periodic_compaction_seconds(config.periodic_compaction_seconds);
} else {
info!(target: TARGET, "Periodic compaction: disabled");
}
info!(target: TARGET, "Enable separation of keys and values: {}", config.enable_blob_files);
target.set_enable_blob_files(config.enable_blob_files);
info!(target: TARGET, "Minimum blob value size: {}", config.min_blob_size);
target.set_min_blob_size(config.min_blob_size);
info!(target: TARGET, "Target blob file size: {}", config.blob_file_size);
target.set_blob_file_size(config.blob_file_size);
let (db_compression, name) = match config.blob_compression_type {
cnf::BlobCompression::Snappy => (DBCompressionType::Snappy, "snappy"),
cnf::BlobCompression::Lz4 => (DBCompressionType::Lz4, "lz4"),
cnf::BlobCompression::Zstd => (DBCompressionType::Zstd, "zstd"),
cnf::BlobCompression::None => (DBCompressionType::None, "none"),
};
info!(target: TARGET, "Blob compression type: {name}");
target.set_blob_compression_type(db_compression);
info!(target: TARGET, "Enable blob garbage collection: {}", config.enable_blob_gc);
target.set_enable_blob_gc(config.enable_blob_gc);
info!(target: TARGET, "Blob GC age cutoff: {}", config.blob_gc_age_cutoff);
target.set_blob_gc_age_cutoff(config.blob_gc_age_cutoff);
info!(target: TARGET, "Blob GC force threshold: {}", config.blob_gc_force_threshold);
target.set_blob_gc_force_threshold(config.blob_gc_force_threshold);
info!(target: TARGET, "Blob compaction readahead size: {}", config.blob_compaction_readahead_size);
target.set_blob_compaction_readahead_size(config.blob_compaction_readahead_size);
info!(target: TARGET, "Setting delete compaction factory: {} / {} ({})",
config.deletion_factory_window_size,
config.deletion_factory_delete_count,
config.deletion_factory_ratio,
);
target.add_compact_on_deletion_collector_factory(
config.deletion_factory_window_size,
config.deletion_factory_delete_count,
config.deletion_factory_ratio,
);
info!(target: TARGET, "Setting compaction style: {}", config.compaction_style);
let style = match config.compaction_style.to_ascii_lowercase().as_str() {
"universal" => DBCompactionStyle::Universal,
_ => DBCompactionStyle::Level,
};
target.set_compaction_style(style);
if matches!(style, DBCompactionStyle::Universal) {
let mut uco = UniversalCompactOptions::default();
info!(target: TARGET, "Universal compaction size ratio: {}", config.universal_size_ratio);
uco.set_size_ratio(config.universal_size_ratio);
let min = config.universal_min_merge_width.min(i32::MAX as u32);
info!(target: TARGET, "Universal compaction min merge width: {min}");
uco.set_min_merge_width(min as i32);
let max = config.universal_max_merge_width.min(i32::MAX as u32);
info!(target: TARGET, "Universal compaction max merge width: {max}");
uco.set_max_merge_width(max as i32);
let amp_pct = config.universal_max_size_amplification_percent.min(i32::MAX as u32);
info!(target: TARGET, "Universal compaction max size amplification percent: {amp_pct}");
uco.set_max_size_amplification_percent(amp_pct as i32);
let compress_pct = config.universal_compression_size_percent;
info!(target: TARGET, "Universal compaction compression size percent: {compress_pct}");
uco.set_compression_size_percent(compress_pct);
let style = config.universal_stop_style.to_ascii_lowercase();
info!(target: TARGET, "Universal compaction stop style: {style}");
uco.set_stop_style(match style.as_str() {
"similar_size" | "similar" => UniversalCompactionStopStyle::Similar,
_ => UniversalCompactionStopStyle::Total,
});
target.set_universal_compaction_options(&uco);
}
info!(target: TARGET, "Setting compression level");
target.set_compression_per_level(&[
DBCompressionType::None, DBCompressionType::Lz4, DBCompressionType::Lz4, DBCompressionType::Lz4, DBCompressionType::Lz4, DBCompressionType::Zstd, DBCompressionType::Zstd, DBCompressionType::Zstd, ]);
info!(target: TARGET, "Setting bottommost compression type: Zstd");
target.set_bottommost_compression_type(DBCompressionType::Zstd);
info!(target: TARGET, "Using Zstd typed dictionary training");
target.set_bottommost_zstd_max_train_bytes(0, true);
if config.prefix_extractor_enabled {
info!(target: TARGET, "Prefix extractor: enabled ({})", prefix_extractor::NAME);
target.set_prefix_extractor(prefix_extractor::build());
let ratio = config.memtable_prefix_bloom_ratio;
if ratio > 0.0 {
info!(target: TARGET, "Memtable prefix bloom ratio: {ratio}");
target.set_memtable_prefix_bloom_ratio(ratio);
}
} else {
info!(target: TARGET, "Prefix extractor: disabled");
}
}
impl Datastore {
pub(crate) async fn new(path: &str, config: RocksDbConfig) -> Result<Datastore> {
let mut opts = Options::default();
opts.set_use_fsync(false);
opts.create_if_missing(true);
opts.create_missing_column_families(true);
opts.set_manual_wal_flush(false);
opts.set_wal_bytes_per_sync(2 * 1024 * 1024);
let threads = config.thread_count.min(i32::MAX as usize) as i32;
info!(target: TARGET, "Background thread count: {threads}");
opts.increase_parallelism(threads);
let background_jobs = config.jobs_count.min(i32::MAX as usize) as i32;
info!(target: TARGET, "Maximum background jobs count: {background_jobs}");
opts.set_max_background_jobs(background_jobs);
let max_open_files = config.max_open_files.min(i32::MAX as usize) as i32;
info!(target: TARGET, "Maximum number of open files: {max_open_files}");
opts.set_max_open_files(max_open_files);
info!(target: TARGET, "Number of log files to keep: {}", config.keep_log_file_num);
opts.set_keep_log_file_num(config.keep_log_file_num);
info!(target: TARGET, "Compaction readahead size: {}", config.compaction_readahead_size);
opts.set_compaction_readahead_size(config.compaction_readahead_size);
info!(target: TARGET, "Maximum concurrent subcompactions: {}", config.max_concurrent_subcompactions);
opts.set_max_subcompactions(config.max_concurrent_subcompactions);
info!(target: TARGET, "Use separate thread queues: {}", config.enable_pipelined_writes);
opts.set_enable_pipelined_write(config.enable_pipelined_writes);
info!(target: TARGET, "Write-ahead-log file size limit: {}MB", config.wal_size_limit);
opts.set_wal_size_limit_mb(config.wal_size_limit);
info!(target: TARGET, "Allow concurrent memtable writes: true");
opts.set_allow_concurrent_memtable_write(true);
info!(target: TARGET, "Avoid unnecessary blocking IO: true");
opts.set_avoid_unnecessary_blocking_io(true);
info!(target: TARGET, "Allow adaptive write thread yielding: true");
opts.set_enable_write_thread_adaptive_yield(true);
info!(target: TARGET, "Setting storage engine log level: {}", config.storage_log_level);
opts.set_log_level(match config.storage_log_level.to_ascii_lowercase().as_str() {
"debug" => LogLevel::Debug,
"info" => LogLevel::Info,
"warn" => LogLevel::Warn,
"error" => LogLevel::Error,
"fatal" => LogLevel::Fatal,
l => {
return Err(Error::Datastore(format!(
"Invalid storage engine log level specified: {l}"
)));
}
});
apply_cf_level_options(&mut opts, &config);
let memory_manager = Arc::new(MemoryManager::configure(&mut opts, &config)?);
let cf_opts = if config.versioned {
info!(target: TARGET, "Enabling user-defined timestamps (versioning)");
let mut cf_opts = Options::default();
cf_opts.set_comparator_with_ts(
comparator::NAME,
comparator::TIMESTAMP_SIZE,
Box::new(comparator::compare),
Box::new(comparator::compare_ts),
Box::new(comparator::compare_without_ts),
);
apply_cf_level_options(&mut cf_opts, &config);
memory_manager.apply_to_cf_options(&mut cf_opts, &config);
Some(cf_opts)
} else {
None
};
let should_create_disk_space_manager = DiskSpaceManager::configure(&mut opts, &config)?;
match config.sync_mode {
SyncMode::Interval(_) => BackgroundFlusher::configure(&mut opts, &config),
SyncMode::Every => CommitCoordinator::configure(&mut opts, &config),
SyncMode::Never => {}
};
let disk_space_manager = if should_create_disk_space_manager {
Some(Arc::new(DiskSpaceManager::new(&mut opts, &config)?))
} else {
None
};
let db = if let Some(cf_opts) = cf_opts {
let descriptors = vec![ColumnFamilyDescriptor::new("default", cf_opts)];
Arc::pin(OptimisticTransactionDB::open_cf_descriptors(&opts, path, descriptors)?)
} else {
Arc::pin(OptimisticTransactionDB::open(&opts, path)?)
};
let commit_coordinator = if let SyncMode::Every = config.sync_mode {
Some(Arc::new(CommitCoordinator::new(db.clone(), &config)?))
} else {
None
};
let background_flusher = if let SyncMode::Interval(interval) = config.sync_mode {
Some(Arc::new(BackgroundFlusher::new(db.clone(), interval)?))
} else {
None
};
let garbage_collector = if config.versioned && config.retention != Duration::ZERO {
Some(Arc::new(GarbageCollector::new(db.clone(), config.retention)?))
} else {
None
};
if let SyncMode::Never = config.sync_mode {
info!(target: TARGET, "Sync mode: never (handled by the OS");
opts.set_manual_wal_flush(false);
}
memory_manager.register_with_allocator_tracker();
let inline_guard =
Arc::new(InlineGuard::new(config.runtime_worker_threads, config.runtime_reserve));
Ok(Datastore {
db,
versioned: config.versioned,
memory_manager,
disk_space_manager,
background_flusher,
commit_coordinator,
garbage_collector,
prefix_extractor_enabled: config.prefix_extractor_enabled,
scan_verify_checksums: config.scan_verify_checksums,
compact_on_shutdown: config.compact_on_shutdown,
shutdown_wait_for_compact_seconds: config.shutdown_wait_for_compact_seconds,
inline_guard,
})
}
const BLOCK_CACHE_USAGE: &str = "rocksdb.block_cache_usage";
const BLOCK_CACHE_PINNED_USAGE: &str = "rocksdb.block_cache_pinned_usage";
const ESTIMATE_TABLE_READERS_MEM: &str = "rocksdb.estimate_table_readers_mem";
const CUR_SIZE_ALL_MEM_TABLES: &str = "rocksdb.cur_size_all_mem_tables";
const TOTAL_SST_FILES_SIZE: &str = "rocksdb.total_sst_files_size";
const LIVE_SST_FILES_SIZE: &str = "rocksdb.live_sst_files_size";
const ESTIMATE_LIVE_DATA_SIZE: &str = "rocksdb.estimate_live_data_size";
const ESTIMATE_NUM_KEYS: &str = "rocksdb.estimate_num_keys";
const COMPACTION_PENDING: &str = "rocksdb.compaction_pending";
const NUM_RUNNING_COMPACTIONS: &str = "rocksdb.num_running_compactions";
const NUM_RUNNING_FLUSHES: &str = "rocksdb.num_running_flushes";
const INLINE_BLOCKING_GRANTED: &str = "rocksdb.inline_blocking_granted";
const INLINE_BLOCKING_DIVERTED: &str = "rocksdb.inline_blocking_diverted";
pub(crate) fn register_metrics(&self) -> Metrics {
Metrics {
name: "surrealdb.rocksdb",
u64_metrics: vec![
Metric {
name: Self::BLOCK_CACHE_USAGE,
description: "Returns the memory size (in bytes) for the entries residing in block cache.",
},
Metric {
name: Self::BLOCK_CACHE_PINNED_USAGE,
description: "Returns the memory size (in bytes) for the entries being pinned.",
},
Metric {
name: Self::ESTIMATE_TABLE_READERS_MEM,
description: "Returns estimated memory size (in bytes) used for reading SST tables, excluding memory used in block cache (e.g., filter and index blocks).",
},
Metric {
name: Self::CUR_SIZE_ALL_MEM_TABLES,
description: "Returns approximate size (in bytes) of active and unflushed immutable memtables",
},
Metric {
name: Self::TOTAL_SST_FILES_SIZE,
description: "Total on-disk size (bytes) of all SST files, including obsolete ones that are still referenced by snapshots.",
},
Metric {
name: Self::LIVE_SST_FILES_SIZE,
description: "On-disk size (bytes) of SST files referenced by the current LSM tree.",
},
Metric {
name: Self::ESTIMATE_LIVE_DATA_SIZE,
description: "Estimated logical live data size (bytes) after applying tombstones.",
},
Metric {
name: Self::ESTIMATE_NUM_KEYS,
description: "Estimated number of live keys in the LSM tree.",
},
Metric {
name: Self::COMPACTION_PENDING,
description: "1 if a compaction is pending, 0 otherwise.",
},
Metric {
name: Self::NUM_RUNNING_COMPACTIONS,
description: "Number of compactions currently running.",
},
Metric {
name: Self::NUM_RUNNING_FLUSHES,
description: "Number of memtable flushes currently running.",
},
Metric {
name: Self::INLINE_BLOCKING_GRANTED,
description: "Per-datastore count of storage calls that ran inline on a tokio worker (inline-blocking permit granted).",
},
Metric {
name: Self::INLINE_BLOCKING_DIVERTED,
description: "Per-datastore count of storage calls diverted to the affinity pool because the inline-blocking cap was hit.",
},
],
}
}
pub(crate) fn collect_u64_metric(&self, metric: &str) -> Option<u64> {
match metric {
Self::INLINE_BLOCKING_GRANTED => return Some(self.inline_guard.granted()),
Self::INLINE_BLOCKING_DIVERTED => return Some(self.inline_guard.diverted()),
_ => {}
}
let metric = match metric {
Self::BLOCK_CACHE_USAGE => Some(properties::BLOCK_CACHE_USAGE),
Self::BLOCK_CACHE_PINNED_USAGE => Some(properties::BLOCK_CACHE_PINNED_USAGE),
Self::ESTIMATE_TABLE_READERS_MEM => Some(properties::ESTIMATE_TABLE_READERS_MEM),
Self::CUR_SIZE_ALL_MEM_TABLES => Some(properties::CUR_SIZE_ALL_MEM_TABLES),
Self::TOTAL_SST_FILES_SIZE => Some(properties::TOTAL_SST_FILES_SIZE),
Self::LIVE_SST_FILES_SIZE => Some(properties::LIVE_SST_FILES_SIZE),
Self::ESTIMATE_LIVE_DATA_SIZE => Some(properties::ESTIMATE_LIVE_DATA_SIZE),
Self::ESTIMATE_NUM_KEYS => Some(properties::ESTIMATE_NUM_KEYS),
Self::COMPACTION_PENDING => Some(properties::COMPACTION_PENDING),
Self::NUM_RUNNING_COMPACTIONS => Some(properties::NUM_RUNNING_COMPACTIONS),
Self::NUM_RUNNING_FLUSHES => Some(properties::NUM_RUNNING_FLUSHES),
_ => None,
};
metric.map(|metric| {
self.db.property_int_value(metric).unwrap_or_default().unwrap_or_default()
})
}
pub(crate) async fn shutdown(&self) -> Result<()> {
if let Some(garbage_collector) = &self.garbage_collector {
garbage_collector.shutdown()?;
}
if let Some(background_flusher) = &self.background_flusher {
background_flusher.shutdown()?;
}
if let Some(commit_coordinator) = &self.commit_coordinator {
commit_coordinator.shutdown()?;
}
let mut flush_opts = FlushOptions::default();
flush_opts.set_wait(true);
if let Err(e) = self.db.flush_wal(true) {
error!("An error occurred flushing the WAL buffer to disk: {e}");
}
if let Err(e) = self.db.flush_opt(&flush_opts) {
error!("An error occurred flushing memtables to SST files: {e}");
}
let compact_on_shutdown = self.compact_on_shutdown;
let wait_for_compact_seconds = self.shutdown_wait_for_compact_seconds;
let cleanup: anyhow::Result<()> = affinitypool::spawn_local(move || {
if compact_on_shutdown {
info!(
target: TARGET,
"Running full-keyspace compaction on shutdown",
);
let mut copts = CompactOptions::default();
copts.set_exclusive_manual_compaction(true);
copts.set_change_level(true);
copts.set_target_level(6);
copts.set_bottommost_level_compaction(BottommostLevelCompaction::Force);
self.db.compact_range_opt::<&[u8], &[u8]>(None, None, &copts);
}
let mut wfco = WaitForCompactOptions::default();
let timeout_us = wait_for_compact_seconds.saturating_mul(1_000_000);
wfco.set_timeout(timeout_us);
info!(
target: TARGET,
"Waiting for in-flight compactions to drain (timeout: {wait_for_compact_seconds}s)",
);
if let Err(e) = self.db.wait_for_compact(&wfco) {
error!("An error occurred waiting for compactions to drain: {e}");
}
info!(target: TARGET, "Cancelling background work");
self.db.cancel_all_background_work(true);
Ok(())
})
.await;
if let Err(e) = cleanup {
error!("An error occurred during shutdown cleanup: {e}");
}
self.memory_manager.shutdown()?;
Ok(())
}
pub(crate) async fn transaction(&self, write: bool, _: bool) -> Result<Box<dyn Transactable>> {
let mut to = OptimisticTransactionOptions::default();
to.set_snapshot(true);
let mut wo = WriteOptions::default();
wo.set_sync(false);
let tx = self.db.transaction_opt(&wo, &to);
if self.versioned {
let read_ts = HlcTimeStamp::next();
tx.set_read_timestamp_for_validation(read_ts.0);
}
let tx = unsafe {
std::mem::transmute::<
rocksdb::Transaction<'_, OptimisticTransactionDB>,
rocksdb::Transaction<'static, OptimisticTransactionDB>,
>(tx)
};
let tx = Box::new(tx);
let snapshot = unsafe {
std::mem::transmute::<
SnapshotWithThreadMode<'_, rocksdb::Transaction<'static, OptimisticTransactionDB>>,
SnapshotWithThreadMode<
'static,
rocksdb::Transaction<'static, OptimisticTransactionDB>,
>,
>(tx.as_ref().snapshot())
};
let mut ro = ReadOptions::default();
ro.set_snapshot(&snapshot);
ro.set_async_io(true);
ro.fill_cache(true);
if self.versioned {
ro.set_timestamp(u64::MAX.to_le_bytes().to_vec());
}
Ok(Box::new(Transaction {
cursors_alive: AtomicUsize::new(0),
done: AtomicBool::new(false),
write,
versioned: self.versioned,
read_options: ro,
inner: Mutex::new(Some(TransactionInner {
tx,
snapshot,
})),
transaction_state: Arc::new(Default::default()),
disk_space_manager: self.disk_space_manager.clone(),
commit_coordinator: self.commit_coordinator.clone(),
db: self.db.clone(),
prefix_extractor_enabled: self.prefix_extractor_enabled,
scan_verify_checksums: self.scan_verify_checksums,
inline_guard: Arc::clone(&self.inline_guard),
}))
}
}
impl Transaction {
fn current_state(&self) -> TransactionState {
match self.transaction_state.load(Ordering::Acquire) {
0 => TransactionState::ReadsOnly,
1 => TransactionState::HasDeletes,
2 => TransactionState::HasWrites,
_ => unreachable!(),
}
}
fn store_deletes(&self) {
if self.current_state() < TransactionState::HasDeletes {
self.transaction_state.store(TransactionState::HasDeletes as u8, Ordering::Release);
}
}
fn store_writes(&self) {
if self.current_state() < TransactionState::HasWrites {
self.transaction_state.store(TransactionState::HasWrites as u8, Ordering::Release);
}
}
fn contains_deletes(&self) -> bool {
self.current_state() == TransactionState::HasDeletes
}
fn contains_writes(&self) -> bool {
self.current_state() == TransactionState::HasWrites
}
fn is_restricted(&self, recalculate: bool) -> bool {
if let Some(dsm) = self.disk_space_manager.as_ref() {
match recalculate {
false => dsm.cached_state() == DiskSpaceState::ReadAndDeletionOnly,
true => dsm.latest_state() == DiskSpaceState::ReadAndDeletionOnly,
}
} else {
false
}
}
fn versioned_read_options(
&self,
version: Option<u64>,
inner: &TransactionInner,
) -> ReadOptions {
let mut ro = ReadOptions::default();
ro.set_snapshot(&inner.snapshot);
ro.set_async_io(true);
ro.fill_cache(true);
if self.versioned {
let ts = version.unwrap_or(u64::MAX);
ro.set_timestamp(ts.to_le_bytes().to_vec());
}
ro
}
fn apply_prefix_mode(&self, ro: &mut ReadOptions, rng: &Range<Key>) {
if !self.prefix_extractor_enabled {
return;
}
match (prefix_extractor::extract(&rng.start), prefix_extractor::extract(&rng.end)) {
(Some(sp), Some(ep)) if sp == ep => {
ro.set_prefix_same_as_start(true);
}
_ => {
ro.set_total_order_seek(true);
}
}
}
fn scan_read_options(
&self,
rng: &Range<Key>,
version: Option<u64>,
inner: &TransactionInner,
) -> ReadOptions {
let mut ro = ReadOptions::default();
ro.set_snapshot(&inner.snapshot);
ro.set_iterate_lower_bound(rng.start.clone());
ro.set_iterate_upper_bound(rng.end.clone());
ro.set_auto_readahead_size(true);
ro.set_async_io(true);
ro.fill_cache(true);
ro.set_verify_checksums(self.scan_verify_checksums);
self.apply_prefix_mode(&mut ro, rng);
if self.versioned {
let ts = version.unwrap_or(u64::MAX);
ro.set_timestamp(ts.to_le_bytes().to_vec());
}
ro
}
fn count_read_options(
&self,
rng: &Range<Key>,
version: Option<u64>,
inner: &TransactionInner,
) -> ReadOptions {
let mut ro = ReadOptions::default();
ro.set_snapshot(&inner.snapshot);
ro.set_iterate_lower_bound(rng.start.clone());
ro.set_iterate_upper_bound(rng.end.clone());
ro.set_auto_readahead_size(true);
ro.set_readahead_size(2 * 1024 * 1024);
ro.set_async_io(true);
ro.fill_cache(false);
ro.set_verify_checksums(self.scan_verify_checksums);
self.apply_prefix_mode(&mut ro, rng);
if self.versioned {
let ts = version.unwrap_or(u64::MAX);
ro.set_timestamp(ts.to_le_bytes().to_vec());
}
ro
}
async fn run_blocking<'a, F, R>(&'a self, op: F) -> Result<R>
where
F: FnOnce(MutexGuard<'a, Option<TransactionInner>>) -> Result<R> + Send + 'a,
R: Send + 'a,
{
let guard = self.inner.lock().await;
self.inline_guard.try_inline_or_offload(move || op(guard)).await
}
fn count_blocking(
&self,
rng: Range<Key>,
version: Option<u64>,
guard: MutexGuard<'_, Option<TransactionInner>>,
) -> Result<usize> {
let inner =
guard.as_ref().ok_or_else(|| Error::Internal("expected a transaction".into()))?;
let ro = self.count_read_options(&rng, version, inner);
let mut res: usize = 0;
if self.write {
let mut iter = inner.tx.raw_iterator_opt(ro);
iter.seek(&rng.start);
while iter.valid() {
res += 1;
iter.next();
}
iter.status()?;
}
else {
drop(guard);
let mut iter = self.db.raw_iterator_opt(ro);
iter.seek(&rng.start);
while iter.valid() {
res += 1;
iter.next();
}
iter.status()?;
}
Ok(res)
}
fn keys_blocking(
&self,
rng: Range<Key>,
limit: u32,
skip: u32,
version: Option<u64>,
dir: Direction,
guard: MutexGuard<'_, Option<TransactionInner>>,
) -> Result<KeysResult> {
let inner =
guard.as_ref().ok_or_else(|| Error::Internal("expected a transaction".into()))?;
let ro = self.scan_read_options(&rng, version, inner);
if self.write {
let mut iter = inner.tx.raw_iterator_opt(ro);
match dir {
Direction::Forward => iter.seek(&rng.start),
Direction::Backward => iter.seek_for_prev(&rng.end),
}
consume_keys(&mut iter, limit, skip, dir)
}
else {
drop(guard);
let mut iter = self.db.raw_iterator_opt(ro);
match dir {
Direction::Forward => iter.seek(&rng.start),
Direction::Backward => iter.seek_for_prev(&rng.end),
}
consume_keys(&mut iter, limit, skip, dir)
}
}
fn scan_blocking(
&self,
rng: Range<Key>,
limit: u32,
skip: u32,
version: Option<u64>,
dir: Direction,
guard: MutexGuard<'_, Option<TransactionInner>>,
) -> Result<ScanResult> {
let inner =
guard.as_ref().ok_or_else(|| Error::Internal("expected a transaction".into()))?;
let ro = self.scan_read_options(&rng, version, inner);
if self.write {
let mut iter = inner.tx.raw_iterator_opt(ro);
match dir {
Direction::Forward => iter.seek(&rng.start),
Direction::Backward => iter.seek_for_prev(&rng.end),
}
consume_vals(&mut iter, limit, skip, dir)
}
else {
drop(guard);
let mut iter = self.db.raw_iterator_opt(ro);
match dir {
Direction::Forward => iter.seek(&rng.start),
Direction::Backward => iter.seek_for_prev(&rng.end),
}
consume_vals(&mut iter, limit, skip, dir)
}
}
fn build_scan_iter(
&self,
rng: &Range<Key>,
version: Option<u64>,
inner: &TransactionInner,
) -> ScanIter {
let ro = self.scan_read_options(rng, version, inner);
if self.write {
let iter = inner.tx.raw_iterator_opt(ro);
let iter: DBRawIteratorWithThreadMode<
'static,
rocksdb::Transaction<'static, OptimisticTransactionDB>,
> = unsafe {
std::mem::transmute::<
DBRawIteratorWithThreadMode<
'_,
rocksdb::Transaction<'static, OptimisticTransactionDB>,
>,
DBRawIteratorWithThreadMode<
'static,
rocksdb::Transaction<'static, OptimisticTransactionDB>,
>,
>(iter)
};
ScanIter::Tx(iter)
} else {
let iter = self.db.raw_iterator_opt(ro);
let iter: DBRawIteratorWithThreadMode<'static, OptimisticTransactionDB> = unsafe {
std::mem::transmute::<
DBRawIteratorWithThreadMode<'_, OptimisticTransactionDB>,
DBRawIteratorWithThreadMode<'static, OptimisticTransactionDB>,
>(iter)
};
ScanIter::Db(iter)
}
}
}
async fn drain_cursors(tx: &Transaction) {
if tx.cursors_alive.load(Ordering::SeqCst) == 0 {
return;
}
let started = Instant::now();
let mut warned = false;
let mut escalated = false;
let mut yields: u32 = 0;
while tx.cursors_alive.load(Ordering::SeqCst) > 0 {
tokio::task::yield_now().await;
yields = yields.wrapping_add(1);
if yields & 1023 == 0 {
let elapsed = started.elapsed();
if !warned && elapsed >= DRAIN_STUCK_WARN_AFTER {
let alive = tx.cursors_alive.load(Ordering::SeqCst);
warn!(
target: TARGET,
"drain_cursors waiting unusually long: {alive} cursor(s) still alive after {elapsed:?}; \
possible stuck task holding a cursor handle past commit/cancel"
);
warned = true;
}
if !escalated && elapsed >= DRAIN_STUCK_WARN_AFTER * 10 {
let alive = tx.cursors_alive.load(Ordering::SeqCst);
warn!(
target: TARGET,
"drain_cursors still blocked: {alive} cursor(s) alive after {elapsed:?}; \
commit/cancel will not progress until every cursor handle is dropped"
);
escalated = true;
}
}
}
}
const DRAIN_STUCK_WARN_AFTER: Duration = Duration::from_secs(5);
fn seek_and_skip(
iter: &mut ScanIter,
dir: Direction,
started: &mut bool,
skip: &mut u32,
start: &Key,
end: &Key,
) -> Result<bool> {
if *started {
return Ok(true);
}
match (&mut *iter, dir) {
(ScanIter::Db(it), Direction::Forward) => it.seek(start),
(ScanIter::Db(it), Direction::Backward) => it.seek_for_prev(end),
(ScanIter::Tx(it), Direction::Forward) => it.seek(start),
(ScanIter::Tx(it), Direction::Backward) => it.seek_for_prev(end),
}
*started = true;
let n = std::mem::take(skip);
for _ in 0..n {
let still_valid = match &mut *iter {
ScanIter::Db(it) => it.valid().then(|| match dir {
Direction::Forward => it.next(),
Direction::Backward => it.prev(),
}),
ScanIter::Tx(it) => it.valid().then(|| match dir {
Direction::Forward => it.next(),
Direction::Backward => it.prev(),
}),
};
if still_valid.is_none() {
match &*iter {
ScanIter::Db(it) => it.status()?,
ScanIter::Tx(it) => it.status()?,
}
return Ok(false);
}
}
Ok(true)
}
fn fill_keys_into_state(state: &mut ScanStateKeys, limit: u32) -> Result<u64> {
if !seek_and_skip(
&mut state.iter,
state.dir,
&mut state.started,
&mut state.skip,
&state.start,
&state.end,
)? {
return Ok(0);
}
match &mut state.iter {
ScanIter::Db(iter) => {
fill_keys_inner(iter, limit, state.dir, &mut state.key_buf, &mut state.key_spans)
}
ScanIter::Tx(iter) => {
fill_keys_inner(iter, limit, state.dir, &mut state.key_buf, &mut state.key_spans)
}
}
}
fn fill_vals_into_state(state: &mut ScanStateVals, limit: u32) -> Result<(u64, u64)> {
if !seek_and_skip(
&mut state.iter,
state.dir,
&mut state.started,
&mut state.skip,
&state.start,
&state.end,
)? {
return Ok((0, 0));
}
match &mut state.iter {
ScanIter::Db(iter) => fill_vals_inner(
iter,
limit,
state.dir,
&mut state.key_buf,
&mut state.val_buf,
&mut state.spans,
),
ScanIter::Tx(iter) => fill_vals_inner(
iter,
limit,
state.dir,
&mut state.key_buf,
&mut state.val_buf,
&mut state.spans,
),
}
}
#[inline]
fn reserve_for_keys(limit: u32, key_buf: &mut Vec<u8>, key_spans: &mut Vec<KeySpan>) {
let count = limit as usize;
let bytes = count.saturating_mul(ESTIMATED_BYTES_PER_KEY as usize);
let need_buf = bytes.saturating_sub(key_buf.capacity());
if need_buf > 0 {
key_buf.reserve(need_buf);
}
let need_spans = count.saturating_sub(key_spans.capacity());
if need_spans > 0 {
key_spans.reserve(need_spans);
}
}
#[inline]
fn reserve_for_vals(
limit: u32,
key_buf: &mut Vec<u8>,
val_buf: &mut Vec<u8>,
spans: &mut Vec<KeyValSpan>,
) {
let count = limit as usize;
let bytes = count.saturating_mul(ESTIMATED_BYTES_PER_KV as usize);
let half = bytes / 2;
let need_kbuf = half.saturating_sub(key_buf.capacity());
if need_kbuf > 0 {
key_buf.reserve(need_kbuf);
}
let need_vbuf = half.saturating_sub(val_buf.capacity());
if need_vbuf > 0 {
val_buf.reserve(need_vbuf);
}
let need_spans = count.saturating_sub(spans.capacity());
if need_spans > 0 {
spans.reserve(need_spans);
}
}
fn fill_keys_inner<D: rocksdb::DBAccess>(
iter: &mut rocksdb::DBRawIteratorWithThreadMode<'_, D>,
limit: u32,
dir: Direction,
key_buf: &mut Vec<u8>,
key_spans: &mut Vec<KeySpan>,
) -> Result<u64> {
let mut key_bytes: u64 = 0;
reserve_for_keys(limit, key_buf, key_spans);
let push_key = |k: &[u8], key_buf: &mut Vec<u8>, key_spans: &mut Vec<KeySpan>| {
let offset = key_buf.len();
let len = k.len();
key_buf.extend_from_slice(k);
key_spans.push(KeySpan {
offset,
len,
});
};
let mut count = 0u64;
while count < limit as u64 {
let Some(k) = iter.key() else {
break;
};
push_key(k, key_buf, key_spans);
key_bytes += k.len() as u64;
count += 1;
match dir {
Direction::Forward => iter.next(),
Direction::Backward => iter.prev(),
};
}
iter.status()?;
Ok(key_bytes)
}
fn fill_vals_inner<D: rocksdb::DBAccess>(
iter: &mut rocksdb::DBRawIteratorWithThreadMode<'_, D>,
limit: u32,
dir: Direction,
key_buf: &mut Vec<u8>,
val_buf: &mut Vec<u8>,
spans: &mut Vec<KeyValSpan>,
) -> Result<(u64, u64)> {
let mut key_bytes: u64 = 0;
let mut value_bytes: u64 = 0;
reserve_for_vals(limit, key_buf, val_buf, spans);
let push_pair = |k: &[u8],
v: &[u8],
key_buf: &mut Vec<u8>,
val_buf: &mut Vec<u8>,
spans: &mut Vec<KeyValSpan>| {
let key_offset = key_buf.len();
let key_len = k.len();
key_buf.extend_from_slice(k);
let val_offset = val_buf.len();
let val_len = v.len();
val_buf.extend_from_slice(v);
spans.push(KeyValSpan {
key_offset,
key_len,
val_offset,
val_len,
});
};
let mut count = 0u64;
while count < limit as u64 {
let Some((k, v)) = iter.item() else {
break;
};
push_pair(k, v, key_buf, val_buf, spans);
key_bytes += k.len() as u64;
value_bytes += v.len() as u64;
count += 1;
match dir {
Direction::Forward => iter.next(),
Direction::Backward => iter.prev(),
};
}
iter.status()?;
Ok((key_bytes, value_bytes))
}
pub(in crate::kvs::rocksdb) async fn cursor_next_keys<'s>(
cursor: &'s mut RocksDbKeysCursor<'_>,
limit: u32,
) -> Result<KeysBatch<'s>> {
if cursor.tx.done.load(Ordering::Relaxed) {
return Err(Error::TransactionFinished);
}
cursor.state.key_buf.clear();
cursor.state.key_spans.clear();
let state: &mut ScanStateKeys = &mut cursor.state;
let key_bytes = cursor
.tx
.inline_guard
.try_inline_or_offload(move || -> Result<u64> { fill_keys_into_state(state, limit) })
.await?;
Ok(KeysBatch::from_parts(&cursor.state.key_buf, &cursor.state.key_spans, key_bytes))
}
pub(in crate::kvs::rocksdb) async fn cursor_next_vals<'s>(
cursor: &'s mut RocksDbValsCursor<'_>,
limit: u32,
) -> Result<ValsBatch<'s>> {
if cursor.tx.done.load(Ordering::Relaxed) {
return Err(Error::TransactionFinished);
}
cursor.state.key_buf.clear();
cursor.state.val_buf.clear();
cursor.state.spans.clear();
let state: &mut ScanStateVals = &mut cursor.state;
let (key_bytes, value_bytes) = cursor
.tx
.inline_guard
.try_inline_or_offload(move || -> Result<(u64, u64)> { fill_vals_into_state(state, limit) })
.await?;
Ok(ValsBatch::from_parts(
&cursor.state.key_buf,
&cursor.state.val_buf,
&cursor.state.spans,
key_bytes,
value_bytes,
))
}
fn visit_vals_inner<D: rocksdb::DBAccess>(
iter: &mut rocksdb::DBRawIteratorWithThreadMode<'_, D>,
limit: u32,
dir: Direction,
f: &mut dyn ValVisitor,
) -> Result<ScanChunkStats> {
let mut stats = ScanChunkStats::default();
while stats.rows < limit as u64 {
let Some((k, v)) = iter.item() else {
break;
};
let flow = f(k, v)?;
stats.rows += 1;
stats.key_bytes += k.len() as u64;
stats.value_bytes += v.len() as u64;
match dir {
Direction::Forward => iter.next(),
Direction::Backward => iter.prev(),
};
if let std::ops::ControlFlow::Break(()) = flow {
break;
}
}
iter.status()?;
Ok(stats)
}
fn visit_vals_into_state(
state: &mut ScanStateVals,
limit: u32,
f: &mut dyn ValVisitor,
) -> Result<ScanChunkStats> {
if !seek_and_skip(
&mut state.iter,
state.dir,
&mut state.started,
&mut state.skip,
&state.start,
&state.end,
)? {
return Ok(ScanChunkStats::default());
}
match &mut state.iter {
ScanIter::Db(iter) => visit_vals_inner(iter, limit, state.dir, f),
ScanIter::Tx(iter) => visit_vals_inner(iter, limit, state.dir, f),
}
}
pub(in crate::kvs::rocksdb) async fn cursor_for_each_vals(
cursor: &mut RocksDbValsCursor<'_>,
limit: u32,
f: &mut dyn ValVisitor,
) -> Result<ScanChunkStats> {
if cursor.tx.done.load(Ordering::Relaxed) {
return Err(Error::TransactionFinished);
}
let state: &mut ScanStateVals = &mut cursor.state;
cursor
.tx
.inline_guard
.try_inline_or_offload(move || visit_vals_into_state(state, limit, f))
.await
}
fn visit_keys_inner<D: rocksdb::DBAccess>(
iter: &mut rocksdb::DBRawIteratorWithThreadMode<'_, D>,
limit: u32,
dir: Direction,
f: &mut dyn KeyVisitor,
) -> Result<ScanChunkStats> {
let mut stats = ScanChunkStats::default();
while stats.rows < limit as u64 {
let Some(k) = iter.key() else {
break;
};
let flow = f(k)?;
stats.rows += 1;
stats.key_bytes += k.len() as u64;
match dir {
Direction::Forward => iter.next(),
Direction::Backward => iter.prev(),
};
if let std::ops::ControlFlow::Break(()) = flow {
break;
}
}
iter.status()?;
Ok(stats)
}
fn visit_keys_into_state(
state: &mut ScanStateKeys,
limit: u32,
f: &mut dyn KeyVisitor,
) -> Result<ScanChunkStats> {
if !seek_and_skip(
&mut state.iter,
state.dir,
&mut state.started,
&mut state.skip,
&state.start,
&state.end,
)? {
return Ok(ScanChunkStats::default());
}
match &mut state.iter {
ScanIter::Db(iter) => visit_keys_inner(iter, limit, state.dir, f),
ScanIter::Tx(iter) => visit_keys_inner(iter, limit, state.dir, f),
}
}
pub(in crate::kvs::rocksdb) async fn cursor_for_each_keys(
cursor: &mut RocksDbKeysCursor<'_>,
limit: u32,
f: &mut dyn KeyVisitor,
) -> Result<ScanChunkStats> {
if cursor.tx.done.load(Ordering::Relaxed) {
return Err(Error::TransactionFinished);
}
let state: &mut ScanStateKeys = &mut cursor.state;
cursor
.tx
.inline_guard
.try_inline_or_offload(move || visit_keys_into_state(state, limit, f))
.await
}
impl Transactable for Transaction {
fn kind(&self) -> &'static str {
"rocksdb"
}
fn closed(&self) -> bool {
self.done.load(Ordering::Relaxed)
}
fn writeable(&self) -> bool {
self.write
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self))]
fn cancel(&self) -> BoxFut<'_, Result<()>> {
Box::pin(async move {
if self.done.swap(true, Ordering::SeqCst) {
return Err(Error::TransactionFinished);
}
drain_cursors(self).await;
let guard = self.inner.lock().await;
let inner =
guard.as_ref().ok_or_else(|| Error::Internal("expected a transaction".into()))?;
inner.tx.rollback()?;
Ok(())
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self))]
fn commit(&self) -> BoxFut<'_, Result<()>> {
Box::pin(async move {
if self.done.swap(true, Ordering::SeqCst) {
return Err(Error::TransactionFinished);
}
if !self.writeable() {
return Err(Error::TransactionReadonly);
}
if self.is_restricted(true) && self.contains_writes() {
return Err(Error::ReadAndDeleteOnly);
}
drain_cursors(self).await;
let TransactionInner {
tx: inner,
snapshot,
} = self
.inner
.lock()
.await
.take()
.ok_or_else(|| Error::Internal("expected a transaction".into()))?;
drop(snapshot);
if self.versioned {
let ts = HlcTimeStamp::next();
inner.set_commit_timestamp(ts.0);
}
self.inline_guard.try_inline_or_offload(move || (*inner).commit()).await?;
if let Some(coordinator) = &self.commit_coordinator {
coordinator.wait_for_sync().await?;
}
if self.is_restricted(true) && self.contains_deletes() {
self.compact(None).await?;
}
Ok(())
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(key = key.sprint()))]
fn exists(&self, key: Key, version: Option<u64>) -> BoxFut<'_, Result<bool>> {
Box::pin(async move {
self.ensure_versioned(version)?;
if self.closed() {
return Err(Error::TransactionFinished);
}
self.run_blocking(move |guard| {
let inner = guard
.as_ref()
.ok_or_else(|| Error::Internal("expected a transaction".into()))?;
let res = if version.is_some() {
inner.tx.get_pinned_opt(key, &self.versioned_read_options(version, inner))
} else {
inner.tx.get_pinned_opt(key, &self.read_options)
}?
.is_some();
Ok(res)
})
.await
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(key = key.sprint()))]
fn get(&self, key: Key, version: Option<u64>) -> BoxFut<'_, Result<Option<Val>>> {
Box::pin(async move {
self.ensure_versioned(version)?;
if self.closed() {
return Err(Error::TransactionFinished);
}
self.run_blocking(move |guard| {
let inner = guard
.as_ref()
.ok_or_else(|| Error::Internal("expected a transaction".into()))?;
let res = if version.is_some() {
inner.tx.get_opt(key, &self.versioned_read_options(version, inner))
} else {
inner.tx.get_opt(key, &self.read_options)
}?;
Ok(res)
})
.await
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(keys = keys.sprint()))]
fn getm(&self, keys: Vec<Key>, version: Option<u64>) -> BoxFut<'_, Result<GetMultiResult>> {
Box::pin(async move {
self.ensure_versioned(version)?;
if self.closed() {
return Err(Error::TransactionFinished);
}
self.run_blocking(move |guard| {
let inner = guard
.as_ref()
.ok_or_else(|| Error::Internal("expected a transaction".into()))?;
let res = if version.is_some() {
inner.tx.multi_get_opt(keys, &self.versioned_read_options(version, inner))
} else {
inner.tx.multi_get_opt(keys, &self.read_options)
};
let mut records = 0u64;
let mut value_bytes = 0u64;
let values = res
.into_iter()
.map(|r| match r {
Ok(Some(v)) => {
records += 1;
value_bytes += v.len() as u64;
Ok(Some(v))
}
Ok(None) => Ok(None),
Err(e) => Err(e.into()),
})
.collect::<Result<Vec<Option<Val>>>>()?;
Ok(GetMultiResult {
values,
records,
value_bytes,
})
})
.await
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(key = key.sprint()))]
fn set(&self, key: Key, val: Val) -> BoxFut<'_, Result<()>> {
Box::pin(async move {
if self.closed() {
return Err(Error::TransactionFinished);
}
if !self.writeable() {
return Err(Error::TransactionReadonly);
}
if self.is_restricted(false) {
return Err(Error::ReadAndDeleteOnly);
}
let guard = self.inner.lock().await;
let inner =
guard.as_ref().ok_or_else(|| Error::Internal("expected a transaction".into()))?;
inner.tx.put(key, val)?;
self.store_writes();
Ok(())
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(key = key.sprint()))]
fn put(&self, key: Key, val: Val) -> BoxFut<'_, Result<()>> {
Box::pin(async move {
if self.closed() {
return Err(Error::TransactionFinished);
}
if !self.writeable() {
return Err(Error::TransactionReadonly);
}
if self.is_restricted(false) {
return Err(Error::ReadAndDeleteOnly);
}
self.run_blocking(move |guard| {
let inner = guard
.as_ref()
.ok_or_else(|| Error::Internal("expected a transaction".into()))?;
match inner.tx.get_pinned_opt(&key, &self.read_options)? {
None => inner.tx.put(key, val)?,
_ => return Err(Error::TransactionKeyAlreadyExists),
};
self.store_writes();
Ok(())
})
.await
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(key = key.sprint()))]
fn putc(&self, key: Key, val: Val, chk: Option<Val>) -> BoxFut<'_, Result<()>> {
Box::pin(async move {
if self.closed() {
return Err(Error::TransactionFinished);
}
if !self.writeable() {
return Err(Error::TransactionReadonly);
}
if self.is_restricted(false) {
return Err(Error::ReadAndDeleteOnly);
}
self.run_blocking(move |guard| {
let inner = guard
.as_ref()
.ok_or_else(|| Error::Internal("expected a transaction".into()))?;
match (inner.tx.get_pinned_opt(&key, &self.read_options)?, chk) {
(Some(v), Some(w)) if v.eq(&w) => inner.tx.put(key, val)?,
(None, None) => inner.tx.put(key, val)?,
_ => return Err(Error::TransactionConditionNotMet),
};
self.store_writes();
Ok(())
})
.await
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(key = key.sprint()))]
fn del(&self, key: Key) -> BoxFut<'_, Result<()>> {
Box::pin(async move {
if self.closed() {
return Err(Error::TransactionFinished);
}
if !self.writeable() {
return Err(Error::TransactionReadonly);
}
let guard = self.inner.lock().await;
let inner =
guard.as_ref().ok_or_else(|| Error::Internal("expected a transaction".into()))?;
inner.tx.delete(key)?;
self.store_deletes();
Ok(())
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(key = key.sprint()))]
fn delc(&self, key: Key, chk: Option<Val>) -> BoxFut<'_, Result<()>> {
Box::pin(async move {
if self.closed() {
return Err(Error::TransactionFinished);
}
if !self.writeable() {
return Err(Error::TransactionReadonly);
}
self.run_blocking(move |guard| {
let inner = guard
.as_ref()
.ok_or_else(|| Error::Internal("expected a transaction".into()))?;
match (inner.tx.get_pinned_opt(&key, &self.read_options)?, chk) {
(Some(v), Some(w)) if v.eq(&w) => inner.tx.delete(key)?,
(None, None) => inner.tx.delete(key)?,
_ => return Err(Error::TransactionConditionNotMet),
};
self.store_deletes();
Ok(())
})
.await
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(rng = rng.sprint()))]
fn count(&self, rng: Range<Key>, version: Option<u64>) -> BoxFut<'_, Result<usize>> {
Box::pin(async move {
self.ensure_versioned(version)?;
if self.closed() {
return Err(Error::TransactionFinished);
}
if self.write {
let guard = self.inner.lock().await;
return affinitypool::spawn_local(move || {
self.count_blocking(rng, version, guard)
})
.await;
}
let desired = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(8)
.min(COUNT_PARALLEL_MAX_SHARDS);
let sub_ranges = shard_range(&rng.start, &rng.end, desired);
if sub_ranges.len() <= 1 {
let guard = self.inner.lock().await;
return affinitypool::spawn_local(move || {
self.count_blocking(rng, version, guard)
})
.await;
}
let scans = {
let guard = self.inner.lock().await;
let inner = guard
.as_ref()
.ok_or_else(|| Error::Internal("expected a transaction".into()))?;
sub_ranges
.into_iter()
.map(|(lo, hi)| {
let sub_rng = lo..hi;
let ro = self.count_read_options(&sub_rng, version, inner);
(sub_rng, ro)
})
.collect::<Vec<_>>()
};
let mut tasks = Vec::with_capacity(scans.len());
for (sub_rng, ro) in scans {
let db = self.db.clone();
tasks.push(affinitypool::spawn_local(move || -> Result<usize> {
let mut iter = db.raw_iterator_opt(ro);
iter.seek(&sub_rng.start);
let mut res: usize = 0;
while iter.valid() {
res += 1;
iter.next();
}
iter.status()?;
Ok(res)
}));
}
let counts = futures::future::try_join_all(tasks).await?;
Ok(counts.into_iter().sum())
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(rng = rng.sprint()))]
fn keys(
&self,
rng: Range<Key>,
limit: u32,
skip: u32,
version: Option<u64>,
) -> BoxFut<'_, Result<KeysResult>> {
Box::pin(async move {
self.ensure_versioned(version)?;
if self.closed() {
return Err(Error::TransactionFinished);
}
if self.write {
let guard = self.inner.lock().await;
return affinitypool::spawn_local(move || {
self.keys_blocking(rng, limit, skip, version, Direction::Forward, guard)
})
.await;
}
self.run_blocking(move |guard| {
self.keys_blocking(rng, limit, skip, version, Direction::Forward, guard)
})
.await
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(rng = rng.sprint()))]
fn keysr(
&self,
rng: Range<Key>,
limit: u32,
skip: u32,
version: Option<u64>,
) -> BoxFut<'_, Result<KeysResult>> {
Box::pin(async move {
self.ensure_versioned(version)?;
if self.closed() {
return Err(Error::TransactionFinished);
}
if self.write {
let guard = self.inner.lock().await;
return affinitypool::spawn_local(move || {
self.keys_blocking(rng, limit, skip, version, Direction::Backward, guard)
})
.await;
}
self.run_blocking(move |guard| {
self.keys_blocking(rng, limit, skip, version, Direction::Backward, guard)
})
.await
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(rng = rng.sprint()))]
fn scan(
&self,
rng: Range<Key>,
limit: u32,
skip: u32,
version: Option<u64>,
) -> BoxFut<'_, Result<ScanResult>> {
Box::pin(async move {
self.ensure_versioned(version)?;
if self.closed() {
return Err(Error::TransactionFinished);
}
if self.write {
let guard = self.inner.lock().await;
return affinitypool::spawn_local(move || {
self.scan_blocking(rng, limit, skip, version, Direction::Forward, guard)
})
.await;
}
self.run_blocking(move |guard| {
self.scan_blocking(rng, limit, skip, version, Direction::Forward, guard)
})
.await
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(rng = rng.sprint()))]
fn scanr(
&self,
rng: Range<Key>,
limit: u32,
skip: u32,
version: Option<u64>,
) -> BoxFut<'_, Result<ScanResult>> {
Box::pin(async move {
self.ensure_versioned(version)?;
if self.closed() {
return Err(Error::TransactionFinished);
}
if self.write {
let guard = self.inner.lock().await;
return affinitypool::spawn_local(move || {
self.scan_blocking(rng, limit, skip, version, Direction::Backward, guard)
})
.await;
}
self.run_blocking(move |guard| {
self.scan_blocking(rng, limit, skip, version, Direction::Backward, guard)
})
.await
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(rng = rng.sprint()))]
fn open_keys_cursor<'a>(
&'a self,
rng: Range<Key>,
dir: Direction,
skip: u32,
version: Option<u64>,
) -> BoxFut<'a, Result<Box<dyn ScanCursorKeys + 'a>>> {
Box::pin(async move {
self.ensure_versioned(version)?;
self.cursors_alive.fetch_add(1, Ordering::SeqCst);
let alive_guard = AliveGuard::new(self);
if self.done.load(Ordering::SeqCst) {
return Err(Error::TransactionFinished);
}
let guard = self.inner.lock().await;
let inner =
guard.as_ref().ok_or_else(|| Error::Internal("expected a transaction".into()))?;
let iter = self.build_scan_iter(&rng, version, inner);
drop(guard);
Ok(Box::new(RocksDbKeysCursor {
tx: self,
state: ScanStateKeys {
iter,
dir,
started: false,
skip,
start: rng.start,
end: rng.end,
key_buf: Vec::new(),
key_spans: Vec::new(),
},
_alive_guard: alive_guard,
}) as Box<dyn ScanCursorKeys + 'a>)
})
}
#[instrument(level = "trace", target = "surrealdb::core::kvs::api", skip(self), fields(rng = rng.sprint()))]
fn open_vals_cursor<'a>(
&'a self,
rng: Range<Key>,
dir: Direction,
skip: u32,
version: Option<u64>,
) -> BoxFut<'a, Result<Box<dyn ScanCursorVals + 'a>>> {
Box::pin(async move {
self.ensure_versioned(version)?;
self.cursors_alive.fetch_add(1, Ordering::SeqCst);
let alive_guard = AliveGuard::new(self);
if self.done.load(Ordering::SeqCst) {
return Err(Error::TransactionFinished);
}
let guard = self.inner.lock().await;
let inner =
guard.as_ref().ok_or_else(|| Error::Internal("expected a transaction".into()))?;
let iter = self.build_scan_iter(&rng, version, inner);
drop(guard);
Ok(Box::new(RocksDbValsCursor {
tx: self,
state: ScanStateVals {
iter,
dir,
started: false,
skip,
start: rng.start,
end: rng.end,
key_buf: Vec::new(),
val_buf: Vec::new(),
spans: Vec::new(),
},
_alive_guard: alive_guard,
}) as Box<dyn ScanCursorVals + 'a>)
})
}
fn new_save_point(&self) -> BoxFut<'_, Result<()>> {
Box::pin(async move {
let guard = self.inner.lock().await;
if let Some(state) = guard.as_ref() {
state.tx.set_savepoint();
}
Ok(())
})
}
fn rollback_to_save_point(&self) -> BoxFut<'_, Result<()>> {
Box::pin(async move {
let guard = self.inner.lock().await;
if let Some(state) = guard.as_ref() {
state.tx.rollback_to_savepoint()?;
}
Ok(())
})
}
fn release_last_save_point(&self) -> BoxFut<'_, Result<()>> {
Box::pin(async move { Ok(()) })
}
fn compact(&self, range: Option<Range<Key>>) -> BoxFut<'_, anyhow::Result<()>> {
Box::pin(async move {
let mut fopts = FlushOptions::default();
fopts.set_wait(true);
let mut copts = CompactOptions::default();
copts.set_exclusive_manual_compaction(true);
copts.set_change_level(true);
copts.set_target_level(6);
copts.set_bottommost_level_compaction(BottommostLevelCompaction::Force);
affinitypool::spawn_local(move || {
self.db.flush_wal(true)?;
self.db.flush_opt(&fopts)?;
let (start, end) = match range {
Some(r) => (Some(r.start), Some(r.end)),
None => (None, None),
};
self.db.compact_range_opt(start, end, &copts);
Ok(())
})
.await
})
}
}
fn consume_keys<D: rocksdb::DBAccess>(
iter: &mut rocksdb::DBRawIteratorWithThreadMode<'_, D>,
limit: u32,
skip: u32,
dir: Direction,
) -> Result<KeysResult> {
for _ in 0..skip {
if iter.valid() {
match dir {
Direction::Forward => iter.next(),
Direction::Backward => iter.prev(),
}
} else {
iter.status()?;
return Ok(KeysResult::default());
}
}
let mut key_bytes = 0u64;
let mut keys = Vec::with_capacity(limit.min(4096) as usize);
while keys.len() < limit as usize {
if let Some(k) = iter.key() {
key_bytes += k.len() as u64;
keys.push(k.to_vec());
match dir {
Direction::Forward => iter.next(),
Direction::Backward => iter.prev(),
};
} else {
break;
}
}
iter.status()?;
Ok(KeysResult {
keys,
key_bytes,
})
}
fn consume_vals<D: rocksdb::DBAccess>(
iter: &mut rocksdb::DBRawIteratorWithThreadMode<'_, D>,
limit: u32,
skip: u32,
dir: Direction,
) -> Result<ScanResult> {
for _ in 0..skip {
if iter.valid() {
match dir {
Direction::Forward => iter.next(),
Direction::Backward => iter.prev(),
}
} else {
iter.status()?;
return Ok(ScanResult::default());
}
}
let mut key_bytes = 0u64;
let mut value_bytes = 0u64;
let mut values = Vec::with_capacity(limit.min(4096) as usize);
while values.len() < limit as usize {
if let Some((k, v)) = iter.item() {
key_bytes += k.len() as u64;
value_bytes += v.len() as u64;
values.push((k.to_vec(), v.to_vec()));
match dir {
Direction::Forward => iter.next(),
Direction::Backward => iter.prev(),
};
} else {
break;
}
}
iter.status()?;
Ok(ScanResult {
values,
key_bytes,
value_bytes,
})
}