use std::collections::{HashMap, VecDeque};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::ops::{Bound, Deref, RangeBounds};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use crossbeam_epoch::{self as epoch, Guard};
use crossbeam_skiplist::SkipMap;
use crossbeam_utils::CachePadded;
use memmap2::Mmap;
use parking_lot::{Mutex, MutexGuard, RwLock, RwLockReadGuard};
use crate::error::from_fsys;
use crate::storage::arc_cell::ArcCell;
use crate::storage::flush::FlushPolicy;
use crate::storage::format::{self, RecordView};
#[cfg(feature = "encrypt")]
use crate::storage::format::{OwnedRecord, NONCE_LEN};
use crate::storage::index::{Index, KeyCheck, KeyHash, KeyHasher};
use crate::storage::integrity;
use crate::storage::meta::{self, FLAG_ENCRYPTED};
#[cfg(feature = "encrypt")]
use crate::storage::meta::{MetaHeader, FLAG_CIPHER_CHACHA20};
use crate::storage::store::{
self, batch_payload_starts, remove_if_exists, sync_dir, Store, FSYS_MAX_PAYLOAD,
FSYS_PRE_PAYLOAD_BYTES,
};
use crate::{Error, Result};
pub(crate) const REWRITE_CHUNK_BYTES: usize = 4 << 20;
pub(crate) const DEFAULT_NAMESPACE_ID: u32 = 0;
pub(crate) struct NamespaceRuntime {
index: Index,
range_index: Option<Arc<SkipMap<Vec<u8>, u64>>>,
}
impl NamespaceRuntime {
fn new(range_scans_enabled: bool) -> Self {
Self {
index: Index::new(),
range_index: range_scans_enabled.then(|| Arc::new(SkipMap::new())),
}
}
}
impl std::fmt::Debug for NamespaceRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NamespaceRuntime")
.field("len", &self.index.len())
.finish()
}
}
enum NsRef<'a> {
Default(&'a NamespaceRuntime),
Named(Arc<NamespaceRuntime>),
}
impl Deref for NsRef<'_> {
type Target = NamespaceRuntime;
#[inline]
fn deref(&self) -> &NamespaceRuntime {
match self {
Self::Default(ns) => ns,
Self::Named(ns) => ns,
}
}
}
const WRITE_STRIPES: usize = 1024;
struct WriteStripes {
stripes: Box<[CachePadded<Mutex<()>>]>,
}
#[must_use = "the write gate and stripes are released when this guard drops"]
pub(crate) struct WriteGuard<'a> {
_stripes: StripeSet<'a>,
_gate: RwLockReadGuard<'a, ()>,
}
enum StripeSet<'a> {
One { _guard: MutexGuard<'a, ()> },
Many { _guards: Vec<MutexGuard<'a, ()>> },
}
impl WriteStripes {
fn new() -> Self {
Self {
stripes: (0..WRITE_STRIPES)
.map(|_| CachePadded::new(Mutex::new(())))
.collect(),
}
}
#[inline]
fn stripe_of(ns_id: u32, hash: KeyHash) -> usize {
let mixed = hash.rotate_right(24) ^ u64::from(ns_id).wrapping_mul(0x9e37_79b9_7f4a_7c15);
(mixed as usize) & (WRITE_STRIPES - 1)
}
fn lock_one(&self, ns_id: u32, hash: KeyHash) -> StripeSet<'_> {
StripeSet::One {
_guard: self.stripes[Self::stripe_of(ns_id, hash)].lock(),
}
}
fn lock_many(&self, ns_id: u32, hashes: &[KeyHash]) -> StripeSet<'_> {
let mut wanted = [0_u64; WRITE_STRIPES / 64];
for &hash in hashes {
let stripe = Self::stripe_of(ns_id, hash);
wanted[stripe / 64] |= 1 << (stripe % 64);
}
let mut guards = Vec::with_capacity(hashes.len().min(WRITE_STRIPES));
for (word_index, &word) in wanted.iter().enumerate() {
let mut bits = word;
while bits != 0 {
let bit = bits.trailing_zeros() as usize;
bits &= bits - 1;
guards.push(self.stripes[word_index * 64 + bit].lock());
}
}
StripeSet::Many { _guards: guards }
}
}
#[derive(Debug)]
pub(crate) enum BatchOp {
Insert {
key: Vec<u8>,
value: Vec<u8>,
expires_at: u64,
},
Remove {
key: Vec<u8>,
},
}
impl BatchOp {
fn key(&self) -> &[u8] {
match self {
Self::Insert { key, .. } | Self::Remove { key } => key,
}
}
}
enum Decoded<'a> {
Borrowed {
key: &'a [u8],
value: &'a [u8],
expires_at: u64,
},
#[cfg_attr(not(feature = "encrypt"), allow(dead_code))]
Owned {
key: Vec<u8>,
value: Vec<u8>,
expires_at: u64,
},
}
impl Decoded<'_> {
fn key(&self) -> &[u8] {
match self {
Self::Borrowed { key, .. } => key,
Self::Owned { key, .. } => key,
}
}
fn expires_at(&self) -> u64 {
match self {
Self::Borrowed { expires_at, .. } | Self::Owned { expires_at, .. } => *expires_at,
}
}
fn into_value(self) -> Vec<u8> {
match self {
Self::Borrowed { value, .. } => value.to_vec(),
Self::Owned { value, .. } => value,
}
}
fn into_triple(self) -> RecordSnapshot {
match self {
Self::Borrowed {
key,
value,
expires_at,
} => (key.to_vec(), value.to_vec(), expires_at),
Self::Owned {
key,
value,
expires_at,
} => (key, value, expires_at),
}
}
}
#[inline]
pub(crate) fn is_live(expires_at: u64, now_ms: u64) -> bool {
expires_at == 0 || now_ms == 0 || expires_at > now_ms
}
pub(crate) struct RangeCursor {
ns_id: u32,
map: Arc<SkipMap<Vec<u8>, u64>>,
generation: u64,
view: ReadView,
next: Bound<Vec<u8>>,
end: Bound<Vec<u8>>,
done: bool,
}
impl std::fmt::Debug for RangeCursor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RangeCursor")
.field("ns_id", &self.ns_id)
.field("generation", &self.generation)
.field("done", &self.done)
.finish_non_exhaustive()
}
}
impl RangeCursor {
pub(crate) fn view(&self) -> &ReadView {
&self.view
}
}
fn bound_as_slice(bound: &Bound<Vec<u8>>) -> Bound<&[u8]> {
match bound {
Bound::Included(v) => Bound::Included(v.as_slice()),
Bound::Excluded(v) => Bound::Excluded(v.as_slice()),
Bound::Unbounded => Bound::Unbounded,
}
}
fn owned_bound(bound: Bound<&Vec<u8>>) -> Bound<Vec<u8>> {
match bound {
Bound::Included(v) => Bound::Included(v.clone()),
Bound::Excluded(v) => Bound::Excluded(v.clone()),
Bound::Unbounded => Bound::Unbounded,
}
}
#[cfg(feature = "encrypt")]
pub(crate) type SharedEncryption = Option<Arc<crate::encryption::EncryptionContext>>;
#[derive(Clone)]
pub(crate) struct EngineConfig {
pub(crate) path: PathBuf,
pub(crate) flags: u32,
pub(crate) enable_range_scans: bool,
pub(crate) flush_policy: FlushPolicy,
pub(crate) iouring_sqpoll_idle_ms: Option<u32>,
#[cfg(feature = "encrypt")]
pub(crate) encryption_key: Option<crate::encryption::KeyBytes>,
#[cfg(feature = "encrypt")]
pub(crate) cipher: Option<crate::encryption::Cipher>,
#[cfg(feature = "encrypt")]
pub(crate) encryption_passphrase: Option<crate::encryption::Passphrase>,
}
impl std::fmt::Debug for EngineConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("EngineConfig");
let _ = s
.field("path", &self.path)
.field("flags", &self.flags)
.field("enable_range_scans", &self.enable_range_scans)
.field("flush_policy", &self.flush_policy)
.field("iouring_sqpoll_idle_ms", &self.iouring_sqpoll_idle_ms);
#[cfg(feature = "encrypt")]
{
let _ = s
.field(
"encryption_key",
&self.encryption_key.as_ref().map(|_| "<redacted>"),
)
.field("cipher", &self.cipher)
.field(
"encryption_passphrase",
&self.encryption_passphrase.as_ref().map(|_| "<redacted>"),
);
}
s.finish()
}
}
impl Default for EngineConfig {
fn default() -> Self {
Self {
path: PathBuf::new(),
flags: 0,
enable_range_scans: false,
flush_policy: FlushPolicy::default(),
iouring_sqpoll_idle_ms: None,
#[cfg(feature = "encrypt")]
encryption_key: None,
#[cfg(feature = "encrypt")]
cipher: None,
#[cfg(feature = "encrypt")]
encryption_passphrase: None,
}
}
}
pub(crate) struct Engine {
store: Arc<Store>,
write_gate: RwLock<()>,
default_ns: ArcCell<NamespaceRuntime>,
hasher: KeyHasher,
write_stripes: WriteStripes,
namespaces: RwLock<HashMap<u32, Arc<NamespaceRuntime>>>,
namespace_names: RwLock<HashMap<String, u32>>,
next_namespace_id: AtomicU64,
range_scans_enabled: bool,
#[cfg(feature = "encrypt")]
encryption: SharedEncryption,
}
impl std::fmt::Debug for Engine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Engine")
.field("store", &self.store)
.finish()
}
}
pub(crate) type RecordSnapshot = (Vec<u8>, Vec<u8>, u64);
pub(crate) type ReadView = Arc<Mmap>;
#[cfg(feature = "encrypt")]
type ResolvedEncryption = (
Option<crate::encryption::KeyBytes>,
Option<[u8; meta::META_SALT_LEN]>,
Option<crate::encryption::Cipher>,
);
enum RecoveryAction {
Insert { ns_id: u32, key: Vec<u8> },
Remove { ns_id: u32, key: Vec<u8> },
NamespaceName { ns_id: u32, name: Vec<u8> },
}
#[derive(Default)]
struct RecoveryState {
dropped: HashMap<u32, String>,
}
impl Engine {
pub(crate) fn open(config: EngineConfig) -> Result<Self> {
#[cfg(feature = "encrypt")]
let (resolved_key, fresh_salt, resolved_cipher) = Self::resolve_encryption(&config)?;
#[cfg(feature = "encrypt")]
let flags = {
let mut f = config.flags;
if resolved_key.is_some() {
f |= FLAG_ENCRYPTED;
if let Some(crate::encryption::Cipher::ChaCha20Poly1305) = resolved_cipher {
f |= FLAG_CIPHER_CHACHA20;
}
}
f
};
#[cfg(not(feature = "encrypt"))]
let flags = config.flags;
let store = Arc::new(Store::open_with_policy(
config.path.clone(),
flags,
config.flush_policy,
config.iouring_sqpoll_idle_ms,
)?);
let header = store.header()?;
#[cfg(feature = "encrypt")]
let encryption: SharedEncryption = match resolved_key {
None => None,
Some(key) => {
let cipher = resolved_cipher
.or_else(|| Some(Self::cipher_from_flags(header.flags)))
.unwrap_or(crate::encryption::Cipher::Aes256Gcm);
let ctx = crate::encryption::EncryptionContext::from_key_with_cipher(&key, cipher);
let arc = Arc::new(ctx);
Self::handle_verification(&store, &arc, fresh_salt, &header)?;
Some(arc)
}
};
#[cfg(not(feature = "encrypt"))]
if header.flags & FLAG_ENCRYPTED != 0 {
return Err(Error::InvalidConfig(
"this database was created with encryption; rebuild with the `encrypt` feature",
));
}
let range_scans_enabled = config.enable_range_scans;
let default_ns = Arc::new(NamespaceRuntime::new(range_scans_enabled));
let mut namespaces = HashMap::new();
let _none = namespaces.insert(DEFAULT_NAMESPACE_ID, Arc::clone(&default_ns));
let mut engine = Self {
store,
write_gate: RwLock::new(()),
default_ns: ArcCell::new(default_ns),
hasher: KeyHasher::random(),
write_stripes: WriteStripes::new(),
namespaces: RwLock::new(namespaces),
namespace_names: RwLock::new(HashMap::new()),
next_namespace_id: AtomicU64::new(1),
range_scans_enabled,
#[cfg(feature = "encrypt")]
encryption,
};
#[cfg(feature = "ttl")]
let expired = engine.recovery_scan()?;
#[cfg(not(feature = "ttl"))]
engine.recovery_scan()?;
Arc::get_mut(&mut engine.store)
.ok_or(Error::InvalidConfig(
"store shared before the open finished",
))?
.open_journal()?;
engine.store.finish_open()?;
engine.remove_stale_rewrite_files();
#[cfg(feature = "ttl")]
engine.sweep_expired_on_open(expired);
Ok(engine)
}
fn lock_key(&self, ns_id: u32, hash: KeyHash) -> WriteGuard<'_> {
let gate = self.write_gate.read();
WriteGuard {
_stripes: self.write_stripes.lock_one(ns_id, hash),
_gate: gate,
}
}
fn lock_keys(&self, ns_id: u32, hashes: &[KeyHash]) -> WriteGuard<'_> {
let gate = self.write_gate.read();
WriteGuard {
_stripes: self.write_stripes.lock_many(ns_id, hashes),
_gate: gate,
}
}
fn remove_stale_rewrite_files(&self) {
let tmp = compaction_temp_path(self.store.path());
let _ignored = remove_if_exists(&meta::meta_path_for(&tmp));
let _ignored = remove_if_exists(&tmp);
}
#[cfg(feature = "encrypt")]
fn resolve_encryption(config: &EngineConfig) -> Result<ResolvedEncryption> {
if config.encryption_key.is_some() && config.encryption_passphrase.is_some() {
return Err(Error::InvalidConfig(
"encryption_key and encryption_passphrase are mutually exclusive — pick one",
));
}
let peeked = peek_header(&config.path)?;
let keyed = config.encryption_key.is_some() || config.encryption_passphrase.is_some();
if !keyed {
if let Some(header) = peeked {
if header.flags & FLAG_ENCRYPTED != 0
|| header.encryption_verify != [0_u8; meta::META_VERIFY_LEN]
{
return Err(Error::InvalidConfig(
"this database was created with at-rest encryption; supply encryption_key or encryption_passphrase",
));
}
}
return Ok((None, None, None));
}
let has_verify_block =
peeked.is_some_and(|h| h.encryption_verify != [0_u8; meta::META_VERIFY_LEN]);
if !has_verify_block && journal_has_bytes(&config.path)? {
return Err(Error::InvalidConfig(
"this database is not encrypted (it has records but no encryption metadata); open it without a key, or convert it first with Emdb::enable_encryption",
));
}
let on_disk_cipher = peeked
.filter(|_| has_verify_block)
.map(|h| Self::cipher_from_flags(h.flags));
let cipher = match (config.cipher, on_disk_cipher) {
(Some(requested), Some(disk)) if requested != disk => {
return Err(Error::InvalidConfig(
"EmdbBuilder::cipher disagrees with the cipher this database was created with",
));
}
(Some(requested), _) => Some(requested),
(None, Some(disk)) => Some(disk),
(None, None) => None,
};
if let Some(passphrase) = config.encryption_passphrase.as_ref() {
let (salt, fresh) = match peeked {
Some(header) if has_verify_block => {
if header.encryption_salt == [0_u8; meta::META_SALT_LEN] {
return Err(Error::InvalidConfig(
"this database was created with a raw encryption_key; supply via encryption_key, not encryption_passphrase",
));
}
(header.encryption_salt, None)
}
_ => {
let s = crate::encryption::random_salt()?;
(s, Some(s))
}
};
let derived = crate::encryption::derive_key_from_passphrase(passphrase, &salt)?;
return Ok((Some(derived), fresh, cipher));
}
if let Some(key) = config.encryption_key.as_ref() {
if let Some(header) = peeked {
if has_verify_block && header.encryption_salt != [0_u8; meta::META_SALT_LEN] {
return Err(Error::InvalidConfig(
"this database was created with an encryption_passphrase; supply via encryption_passphrase, not encryption_key",
));
}
}
return Ok((Some(key.clone()), None, cipher));
}
Ok((None, None, None))
}
#[cfg(feature = "encrypt")]
fn cipher_from_flags(flags: u32) -> crate::encryption::Cipher {
if flags & FLAG_CIPHER_CHACHA20 != 0 {
crate::encryption::Cipher::ChaCha20Poly1305
} else {
crate::encryption::Cipher::Aes256Gcm
}
}
#[cfg(feature = "encrypt")]
fn handle_verification(
store: &Store,
ctx: &Arc<crate::encryption::EncryptionContext>,
fresh_salt: Option<[u8; meta::META_SALT_LEN]>,
existing_header: &MetaHeader,
) -> Result<()> {
if existing_header.encryption_verify == [0_u8; meta::META_VERIFY_LEN] {
if store.tail() != 0 {
return Err(Error::InvalidConfig(
"this database is not encrypted (it has records but no encryption metadata); open it without a key, or convert it first with Emdb::enable_encryption",
));
}
let salt = fresh_salt.unwrap_or([0_u8; meta::META_SALT_LEN]);
let nonce_then_ct = ctx.encrypt(crate::encryption::VERIFICATION_PLAINTEXT)?;
debug_assert_eq!(nonce_then_ct.len(), meta::META_VERIFY_LEN);
let mut verify = [0_u8; meta::META_VERIFY_LEN];
verify.copy_from_slice(&nonce_then_ct);
store.set_encryption_metadata(salt, verify)?;
return Ok(());
}
let plaintext = ctx.decrypt(&existing_header.encryption_verify)?;
if plaintext.as_slice() != crate::encryption::VERIFICATION_PLAINTEXT {
return Err(Error::EncryptionKeyMismatch);
}
Ok(())
}
fn recovery_scan(&self) -> Result<OpenExpired> {
let mut reader = self.store.open_reader()?;
let _advice = reader.advise_sequential();
let mut state = RecoveryState::default();
#[cfg(feature = "ttl")]
let mut expiry = ExpiryTracker::new(crate::ttl::now_unix_millis());
for record_result in reader.iter() {
let record = record_result.map_err(from_fsys)?;
let payload_start = record.lsn.as_u64() + FSYS_PRE_PAYLOAD_BYTES;
self.apply_recovered_payload(&record.payload, payload_start, &mut state)?;
#[cfg(feature = "ttl")]
expiry.observe(&record.payload);
}
if reader.tail_state() != fsys::JournalTailState::CleanEnd {
integrity::check_damage(
self.store.path(),
reader.position().as_u64(),
reader.file_size(),
)?;
}
#[cfg(feature = "ttl")]
return Ok(expiry.finish());
#[cfg(not(feature = "ttl"))]
Ok(())
}
fn apply_recovered_payload(
&self,
payload: &[u8],
payload_start: u64,
state: &mut RecoveryState,
) -> Result<()> {
if payload.is_empty() {
return Err(Error::Corrupted {
offset: payload_start,
reason: "empty record payload during recovery",
});
}
let tag = payload[0];
let encrypted = (tag & format::TAG_ENCRYPTED_FLAG) != 0;
#[cfg(feature = "encrypt")]
if !encrypted && self.encryption.is_some() {
return Err(Error::Corrupted {
offset: payload_start,
reason: "plaintext record in an encrypted database",
});
}
let action = if encrypted {
#[cfg(feature = "encrypt")]
{
let ctx = match self.encryption.as_ref() {
Some(c) => Arc::clone(c),
None => {
return Err(Error::InvalidConfig(
"encrypted record encountered while opening unencrypted database",
));
}
};
let owned = format::decode_payload_encrypted(payload, |nonce, ct| {
let mut input = Vec::with_capacity(NONCE_LEN + ct.len());
input.extend_from_slice(nonce);
input.extend_from_slice(ct);
ctx.decrypt(&input)
})
.map_err(|err| relocate_corruption(err, payload_start))?;
match owned {
OwnedRecord::Insert { ns_id, key, .. } => RecoveryAction::Insert { ns_id, key },
OwnedRecord::Remove { ns_id, key } => RecoveryAction::Remove { ns_id, key },
OwnedRecord::NamespaceName { ns_id, name } => {
RecoveryAction::NamespaceName { ns_id, name }
}
}
}
#[cfg(not(feature = "encrypt"))]
{
return Err(Error::InvalidConfig(
"encrypted record present but the `encrypt` feature is not compiled in",
));
}
} else {
match format::decode_payload(payload)
.map_err(|err| relocate_corruption(err, payload_start))?
{
RecordView::Insert { ns_id, key, .. } => RecoveryAction::Insert {
ns_id,
key: key.to_vec(),
},
RecordView::Remove { ns_id, key } => RecoveryAction::Remove {
ns_id,
key: key.to_vec(),
},
RecordView::NamespaceName { ns_id, name } => RecoveryAction::NamespaceName {
ns_id,
name: name.to_vec(),
},
}
};
self.apply_recovered_action(action, payload_start, state)
}
fn apply_recovered_action(
&self,
action: RecoveryAction,
offset: u64,
state: &mut RecoveryState,
) -> Result<()> {
match action {
RecoveryAction::Insert { ns_id, key } => {
let ns = self.recovered_namespace(ns_id, offset, state)?;
let key_hash = self.hasher.hash(&key);
self.index_insert(&ns, ns_id, key_hash, &key, offset)?;
}
RecoveryAction::Remove { ns_id, key } => {
let ns = self.recovered_namespace(ns_id, offset, state)?;
let key_hash = self.hasher.hash(&key);
self.index_remove(&ns, ns_id, key_hash, &key)?;
}
RecoveryAction::NamespaceName { ns_id, name } => {
if ns_id == DEFAULT_NAMESPACE_ID || ns_id == u32::MAX {
return Err(Error::Corrupted {
offset,
reason: "namespace-name record with a reserved id",
});
}
if name.is_empty() {
return self.unbind_recovered_namespace(ns_id, offset, state);
}
if state.dropped.contains_key(&ns_id) {
return Err(Error::Corrupted {
offset,
reason: "namespace-name record rebinds a dropped namespace id",
});
}
let name_str = match std::str::from_utf8(&name) {
Ok(s) => s.to_string(),
Err(_) => {
return Err(Error::Corrupted {
offset,
reason: "namespace-name record carried non-UTF-8 name",
});
}
};
let mut name_guard = self.namespace_names.write();
let already_bound = self.namespaces.read().contains_key(&ns_id);
if already_bound && name_guard.get(name_str.as_str()) != Some(&ns_id) {
return Err(Error::Corrupted {
offset,
reason: "namespace id bound to a second name",
});
}
let _ = self.ensure_namespace_runtime(ns_id)?;
let _existing = name_guard.insert(name_str, ns_id);
drop(name_guard);
}
}
Ok(())
}
fn recovered_namespace(
&self,
ns_id: u32,
offset: u64,
state: &mut RecoveryState,
) -> Result<Arc<NamespaceRuntime>> {
if let Some(ns) = self.namespaces.read().get(&ns_id) {
return Ok(Arc::clone(ns));
}
let Some(name) = state.dropped.remove(&ns_id) else {
return Err(Error::Corrupted {
offset,
reason: "record references a namespace id with no namespace-name binding",
});
};
let mut name_guard = self.namespace_names.write();
if name.is_empty() || name_guard.contains_key(name.as_str()) {
return Err(Error::Corrupted {
offset,
reason: "record references a dropped namespace whose name was reused",
});
}
let ns = self.ensure_namespace_runtime(ns_id)?;
let _previous = name_guard.insert(name, ns_id);
Ok(ns)
}
fn unbind_recovered_namespace(
&self,
ns_id: u32,
offset: u64,
state: &mut RecoveryState,
) -> Result<()> {
if self.namespaces.write().remove(&ns_id).is_none() {
return Err(Error::Corrupted {
offset,
reason: "namespace unbind record for an id that is not bound",
});
}
let mut name_guard = self.namespace_names.write();
let name = name_guard
.iter()
.find_map(|(name, id)| (*id == ns_id).then(|| name.clone()))
.unwrap_or_default();
let _bound = name_guard.remove(name.as_str());
let _previous = state.dropped.insert(ns_id, name);
Ok(())
}
fn check_key_at(&self, ns_id: u32, offset: u64, key: &[u8]) -> Result<KeyCheck> {
let guard = epoch::pin();
Ok(match self.decode_insert_at(ns_id, offset, &guard)? {
Some(record) if record.key() == key => KeyCheck::Same,
Some(record) => KeyCheck::Other(record.key().to_vec()),
None => KeyCheck::Unreadable,
})
}
fn decode_insert_at<'a>(
&'a self,
ns_id: u32,
offset: u64,
guard: &'a Guard,
) -> Result<Option<Decoded<'a>>> {
match self.store.payload(offset, guard)? {
Some((payload, _view)) => self.decode_insert_payload(payload, ns_id),
None => Ok(None),
}
}
fn decode_insert_in<'a>(
&self,
view: &'a ReadView,
ns_id: u32,
offset: u64,
) -> Result<Option<Decoded<'a>>> {
let Ok(start) = usize::try_from(offset) else {
return Ok(None);
};
match format::payload_at(view, start) {
Ok(payload) => self.decode_insert_payload(payload, ns_id),
Err(_) => Ok(None),
}
}
fn decode_insert_payload<'a>(
&self,
payload: &'a [u8],
ns_id: u32,
) -> Result<Option<Decoded<'a>>> {
#[cfg(feature = "encrypt")]
if let Some(ctx) = self.encryption.as_ref() {
let owned = format::decode_payload_encrypted(payload, |nonce, ct| {
let mut input = Vec::with_capacity(NONCE_LEN + ct.len());
input.extend_from_slice(nonce);
input.extend_from_slice(ct);
ctx.decrypt(&input)
})?;
return Ok(match owned {
OwnedRecord::Insert {
ns_id: record_ns,
key,
value,
expires_at,
} if record_ns == ns_id => Some(Decoded::Owned {
key,
value,
expires_at,
}),
_ => None,
});
}
Ok(match format::decode_payload(payload)? {
RecordView::Insert {
ns_id: record_ns,
key,
value,
expires_at,
} if record_ns == ns_id => Some(Decoded::Borrowed {
key,
value,
expires_at,
}),
_ => None,
})
}
fn key_from_payload(&self, payload: &[u8]) -> Result<Option<Vec<u8>>> {
#[cfg(feature = "encrypt")]
if let Some(ctx) = self.encryption.as_ref() {
let ctx = Arc::clone(ctx);
let owned = format::decode_payload_encrypted(payload, |nonce, ct| {
let mut input = Vec::with_capacity(NONCE_LEN + ct.len());
input.extend_from_slice(nonce);
input.extend_from_slice(ct);
ctx.decrypt(&input)
})?;
return Ok(match owned {
OwnedRecord::Insert { key, .. } => Some(key),
_ => None,
});
}
Ok(match format::decode_payload(payload)? {
RecordView::Insert { key, .. } => Some(key.to_vec()),
_ => None,
})
}
fn ensure_namespace_runtime(&self, ns_id: u32) -> Result<Arc<NamespaceRuntime>> {
{
let guard = self.namespaces.read();
if let Some(ns) = guard.get(&ns_id) {
return Ok(Arc::clone(ns));
}
}
let mut guard = self.namespaces.write();
let range_scans = self.range_scans_enabled;
let entry = guard
.entry(ns_id)
.or_insert_with(|| Arc::new(NamespaceRuntime::new(range_scans)));
let next = u64::from(ns_id) + 1;
if next > self.next_namespace_id.load(Ordering::Acquire) {
self.next_namespace_id.store(next, Ordering::Release);
}
Ok(Arc::clone(entry))
}
#[inline]
fn namespace_in<'a>(&'a self, ns_id: u32, guard: &'a Guard) -> Result<NsRef<'a>> {
if ns_id == DEFAULT_NAMESPACE_ID {
return Ok(NsRef::Default(self.default_ns.load(guard).get()));
}
self.named_namespace(ns_id)
}
#[inline]
fn namespace_for_write<'a>(
&'a self,
ns_id: u32,
_write: &'a WriteGuard<'a>,
) -> Result<NsRef<'a>> {
if ns_id == DEFAULT_NAMESPACE_ID {
return Ok(NsRef::Default(unsafe { self.default_ns.get_unpinned() }));
}
self.named_namespace(ns_id)
}
fn named_namespace(&self, ns_id: u32) -> Result<NsRef<'static>> {
self.namespaces
.read()
.get(&ns_id)
.map(|ns| NsRef::Named(Arc::clone(ns)))
.ok_or(Error::InvalidConfig("unknown namespace id"))
}
fn namespace(&self, ns_id: u32) -> Result<Arc<NamespaceRuntime>> {
self.namespaces
.read()
.get(&ns_id)
.map(Arc::clone)
.ok_or(Error::InvalidConfig("unknown namespace id"))
}
fn index_insert(
&self,
ns: &NamespaceRuntime,
ns_id: u32,
hash: KeyHash,
key: &[u8],
offset: u64,
) -> Result<()> {
let _previous = ns.index.replace(hash, key, offset, |existing, key| {
self.check_key_at(ns_id, existing, key)
})?;
if let Some(range_map) = ns.range_index.as_ref() {
let _ = range_map.insert(key.to_vec(), offset);
}
Ok(())
}
fn index_remove(
&self,
ns: &NamespaceRuntime,
ns_id: u32,
hash: KeyHash,
key: &[u8],
) -> Result<()> {
let _previous = ns.index.remove(hash, key, |existing, key| {
self.check_key_at(ns_id, existing, key)
})?;
if let Some(range_map) = ns.range_index.as_ref() {
let _ = range_map.remove(key);
}
Ok(())
}
pub(crate) fn insert(
&self,
ns_id: u32,
key: &[u8],
value: &[u8],
expires_at: u64,
) -> Result<()> {
let hash = self.hasher.hash(key);
let write = self.lock_key(ns_id, hash);
let ns = self.namespace_for_write(ns_id, &write)?;
let offset = self.append_insert(ns_id, key, value, expires_at)?;
self.index_insert(&ns, ns_id, hash, key, offset)
}
pub(crate) fn insert_many(
&self,
ns_id: u32,
items: impl IntoIterator<Item = (Vec<u8>, Vec<u8>, u64)>,
) -> Result<()> {
let ops: Vec<BatchOp> = items
.into_iter()
.map(|(key, value, expires_at)| BatchOp::Insert {
key,
value,
expires_at,
})
.collect();
self.write_batch(ns_id, ops)
}
pub(crate) fn write_batch(&self, ns_id: u32, ops: Vec<BatchOp>) -> Result<()> {
if ops.is_empty() {
return Ok(());
}
let hashes: Vec<KeyHash> = ops.iter().map(|op| self.hasher.hash(op.key())).collect();
let write = self.lock_keys(ns_id, &hashes);
let ns = self.namespace_for_write(ns_id, &write)?;
let mut inserted: std::collections::HashSet<&[u8]> = std::collections::HashSet::new();
let has_removes = ops.iter().any(|op| matches!(op, BatchOp::Remove { .. }));
let mut payloads: Vec<Vec<u8>> = Vec::with_capacity(ops.len());
let mut written: Vec<usize> = Vec::with_capacity(ops.len());
for (i, op) in ops.iter().enumerate() {
match op {
BatchOp::Insert {
key,
value,
expires_at,
} => {
payloads.push(self.encode_insert_payload(ns_id, key, value, *expires_at)?);
if has_removes {
let _ = inserted.insert(key.as_slice());
}
}
BatchOp::Remove { key } => {
let present =
inserted.remove(key.as_slice()) || ns.index.get(hashes[i], key)?.is_some();
if !present {
continue;
}
payloads.push(self.encode_remove_payload(ns_id, key)?);
}
}
written.push(i);
}
if payloads.is_empty() {
return Ok(());
}
let offsets = self
.store
.append_batch(payloads.iter().map(Vec::as_slice))?;
for (&i, &offset) in written.iter().zip(offsets.iter()) {
match &ops[i] {
BatchOp::Insert { key, .. } => {
self.index_insert(&ns, ns_id, hashes[i], key, offset)?;
}
BatchOp::Remove { key } => self.index_remove(&ns, ns_id, hashes[i], key)?,
}
}
Ok(())
}
fn append_insert(&self, ns_id: u32, key: &[u8], value: &[u8], expires_at: u64) -> Result<u64> {
#[cfg(feature = "encrypt")]
if let Some(ctx) = self.encryption.as_ref() {
let payload = ctx.seal_record(format::TAG_INSERT, |body| {
format::encode_insert_body(body, ns_id, key, value, expires_at);
})?;
return self.store.append(&payload);
}
self.store.append_with(|buf| {
buf.push(format::TAG_INSERT);
format::encode_insert_body(buf, ns_id, key, value, expires_at);
Ok(())
})
}
fn append_remove(&self, ns_id: u32, key: &[u8]) -> Result<u64> {
#[cfg(feature = "encrypt")]
if let Some(ctx) = self.encryption.as_ref() {
let payload = ctx.seal_record(format::TAG_REMOVE, |body| {
format::encode_remove_body(body, ns_id, key);
})?;
return self.store.append(&payload);
}
self.store.append_with(|buf| {
buf.push(format::TAG_REMOVE);
format::encode_remove_body(buf, ns_id, key);
Ok(())
})
}
#[inline]
fn consistent<T>(&self, mut read: impl FnMut() -> Result<T>) -> Result<T> {
loop {
let seq = self.store.read_begin();
let out = read();
if self.store.read_validate(seq) {
return out;
}
}
}
pub(crate) fn get_zerocopy(
&self,
ns_id: u32,
key: &[u8],
) -> Result<Option<(crate::ValueRef, u64)>> {
let hash = self.hasher.hash(key);
self.consistent(|| {
let guard = epoch::pin();
let ns = self.namespace_in(ns_id, &guard)?;
let Some(offset) = ns.index.get(hash, key)? else {
return Ok(None);
};
self.read_zerocopy_at(ns_id, offset, key, &guard)
})
}
fn read_zerocopy_at(
&self,
ns_id: u32,
offset: u64,
expected_key: &[u8],
guard: &Guard,
) -> Result<Option<(crate::ValueRef, u64)>> {
let Some((payload, view)) = self.store.payload(offset, guard)? else {
return Ok(None);
};
let Some(record) = self.decode_insert_payload(payload, ns_id)? else {
return Ok(None);
};
if record.key() != expected_key {
return Ok(None);
}
Ok(Some(match record {
Decoded::Borrowed {
value, expires_at, ..
} => {
let start = value.as_ptr() as usize - view.bytes().as_ptr() as usize;
let range = start..start + value.len();
(crate::ValueRef::from_mmap(view.to_arc(), range), expires_at)
}
Decoded::Owned {
value, expires_at, ..
} => (crate::ValueRef::from_owned(value), expires_at),
}))
}
pub(crate) fn get_with_meta(&self, ns_id: u32, key: &[u8]) -> Result<Option<(Vec<u8>, u64)>> {
let hash = self.hasher.hash(key);
self.consistent(|| {
let guard = epoch::pin();
let ns = self.namespace_in(ns_id, &guard)?;
let Some(offset) = ns.index.get(hash, key)? else {
return Ok(None);
};
self.read_value_at(ns_id, offset, key, &guard)
})
}
pub(crate) fn contains_live(&self, ns_id: u32, key: &[u8], now_ms: u64) -> Result<bool> {
let hash = self.hasher.hash(key);
self.consistent(|| {
let guard = epoch::pin();
let ns = self.namespace_in(ns_id, &guard)?;
let Some(offset) = ns.index.get(hash, key)? else {
return Ok(false);
};
Ok(match self.decode_insert_at(ns_id, offset, &guard)? {
Some(record) => record.key() == key && is_live(record.expires_at(), now_ms),
None => false,
})
})
}
fn read_value_at(
&self,
ns_id: u32,
offset: u64,
expected_key: &[u8],
guard: &Guard,
) -> Result<Option<(Vec<u8>, u64)>> {
Ok(match self.decode_insert_at(ns_id, offset, guard)? {
Some(record) if record.key() == expected_key => {
let expires_at = record.expires_at();
Some((record.into_value(), expires_at))
}
_ => None,
})
}
pub(crate) fn remove(&self, ns_id: u32, key: &[u8]) -> Result<Option<Vec<u8>>> {
let hash = self.hasher.hash(key);
let write = self.lock_key(ns_id, hash);
let ns = self.namespace_for_write(ns_id, &write)?;
let Some(offset) = ns.index.get(hash, key)? else {
return Ok(None);
};
let previous = {
let guard = epoch::pin();
self.read_value_at(ns_id, offset, key, &guard)?
};
let Some((value, _expires_at)) = previous else {
return Ok(None);
};
let _remove_offset = self.append_remove(ns_id, key)?;
let _removed = ns.index.remove_if_offset(hash, key, offset);
if let Some(range_map) = ns.range_index.as_ref() {
let _ = range_map.remove(key);
}
Ok(Some(value))
}
#[cfg(feature = "ttl")]
pub(crate) fn remove_if_unchanged(
&self,
ns_id: u32,
key: &[u8],
expected_offset: u64,
) -> Result<bool> {
let hash = self.hasher.hash(key);
let write = self.lock_key(ns_id, hash);
let ns = self.namespace_for_write(ns_id, &write)?;
if ns.index.get(hash, key)? != Some(expected_offset) {
return Ok(false);
}
let _remove_offset = self.append_remove(ns_id, key)?;
let _removed = ns.index.remove_if_offset(hash, key, expected_offset);
if let Some(range_map) = ns.range_index.as_ref() {
let _ = range_map.remove(key);
}
Ok(true)
}
#[cfg(feature = "ttl")]
pub(crate) fn clear_expiry(&self, ns_id: u32, key: &[u8], now_ms: u64) -> Result<bool> {
let hash = self.hasher.hash(key);
let write = self.lock_key(ns_id, hash);
let ns = self.namespace_for_write(ns_id, &write)?;
let Some(offset) = ns.index.get(hash, key)? else {
return Ok(false);
};
let current = {
let guard = epoch::pin();
self.read_value_at(ns_id, offset, key, &guard)?
};
let Some((value, expires_at)) = current else {
return Ok(false);
};
if expires_at == 0 || !is_live(expires_at, now_ms) {
return Ok(false);
}
let new_offset = self.append_insert(ns_id, key, &value, 0)?;
self.index_insert(&ns, ns_id, hash, key, new_offset)?;
Ok(true)
}
#[cfg(feature = "ttl")]
pub(crate) fn expired_entries(&self, ns_id: u32, now_ms: u64) -> Result<Vec<(Vec<u8>, u64)>> {
let (offsets, view) = self.snapshot_offsets(ns_id)?;
let mut expired = Vec::new();
for offset in offsets {
if let Some(record) = self.decode_insert_in(&view, ns_id, offset)? {
if !is_live(record.expires_at(), now_ms) {
expired.push((record.key().to_vec(), offset));
}
}
}
Ok(expired)
}
pub(crate) fn record_count(&self, ns_id: u32) -> Result<u64> {
let guard = epoch::pin();
let ns = self.namespace_in(ns_id, &guard)?;
Ok(ns.index.len() as u64)
}
pub(crate) fn flush(&self) -> Result<()> {
self.store.flush()
}
pub(crate) fn checkpoint(&self) -> Result<()> {
self.store.flush()?;
self.store.persist_meta()
}
pub(crate) fn stats(&self) -> Result<crate::EmdbStats> {
let mut live_records: u64 = 0;
let mut named_namespace_count: usize = 0;
{
let guard = self.namespaces.read();
for (ns_id, ns) in guard.iter() {
live_records = live_records.saturating_add(ns.index.len() as u64);
if *ns_id != DEFAULT_NAMESPACE_ID {
named_namespace_count += 1;
}
}
}
let logical_size_bytes = self.store.tail();
let file_size_bytes = std::fs::metadata(self.store.path())
.map(|m| m.len())
.unwrap_or(logical_size_bytes);
let preallocated_bytes = file_size_bytes.saturating_sub(logical_size_bytes);
let header = self.store.header()?;
let encrypted = (header.flags & meta::FLAG_ENCRYPTED) != 0;
Ok(crate::EmdbStats {
live_records,
namespace_count: named_namespace_count,
logical_size_bytes,
file_size_bytes,
preallocated_bytes,
range_scans_enabled: self.range_scans_enabled,
encrypted,
})
}
pub(crate) fn compact_in_place(&self) -> Result<()> {
let _gate = self.write_gate.write();
let path = self.store.path().to_path_buf();
let tmp = compaction_temp_path(&path);
remove_if_exists(&meta::meta_path_for(&tmp))?;
let namespaces = self.list_namespaces()?;
let live = self.live_offsets(&namespaces)?;
let mut source = SourceReader::open(&path)?;
let rewrite = Rewrite::create(self.store.fs(), &tmp)?;
let built = match self
.write_live(&rewrite, &mut source, &live, true)
.and_then(|built| rewrite.sync().map(|()| built))
{
Ok(built) => built,
Err(err) => return Err(rewrite.abandon(err)),
};
let (read_file, mmap) = match rewrite.open_for_reading() {
Ok(opened) => opened,
Err(err) => return Err(rewrite.abandon(err)),
};
let Some(default_ns) = built.get(&DEFAULT_NAMESPACE_ID).map(Arc::clone) else {
return Err(rewrite.abandon(Error::InvalidConfig(
"compaction rebuilt no default namespace",
)));
};
if let Err(err) = crate::private_fs::keep_permissions(&path, &tmp)
.and_then(|()| std::fs::rename(&tmp, &path))
{
drop((read_file, mmap));
return Err(rewrite.abandon(Error::Io(err)));
}
let dir_synced = sync_dir(&path);
self.store.begin_swap();
self.store
.install_file(rewrite.into_journal(), read_file, mmap);
self.default_ns.store(default_ns);
*self.namespaces.write() = built;
self.store.end_swap();
dir_synced
}
pub(crate) fn backup_to(&self, target: &Path) -> Result<()> {
let source_path = self.store.path().to_path_buf();
let target_canonical = match target.canonicalize() {
Ok(p) => p,
Err(_) => target.to_path_buf(),
};
if let Ok(source_canonical) = source_path.canonicalize() {
if target_canonical == source_canonical || target == source_path {
return Err(Error::InvalidConfig(
"backup target must differ from the source database path",
));
}
} else if target == source_path {
return Err(Error::InvalidConfig(
"backup target must differ from the source database path",
));
}
let (live, mut source, header) = {
let _gate = self.write_gate.write();
let namespaces = self.list_namespaces()?;
let live = self.live_offsets(&namespaces)?;
let source = SourceReader::open(&source_path)?;
(live, source, self.store.header()?)
};
let tmp = backup_temp_path(target);
let tmp_meta = meta::meta_path_for(&tmp);
let target_meta = meta::meta_path_for(target);
remove_if_exists(&tmp_meta)?;
let rewrite = Rewrite::create(self.store.fs(), &tmp)?;
let written = self
.write_live(&rewrite, &mut source, &live, false)
.and_then(|_| rewrite.sync())
.and_then(|()| meta::write_to(self.store.fs(), &tmp_meta, &header));
if let Err(err) = written {
let _ignored = remove_if_exists(&tmp_meta);
return Err(rewrite.abandon(err));
}
drop(rewrite.into_journal());
let committed =
std::fs::rename(&tmp_meta, &target_meta).and_then(|()| std::fs::rename(&tmp, target));
if let Err(err) = committed {
let _ignored = remove_if_exists(&tmp);
let _ignored = remove_if_exists(&tmp_meta);
return Err(Error::Io(err));
}
sync_dir(target)
}
fn live_offsets(&self, namespaces: &[(u32, String)]) -> Result<Vec<LiveNamespace>> {
let guard = self.namespaces.read();
let mut out = Vec::with_capacity(namespaces.len());
for (ns_id, name) in namespaces {
let Some(ns) = guard.get(ns_id) else {
continue;
};
let mut offsets = ns.index.collect_offsets()?;
offsets.sort_unstable();
out.push(LiveNamespace {
ns_id: *ns_id,
name: name.clone(),
offsets,
});
}
Ok(out)
}
fn write_live(
&self,
rewrite: &Rewrite,
source: &mut SourceReader,
live: &[LiveNamespace],
build_index: bool,
) -> Result<HashMap<u32, Arc<NamespaceRuntime>>> {
let mut names = Vec::new();
for ns in live {
if ns.ns_id != DEFAULT_NAMESPACE_ID && !ns.name.is_empty() {
names.push(self.encode_namespace_name_payload(ns.ns_id, ns.name.as_bytes())?);
}
}
let name_refs: Vec<&[u8]> = names.iter().map(Vec::as_slice).collect();
let _name_offsets = rewrite.append_batch(&name_refs)?;
let mut written = if build_index {
Some(SourceReader::open(rewrite.path())?)
} else {
None
};
let mut built = HashMap::with_capacity(live.len());
let mut chunk: Vec<u8> = Vec::new();
let mut ranges: Vec<std::ops::Range<usize>> = Vec::new();
for ns in live {
let runtime = NamespaceRuntime::new(self.range_scans_enabled);
let target = build_index.then_some((ns.ns_id, &runtime));
for &offset in &ns.offsets {
ranges.push(source.append_payload(offset, &mut chunk)?);
if chunk.len() >= REWRITE_CHUNK_BYTES {
self.write_chunk(rewrite, &chunk, &ranges, target, written.as_mut())?;
chunk.clear();
ranges.clear();
}
}
if !ranges.is_empty() {
self.write_chunk(rewrite, &chunk, &ranges, target, written.as_mut())?;
chunk.clear();
ranges.clear();
}
if build_index {
let _previous = built.insert(ns.ns_id, Arc::new(runtime));
}
}
Ok(built)
}
fn write_chunk(
&self,
rewrite: &Rewrite,
chunk: &[u8],
ranges: &[std::ops::Range<usize>],
runtime: Option<(u32, &NamespaceRuntime)>,
written: Option<&mut SourceReader>,
) -> Result<()> {
let payloads: Vec<&[u8]> = ranges.iter().map(|r| &chunk[r.clone()]).collect();
let starts = rewrite.append_batch(&payloads)?;
let (Some((ns_id, runtime)), Some(written)) = (runtime, written) else {
return Ok(());
};
for (payload, start) in payloads.iter().zip(starts) {
let key = self.key_from_payload(payload)?.ok_or(Error::Corrupted {
offset: start,
reason: "live index entry does not hold an insert record",
})?;
let key_hash = self.hasher.hash(&key);
let _previous = runtime
.index
.replace(key_hash, &key, start, |existing, key| {
let payload = written.payload(existing)?;
Ok(match self.decode_insert_payload(payload, ns_id)? {
Some(record) if record.key() == key => KeyCheck::Same,
Some(record) => KeyCheck::Other(record.key().to_vec()),
None => KeyCheck::Unreadable,
})
})?;
if let Some(range_map) = runtime.range_index.as_ref() {
let _ = range_map.insert(key, start);
}
}
Ok(())
}
fn encode_namespace_name_payload(&self, ns_id: u32, name: &[u8]) -> Result<Vec<u8>> {
#[cfg(feature = "encrypt")]
if let Some(ctx) = self.encryption.as_ref() {
return ctx.seal_record(format::TAG_NAMESPACE_NAME, |body| {
format::encode_namespace_name_body(body, ns_id, name);
});
}
let mut payload = Vec::with_capacity(1 + 8 + name.len());
payload.push(format::TAG_NAMESPACE_NAME);
format::encode_namespace_name_body(&mut payload, ns_id, name);
Ok(payload)
}
fn encode_insert_payload(
&self,
ns_id: u32,
key: &[u8],
value: &[u8],
expires_at: u64,
) -> Result<Vec<u8>> {
#[cfg(feature = "encrypt")]
if let Some(ctx) = self.encryption.as_ref() {
return ctx.seal_record(format::TAG_INSERT, |body| {
format::encode_insert_body(body, ns_id, key, value, expires_at);
});
}
let mut payload = Vec::with_capacity(1 + 20 + key.len() + value.len());
payload.push(format::TAG_INSERT);
format::encode_insert_body(&mut payload, ns_id, key, value, expires_at);
Ok(payload)
}
fn encode_remove_payload(&self, ns_id: u32, key: &[u8]) -> Result<Vec<u8>> {
#[cfg(feature = "encrypt")]
if let Some(ctx) = self.encryption.as_ref() {
return ctx.seal_record(format::TAG_REMOVE, |body| {
format::encode_remove_body(body, ns_id, key);
});
}
let mut payload = Vec::with_capacity(1 + 8 + key.len());
payload.push(format::TAG_REMOVE);
format::encode_remove_body(&mut payload, ns_id, key);
Ok(payload)
}
pub(crate) fn clear_namespace(&self, ns_id: u32) -> Result<()> {
let _gate = self.write_gate.write();
let ns = self.namespace(ns_id)?;
self.tombstone_all(ns_id, &ns)
}
fn tombstone_all(&self, ns_id: u32, ns: &NamespaceRuntime) -> Result<()> {
let mut offsets = ns.index.collect_offsets()?;
offsets.sort_unstable();
let mut source = SourceReader::open(self.store.path())?;
let mut keys: Vec<Vec<u8>> = Vec::new();
let mut batch_bytes = 0_usize;
for offset in offsets {
let payload = source.payload(offset)?;
let key = self.key_from_payload(payload)?.ok_or(Error::Corrupted {
offset,
reason: "live index entry does not hold an insert record",
})?;
batch_bytes += key.len() + TOMBSTONE_OVERHEAD;
keys.push(key);
if batch_bytes >= REWRITE_CHUNK_BYTES {
self.tombstone_batch(ns_id, ns, &keys)?;
keys.clear();
batch_bytes = 0;
}
}
self.tombstone_batch(ns_id, ns, &keys)
}
fn tombstone_batch(&self, ns_id: u32, ns: &NamespaceRuntime, keys: &[Vec<u8>]) -> Result<()> {
if keys.is_empty() {
return Ok(());
}
let payloads = keys
.iter()
.map(|key| self.encode_remove_payload(ns_id, key))
.collect::<Result<Vec<_>>>()?;
let _offsets = self
.store
.append_batch(payloads.iter().map(Vec::as_slice))?;
for key in keys {
let key_hash = self.hasher.hash(key);
self.index_remove(ns, ns_id, key_hash, key)?;
}
Ok(())
}
#[cfg(feature = "ttl")]
fn sweep_expired_on_open(&self, expired: OpenExpired) {
let keys = match expired {
Some(keys) => keys,
None => match self.expired_keys_by_reading(DEFAULT_NAMESPACE_ID) {
Ok(keys) => keys,
Err(_) => return,
},
};
if keys.is_empty() {
return;
}
let _gate = self.write_gate.write();
let Ok(ns) = self.namespace(DEFAULT_NAMESPACE_ID) else {
return;
};
for batch in keys.chunks(TOMBSTONE_BATCH_KEYS) {
if self
.tombstone_batch(DEFAULT_NAMESPACE_ID, &ns, batch)
.is_err()
{
return;
}
}
}
#[cfg(feature = "ttl")]
fn expired_keys_by_reading(&self, ns_id: u32) -> Result<Vec<Vec<u8>>> {
let now = crate::ttl::now_unix_millis();
let ns = self.namespace(ns_id)?;
let mut offsets = ns.index.collect_offsets()?;
offsets.sort_unstable();
let mut source = SourceReader::open(self.store.path())?;
let mut out = Vec::new();
for offset in offsets {
let payload = source.payload(offset)?;
if let Some((key, _value, expires_at)) = self.decode_triple(payload)? {
if expires_at != 0 && expires_at <= now {
out.push(key);
}
}
}
Ok(out)
}
pub(crate) fn range_scan<R>(
&self,
ns_id: u32,
range: R,
now_ms: u64,
) -> Result<Vec<(Vec<u8>, Vec<u8>)>>
where
R: RangeBounds<Vec<u8>>,
{
let mut cursor = self.range_cursor(ns_id, range)?;
let mut page = VecDeque::new();
let mut out = Vec::new();
loop {
self.fill_range(&mut cursor, &mut page, 256)?;
if page.is_empty() {
return Ok(out);
}
for (key, offset) in page.drain(..) {
if let Some((value, expires_at)) =
self.read_value_in(cursor.view(), ns_id, offset, &key)?
{
if is_live(expires_at, now_ms) {
out.push((key, value));
}
}
}
}
}
pub(crate) fn snapshot_offsets(&self, ns_id: u32) -> Result<(Vec<u64>, ReadView)> {
self.consistent(|| {
let mut offsets = {
let guard = epoch::pin();
self.namespace_in(ns_id, &guard)?.index.collect_offsets()?
};
offsets.sort_unstable();
let view = self.store.pinned_mapping()?;
Ok((offsets, view))
})
}
pub(crate) fn range_cursor<R>(&self, ns_id: u32, range: R) -> Result<RangeCursor>
where
R: RangeBounds<Vec<u8>>,
{
let (map, generation) = self.consistent(|| {
let generation = self.store.read_begin();
let guard = epoch::pin();
let ns = self.namespace_in(ns_id, &guard)?;
let map = ns.range_index.as_ref().ok_or(Error::InvalidConfig(
"range scans not enabled; pass `EmdbBuilder::enable_range_scans(true)` at open time",
))?;
Ok((Arc::clone(map), generation))
})?;
Ok(RangeCursor {
ns_id,
map,
generation,
view: self.store.pinned_mapping()?,
next: owned_bound(range.start_bound()),
end: owned_bound(range.end_bound()),
done: false,
})
}
pub(crate) fn fill_range(
&self,
cursor: &mut RangeCursor,
out: &mut VecDeque<(Vec<u8>, u64)>,
limit: usize,
) -> Result<()> {
if cursor.done {
return Ok(());
}
let before = out.len();
loop {
let seq = self.store.read_begin();
if seq != cursor.generation {
let current = {
let guard = epoch::pin();
match self.namespace_in(cursor.ns_id, &guard) {
Ok(ns) => ns.range_index.as_ref().map(Arc::clone),
Err(_) => None,
}
};
let Some(map) = current else {
cursor.done = true;
return Ok(());
};
cursor.map = map;
cursor.generation = seq;
}
out.extend(
cursor
.map
.range::<[u8], _>((bound_as_slice(&cursor.next), bound_as_slice(&cursor.end)))
.take(limit)
.map(|entry| (entry.key().clone(), *entry.value())),
);
let view = self.store.pinned_mapping()?;
if self.store.read_validate(seq) {
cursor.view = view;
break;
}
out.truncate(before);
}
let added = out.len() - before;
if added < limit {
cursor.done = true;
}
if let Some((last, _)) = out.back().filter(|_| added > 0) {
cursor.next = Bound::Excluded(last.clone());
}
Ok(())
}
pub(crate) fn decode_owned_in(
&self,
view: &ReadView,
ns_id: u32,
offset: u64,
) -> Result<Option<RecordSnapshot>> {
Ok(self
.decode_insert_in(view, ns_id, offset)?
.map(Decoded::into_triple))
}
pub(crate) fn decode_key_in(
&self,
view: &ReadView,
ns_id: u32,
offset: u64,
) -> Result<Option<(Vec<u8>, u64)>> {
Ok(self
.decode_insert_in(view, ns_id, offset)?
.map(|record| (record.key().to_vec(), record.expires_at())))
}
pub(crate) fn read_value_in(
&self,
view: &ReadView,
ns_id: u32,
offset: u64,
expected_key: &[u8],
) -> Result<Option<(Vec<u8>, u64)>> {
Ok(match self.decode_insert_in(view, ns_id, offset)? {
Some(record) if record.key() == expected_key => {
let expires_at = record.expires_at();
Some((record.into_value(), expires_at))
}
_ => None,
})
}
#[cfg(feature = "encrypt")]
pub(crate) fn for_each_record_batch<F>(&self, ns_id: u32, mut sink: F) -> Result<()>
where
F: FnMut(Vec<RecordSnapshot>) -> Result<()>,
{
let (offsets, view) = self.snapshot_offsets(ns_id)?;
let mut batch = Vec::new();
let mut batch_bytes = 0_usize;
for offset in offsets {
let triple = self
.decode_owned_in(&view, ns_id, offset)?
.ok_or(Error::Corrupted {
offset,
reason: "live index entry does not hold an insert record",
})?;
batch_bytes += triple.0.len() + triple.1.len();
batch.push(triple);
if batch_bytes >= REWRITE_CHUNK_BYTES {
sink(std::mem::take(&mut batch))?;
batch_bytes = 0;
}
}
if !batch.is_empty() {
sink(batch)?;
}
Ok(())
}
#[cfg(feature = "ttl")]
fn decode_triple(&self, payload: &[u8]) -> Result<Option<RecordSnapshot>> {
#[cfg(feature = "encrypt")]
if let Some(ctx) = self.encryption.as_ref() {
let ctx = Arc::clone(ctx);
let owned = format::decode_payload_encrypted(payload, |nonce, ct| {
let mut input = Vec::with_capacity(NONCE_LEN + ct.len());
input.extend_from_slice(nonce);
input.extend_from_slice(ct);
ctx.decrypt(&input)
})?;
return Ok(match owned {
OwnedRecord::Insert {
key,
value,
expires_at,
..
} => Some((key, value, expires_at)),
_ => None,
});
}
Ok(match format::decode_payload(payload)? {
RecordView::Insert {
key,
value,
expires_at,
..
} => Some((key.to_vec(), value.to_vec(), expires_at)),
_ => None,
})
}
pub(crate) fn create_or_open_namespace(&self, name: &str) -> Result<u32> {
if name.is_empty() {
return Err(Error::InvalidConfig(
"namespace name must be non-empty (default namespace is implicit)",
));
}
let _gate = self.write_gate.read();
{
let guard = self.namespace_names.read();
if let Some(id) = guard.get(name) {
return Ok(*id);
}
}
let mut name_guard = self.namespace_names.write();
if let Some(id) = name_guard.get(name) {
return Ok(*id);
}
let next = self.next_namespace_id.load(Ordering::Acquire);
let id = match u32::try_from(next) {
Ok(id) if id != DEFAULT_NAMESPACE_ID && id != u32::MAX => id,
_ => {
return Err(Error::InvalidConfig(
"namespace id space exhausted; no more namespaces can be created in this database",
));
}
};
self.next_namespace_id.store(next + 1, Ordering::Release);
let _record_offset = self.append_namespace_name(id, name)?;
let _ = name_guard.insert(name.to_string(), id);
let mut runtimes = self.namespaces.write();
let _ = runtimes.insert(
id,
Arc::new(NamespaceRuntime::new(self.range_scans_enabled)),
);
Ok(id)
}
fn append_namespace_name(&self, ns_id: u32, name: &str) -> Result<u64> {
#[cfg(feature = "encrypt")]
if let Some(ctx) = self.encryption.as_ref() {
let payload = ctx.seal_record(format::TAG_NAMESPACE_NAME, |body| {
format::encode_namespace_name_body(body, ns_id, name.as_bytes());
})?;
return self.store.append(&payload);
}
self.store.append_with(|buf| {
buf.push(format::TAG_NAMESPACE_NAME);
format::encode_namespace_name_body(buf, ns_id, name.as_bytes());
Ok(())
})
}
pub(crate) fn drop_namespace(&self, name: &str) -> Result<bool> {
if name.is_empty() {
return Err(Error::InvalidConfig("default namespace cannot be dropped"));
}
let _gate = self.write_gate.write();
let Some(id) = self.namespace_names.read().get(name).copied() else {
return Ok(false);
};
if let Ok(ns) = self.namespace(id) {
self.tombstone_all(id, &ns)?;
}
let unbind = self.encode_namespace_name_payload(id, b"")?;
let _offset = self.store.append(&unbind)?;
let _name = self.namespace_names.write().remove(name);
let _runtime = self.namespaces.write().remove(&id);
Ok(true)
}
pub(crate) fn list_namespaces(&self) -> Result<Vec<(u32, String)>> {
let guard = self.namespace_names.read();
let mut out: Vec<(u32, String)> = vec![(DEFAULT_NAMESPACE_ID, String::new())];
for (name, id) in guard.iter() {
out.push((*id, name.clone()));
}
out.sort_by_key(|(id, _)| *id);
Ok(out)
}
}
#[cfg(feature = "encrypt")]
fn peek_header(path: &Path) -> Result<Option<MetaHeader>> {
meta::read(path)
}
fn relocate_corruption(err: Error, payload_start: u64) -> Error {
match err {
Error::Corrupted { offset, reason } => Error::Corrupted {
offset: payload_start.saturating_add(offset),
reason,
},
other => other,
}
}
#[cfg(feature = "encrypt")]
fn journal_has_bytes(path: &std::path::Path) -> Result<bool> {
match std::fs::metadata(path) {
Ok(m) => Ok(m.len() != 0),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(err) => Err(Error::from(err)),
}
}
fn sibling_path(path: &Path, suffix: &str) -> PathBuf {
let mut name = path
.file_name()
.map_or_else(|| std::ffi::OsString::from("emdb"), |n| n.to_os_string());
name.push(suffix);
path.with_file_name(name)
}
fn compaction_temp_path(path: &Path) -> PathBuf {
sibling_path(path, ".compact.tmp")
}
fn backup_temp_path(target: &Path) -> PathBuf {
sibling_path(target, ".backup.tmp")
}
const TOMBSTONE_OVERHEAD: usize = 1 + 4 + 4 + 12;
#[cfg(feature = "ttl")]
const TOMBSTONE_BATCH_KEYS: usize = 4096;
struct LiveNamespace {
ns_id: u32,
name: String,
offsets: Vec<u64>,
}
struct SourceReader {
file: File,
buf: Vec<u8>,
}
impl SourceReader {
fn open(path: &Path) -> Result<Self> {
Ok(Self {
file: File::open(path)?,
buf: Vec::new(),
})
}
fn payload_len(&mut self, payload_start: u64) -> Result<usize> {
let corrupt = |reason| Error::Corrupted {
offset: payload_start,
reason,
};
let frame_start = payload_start
.checked_sub(FSYS_PRE_PAYLOAD_BYTES)
.ok_or_else(|| corrupt("record offset inside the frame header"))?;
let mut header = [0_u8; 8];
let _pos = self.file.seek(SeekFrom::Start(frame_start))?;
self.file.read_exact(&mut header)?;
if header[..4] != store::FSYS_FRAME_MAGIC {
return Err(corrupt("record offset does not point at a journal frame"));
}
let len = u64::from(u32::from_le_bytes([
header[4], header[5], header[6], header[7],
]));
if len > FSYS_MAX_PAYLOAD {
return Err(corrupt("journal frame length exceeds the 256 MiB cap"));
}
usize::try_from(len).map_err(|_| corrupt("journal frame larger than the address space"))
}
fn payload(&mut self, payload_start: u64) -> Result<&[u8]> {
let len = self.payload_len(payload_start)?;
self.buf.resize(len, 0);
self.file.read_exact(&mut self.buf)?;
Ok(&self.buf)
}
fn append_payload(
&mut self,
payload_start: u64,
out: &mut Vec<u8>,
) -> Result<std::ops::Range<usize>> {
let len = self.payload_len(payload_start)?;
let start = out.len();
out.resize(start + len, 0);
self.file.read_exact(&mut out[start..])?;
Ok(start..start + len)
}
}
struct Rewrite {
path: PathBuf,
journal: fsys::JournalHandle,
}
impl Rewrite {
fn create(fs: &fsys::Handle, path: &Path) -> Result<Self> {
remove_if_exists(path)?;
drop(crate::private_fs::create_new_private_file(path)?);
match fs.journal_with(path, store::journal_options()) {
Ok(journal) => Ok(Self {
path: path.to_path_buf(),
journal,
}),
Err(err) => {
let _ignored = remove_if_exists(path);
Err(from_fsys(err))
}
}
}
fn path(&self) -> &Path {
&self.path
}
fn append_batch(&self, payloads: &[&[u8]]) -> Result<Vec<u64>> {
if payloads.is_empty() {
return Ok(Vec::new());
}
let end = self.journal.append_batch(payloads).map_err(from_fsys)?;
Ok(batch_payload_starts(end.as_u64(), payloads))
}
fn sync(&self) -> Result<()> {
self.journal
.sync_through(self.journal.next_lsn())
.map_err(from_fsys)
}
fn open_for_reading(&self) -> Result<(File, Mmap)> {
let file = File::open(&self.path)?;
let mmap = unsafe { Mmap::map(&file)? };
Ok((file, mmap))
}
fn into_journal(self) -> fsys::JournalHandle {
self.journal
}
fn abandon(self, err: Error) -> Error {
let Self { path, journal } = self;
drop(journal);
let _ignored = remove_if_exists(&path);
err
}
}
#[cfg(feature = "ttl")]
type OpenExpired = Option<Vec<Vec<u8>>>;
#[cfg(not(feature = "ttl"))]
type OpenExpired = ();
#[cfg(feature = "ttl")]
struct ExpiryTracker {
now: u64,
expired: std::collections::HashSet<Vec<u8>>,
encrypted: bool,
}
#[cfg(feature = "ttl")]
impl ExpiryTracker {
fn new(now: u64) -> Self {
Self {
now,
expired: std::collections::HashSet::new(),
encrypted: false,
}
}
fn observe(&mut self, payload: &[u8]) {
if payload
.first()
.is_some_and(|tag| tag & format::TAG_ENCRYPTED_FLAG != 0)
{
self.encrypted = true;
return;
}
match format::decode_payload(payload) {
Ok(RecordView::Insert {
ns_id: DEFAULT_NAMESPACE_ID,
key,
expires_at,
..
}) => {
if expires_at != 0 && expires_at <= self.now {
let _new = self.expired.insert(key.to_vec());
} else {
let _was = self.expired.remove(key);
}
}
Ok(RecordView::Remove {
ns_id: DEFAULT_NAMESPACE_ID,
key,
}) => {
let _was = self.expired.remove(key);
}
_ => {}
}
}
fn finish(self) -> OpenExpired {
if self.encrypted {
None
} else {
Some(self.expired.into_iter().collect())
}
}
}