use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Weak};
use pigeonhole_engine::{Engine, EngineShard};
use pigeonhole_format::Durability;
use pigeonhole_io::VfsRef;
use crate::table::TableCore;
use crate::{
Error, ErrorCode, Options, ReadTable, ReaderOptions, Result, TableBuilder, Transaction,
WriteBatch,
};
#[derive(Debug)]
pub(crate) struct Db {
pub(crate) engine: Arc<Engine>,
closed: AtomicBool,
pub(crate) max_value: usize,
}
impl Db {
fn new(engine: Arc<Engine>, max_value: usize) -> Arc<Self> {
Arc::new(Self {
engine,
closed: AtomicBool::new(false),
max_value,
})
}
#[inline]
pub(crate) fn check_open(&self) -> Result<()> {
if self.closed.load(Ordering::Acquire) {
Err(pigeonhole_engine::Error::Closed.into())
} else {
Ok(())
}
}
pub(crate) fn snapshot(self: &Arc<Self>) -> Result<Snapshot> {
self.check_open()?;
Ok(Snapshot {
inner: self.engine.snapshot()?,
db: Arc::downgrade(self),
})
}
#[inline]
pub(crate) fn check_snapshot(self: &Arc<Self>, snapshot: &Snapshot) -> Result<()> {
if std::ptr::eq(Weak::as_ptr(&snapshot.db), Arc::as_ptr(self)) {
Ok(())
} else {
Err(Error::new(
ErrorCode::InvalidArgument,
"the snapshot belongs to another database",
))
}
}
}
#[derive(Debug, Clone)]
pub struct Pigeonhole {
pub(crate) db: Arc<Db>,
}
impl Pigeonhole {
pub fn open(path: impl AsRef<Path>, options: Options) -> Result<Pigeonhole> {
let engine_options = options.to_engine();
let max_value = max_value(&engine_options);
let shm = ShmFootprint::of(&engine_options);
let engine = Engine::open(path.as_ref(), engine_options).map_err(|e| shm.explain(e))?;
Ok(Pigeonhole {
db: Db::new(engine, max_value),
})
}
pub fn open_reader(path: impl AsRef<Path>, options: ReaderOptions) -> Result<PigeonholeReader> {
let engine = Engine::open_reader(path.as_ref(), options.to_engine())?;
Ok(PigeonholeReader {
db: Db::new(engine, 0),
})
}
pub fn open_application_owned(
path: impl AsRef<Path>,
options: Options,
) -> Result<(Pigeonhole, Vec<Shard>)> {
let engine_options = options.to_engine();
let vfs = Arc::clone(&engine_options.vfs);
let max_value = max_value(&engine_options);
let shm = ShmFootprint::of(&engine_options);
let (engine, shards) = Engine::open_application_owned(path.as_ref(), engine_options)
.map_err(|e| shm.explain(e))?;
let shards = shards
.into_iter()
.map(|inner| Shard {
inner,
vfs: Arc::clone(&vfs),
})
.collect();
Ok((
Pigeonhole {
db: Db::new(engine, max_value),
},
shards,
))
}
pub fn table(&self, name: &str) -> Result<TableBuilder<'_>> {
Ok(TableBuilder::new(self, name))
}
pub fn tables(&self) -> Vec<String> {
self.db
.engine
.tables()
.iter()
.map(|t| t.name.clone())
.collect()
}
pub fn drop_table(&self, name: &str) -> Result<()> {
let info = self
.db
.engine
.table(name)
.ok_or_else(|| pigeonhole_engine::Error::TableNotFound(name.to_owned()))?;
Ok(self.db.engine.drop_table(info.id)?)
}
pub fn write_batch(&self) -> WriteBatch {
WriteBatch::new(Arc::clone(&self.db))
}
pub fn transaction(&self) -> Result<Transaction> {
self.db.check_open()?;
Ok(Transaction::new(
Arc::clone(&self.db),
self.db.engine.begin()?,
))
}
pub fn snapshot(&self) -> Result<Snapshot> {
self.db.snapshot()
}
#[doc(hidden)]
pub fn shard_stats(&self) -> Vec<crate::ShardStats> {
self.db.engine.shard_stats()
}
pub fn default_durability(&self) -> Durability {
self.db.engine.default_durability()
}
pub fn set_default_durability(&self, durability: Durability) {
self.db.engine.set_default_durability(durability);
}
pub fn flush(&self) -> Result<()> {
Ok(self.db.engine.flush()?)
}
pub fn compact(&self) -> Result<()> {
Ok(self.db.engine.compact(None)?)
}
pub fn backup(&self, dest: impl AsRef<Path>) -> Result<()> {
Ok(self.db.engine.backup(dest.as_ref())?)
}
pub fn shrink(&self) -> Result<u64> {
Ok(self.db.engine.shrink()?)
}
pub fn close(self) -> Result<()> {
self.db.closed.store(true, Ordering::Release);
Ok(self.db.engine.close()?)
}
}
#[derive(Debug, Clone)]
pub struct PigeonholeReader {
db: Arc<Db>,
}
impl PigeonholeReader {
pub fn table(&self, name: &str) -> Result<ReadTable> {
let info = self
.db
.engine
.table(name)
.ok_or_else(|| pigeonhole_engine::Error::TableNotFound(name.to_owned()))?;
Ok(ReadTable {
core: Arc::new(TableCore::new(Arc::clone(&self.db), info)),
})
}
pub fn tables(&self) -> Vec<String> {
self.db
.engine
.tables()
.iter()
.map(|t| t.name.clone())
.collect()
}
pub fn snapshot(&self) -> Result<Snapshot> {
self.db.snapshot()
}
}
#[derive(Debug, Clone)]
pub struct Snapshot {
pub(crate) inner: pigeonhole_engine::Snapshot,
db: Weak<Db>,
}
impl Snapshot {
pub fn seqno(&self) -> u64 {
self.inner.seqno()
}
}
#[derive(Debug)]
pub struct Shard {
inner: EngineShard,
vfs: VfsRef,
}
impl Shard {
pub fn index(&self) -> usize {
usize::from(self.inner.index())
}
pub fn run_once(&mut self, budget: std::time::Duration) -> bool {
let budget = u64::try_from(budget.as_nanos()).unwrap_or(u64::MAX);
let deadline = self.vfs.monotonic_nanos().saturating_add(budget);
self.inner.run_once(deadline)
}
pub fn next_wakeup(&self) -> Option<std::time::Duration> {
let deadline = self.inner.next_deadline()?;
let now = self.vfs.monotonic_nanos();
Some(std::time::Duration::from_nanos(
deadline.saturating_sub(now),
))
}
pub fn closed(&self) -> Option<Result<()>> {
self.inner.closed().map(|r| r.map_err(Into::into))
}
pub fn set_wakeup(&mut self, wake: Box<dyn Fn() + Send + Sync>) {
self.inner.set_wakeup(wake);
}
}
struct ShmFootprint {
shards: u64,
budget: u64,
region_len: u64,
dir: Option<std::path::PathBuf>,
}
impl ShmFootprint {
fn of(o: &pigeonhole_engine::EngineOptions) -> Self {
let shards = match o.shards {
0 => pigeonhole_io::sys::available_cpus().max(1),
n => n,
};
let count = u32::try_from(shards).unwrap_or(u32::MAX);
let config = pigeonhole_shm::ShmConfig::new(count);
let layout = pigeonhole_format::shm::ShmHeader::layout(
[0; 16],
count,
o.reader_slots.max(1),
config.view_buffer_bytes,
o.memtable_budget,
0,
0,
);
Self {
shards: shards as u64,
budget: o.memtable_budget,
region_len: layout.region_len,
dir: o.shm_dir.clone(),
}
}
fn explain(&self, e: pigeonhole_engine::Error) -> Error {
if !matches!(e, pigeonhole_engine::Error::ShmUnavailable) {
return e.into();
}
let arenas = self.shards.saturating_mul(self.budget);
let need = format!(
"{} ({} shards × {} memtable_budget, plus {} for views and reader slots)",
bytes(self.region_len),
self.shards,
bytes(self.budget),
bytes(self.region_len.saturating_sub(arenas)),
);
let fix = "or lower Options::memtable_budget or Options::shards";
let message = match &self.dir {
Some(dir) => format!(
"the shared-memory region could not be created in {}: it needs {need}. The \
directory must exist and have that much free space; free some, point \
Options::shm_dir at a larger one, {fix}",
dir.display()
),
None if cfg!(any(target_os = "linux", target_os = "android")) => format!(
"the shared-memory region could not be created in /dev/shm: it needs {need}, \
and /dev/shm is too small or missing (Docker and Kubernetes default it to 64 \
MiB). Enlarge it (docker run --shm-size; in Kubernetes, mount an emptyDir \
with medium: Memory at /dev/shm), point Options::shm_dir at a larger tmpfs, \
{fix}"
),
None => format!(
"the shared-memory region could not be created: it needs {need} of shared \
memory, more than the system would commit. Free memory, point \
Options::shm_dir at a directory with room, {fix}"
),
};
Error::new(ErrorCode::ShmUnavailable, message)
}
}
fn bytes(n: u64) -> String {
if n.is_multiple_of(1 << 20) {
format!("{} MiB", n >> 20)
} else {
format!("{} KiB", n.div_ceil(1 << 10))
}
}
fn max_value(o: &pigeonhole_engine::EngineOptions) -> usize {
(o.wal.segment_size as usize)
.saturating_sub(64 * 1024)
.min(64 << 20)
.min((o.memtable_budget / 2) as usize)
}
#[cfg(test)]
mod tests {
use super::*;
fn footprint(shards: usize, budget: u64, dir: Option<&str>) -> ShmFootprint {
let mut o = pigeonhole_engine::EngineOptions::new(pigeonhole_io::sim::SimVfs::new(1));
o.shards = shards;
o.memtable_budget = budget;
o.shm_dir = dir.map(Into::into);
ShmFootprint::of(&o)
}
#[test]
fn shm_unavailable_names_the_footprint_and_the_remedies() {
let e = footprint(4, 64 << 20, None).explain(pigeonhole_engine::Error::ShmUnavailable);
assert_eq!(e.code(), ErrorCode::ShmUnavailable);
let m = e.message();
assert!(
m.contains("266 MiB (4 shards × 64 MiB memtable_budget, plus 10 MiB for views"),
"{m}"
);
assert!(
m.contains("Options::memtable_budget or Options::shards"),
"{m}"
);
assert!(m.contains("Options::shm_dir"), "{m}");
if cfg!(target_os = "linux") {
assert!(m.contains("/dev/shm") && m.contains("--shm-size"), "{m}");
}
let e = footprint(3, 192 << 10, Some("/mnt/small"))
.explain(pigeonhole_engine::Error::ShmUnavailable);
assert_eq!(e.code(), ErrorCode::ShmUnavailable);
let m = e.message();
assert!(
m.contains("in /mnt/small: it needs 16 MiB (3 shards × 192 KiB memtable_budget"),
"{m}"
);
}
#[test]
fn other_open_errors_pass_through() {
let e = footprint(1, 1 << 20, None).explain(pigeonhole_engine::Error::WriterLocked);
assert_eq!(e.code(), ErrorCode::WriterLocked);
assert!(!e.message().contains("shared-memory region"));
}
}