use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use dashmap::DashMap;
use rust_rocksdb::WriteBatch;
use super::engine::META_CF;
use super::RocksDb as DB;
use crate::error::{DbError, DbResult};
const MARKER_PREFIX: &str = "pending_drop:";
const DEFAULT_REUSE_GRACE_SECS: u64 = 300;
const REAP_INTERVAL: Duration = Duration::from_secs(5);
const SLEEP_SLICE: Duration = Duration::from_millis(50);
const MIN_BREATHER: Duration = Duration::from_millis(25);
static DROP_GATE: Mutex<()> = Mutex::new(());
#[cfg(test)]
static DROPS_IN_FLIGHT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
static MOST_DROPS_IN_FLIGHT: std::sync::atomic::AtomicUsize =
std::sync::atomic::AtomicUsize::new(0);
fn reuse_grace() -> Duration {
let secs = std::env::var("SOLIDB_CF_REUSE_GRACE_SECS")
.ok()
.and_then(|v| v.parse::<u64>().ok())
.unwrap_or(DEFAULT_REUSE_GRACE_SECS);
Duration::from_secs(secs)
}
#[derive(Clone, Copy, PartialEq)]
enum DropState {
Pending,
Dropping,
}
pub enum Claim {
Claimed,
InProgress,
NotPending,
}
#[derive(Default)]
pub struct PendingCfDrops {
states: DashMap<String, DropState>,
droppers: Mutex<Vec<JoinHandle<()>>>,
scheduled_at: DashMap<String, Instant>,
shutting_down: Arc<AtomicBool>,
reaper_started: AtomicBool,
}
impl PendingCfDrops {
pub fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub fn contains(&self, cf_name: &str) -> bool {
self.states.contains_key(cf_name)
}
pub fn schedule(&self, db: &DB, db_meta_key: &str, cfs: &[String]) -> DbResult<()> {
let meta_cf = db
.cf_handle(META_CF)
.ok_or_else(|| DbError::InternalError("_meta column family missing".to_string()))?;
let mut batch = WriteBatch::default();
batch.delete_cf(&meta_cf, db_meta_key.as_bytes());
for cf in cfs {
batch.put_cf(
&meta_cf,
format!("{}{}", MARKER_PREFIX, cf).as_bytes(),
b"1",
);
batch.delete_cf(
&meta_cf,
super::collection_registry::entry_key(cf).as_bytes(),
);
}
db.write(&batch).map_err(|e| {
DbError::InternalError(format!("Failed to schedule collection drops: {}", e))
})?;
let now = Instant::now();
for cf in cfs {
self.states.insert(cf.clone(), DropState::Pending);
self.scheduled_at.insert(cf.clone(), now);
}
Ok(())
}
pub fn schedule_one(&self, db: &DB, cf_name: &str) -> DbResult<()> {
let meta_cf = db
.cf_handle(META_CF)
.ok_or_else(|| DbError::InternalError("_meta column family missing".to_string()))?;
db.put_cf(
&meta_cf,
format!("{}{}", MARKER_PREFIX, cf_name).as_bytes(),
b"1",
)
.map_err(|e| {
DbError::InternalError(format!("Failed to schedule collection drop: {}", e))
})?;
self.states.insert(cf_name.to_string(), DropState::Pending);
self.scheduled_at
.insert(cf_name.to_string(), Instant::now());
Ok(())
}
pub fn resume_from_meta(&self, db: &DB) -> Vec<String> {
let meta_cf = match db.cf_handle(META_CF) {
Some(cf) => cf,
None => return vec![],
};
let iter = db.prefix_iterator_cf(&meta_cf, MARKER_PREFIX.as_bytes());
let cfs: Vec<String> = iter
.filter_map(|result| {
result.ok().and_then(|(key, _)| {
let key_str = String::from_utf8(key.to_vec()).ok()?;
key_str.strip_prefix(MARKER_PREFIX).map(|s| s.to_string())
})
})
.collect();
let expired = Instant::now() - reuse_grace();
for cf in &cfs {
self.states.insert(cf.clone(), DropState::Pending);
self.scheduled_at.insert(cf.clone(), expired);
}
cfs
}
pub fn claim_for_recreate(&self, cf_name: &str) -> Claim {
use dashmap::mapref::entry::Entry;
match self.states.entry(cf_name.to_string()) {
Entry::Occupied(mut entry) => match entry.get() {
DropState::Pending => {
*entry.get_mut() = DropState::Dropping;
Claim::Claimed
}
DropState::Dropping => Claim::InProgress,
},
Entry::Vacant(_) => Claim::NotPending,
}
}
pub fn release_claim(&self, cf_name: &str) {
self.states.insert(cf_name.to_string(), DropState::Pending);
}
pub fn complete(&self, db: &DB, cf_name: &str) {
if let Some(meta_cf) = db.cf_handle(META_CF) {
let _ = db.delete_cf(&meta_cf, format!("{}{}", MARKER_PREFIX, cf_name).as_bytes());
}
self.states.remove(cf_name);
self.scheduled_at.remove(cf_name);
super::collection::index_meta::invalidate_index_meta(db, cf_name);
}
pub fn wait_until_dropped(&self, cf_name: &str, timeout: Duration) -> DbResult<()> {
let start = Instant::now();
while self.states.contains_key(cf_name) {
if start.elapsed() > timeout {
return Err(DbError::InternalError(format!(
"Timed out waiting for pending drop of collection '{}'",
cf_name
)));
}
std::thread::sleep(Duration::from_millis(10));
}
Ok(())
}
fn begin_drop(&self, cf_name: &str) -> bool {
use dashmap::mapref::entry::Entry;
match self.states.entry(cf_name.to_string()) {
Entry::Occupied(mut entry) if *entry.get() == DropState::Pending => {
*entry.get_mut() = DropState::Dropping;
true
}
_ => false,
}
}
fn nap(&self, total: Duration) -> bool {
let deadline = Instant::now() + total;
while Instant::now() < deadline {
if self.shutting_down.load(Ordering::Relaxed) {
return false;
}
std::thread::sleep(SLEEP_SLICE.min(deadline - Instant::now()));
}
!self.shutting_down.load(Ordering::Relaxed)
}
fn drop_in_background(&self, db: &DB, cf: &str) -> (Result<(), rust_rocksdb::Error>, bool) {
let _gate = DROP_GATE
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let start = Instant::now();
#[cfg(test)]
{
let now = DROPS_IN_FLIGHT.fetch_add(1, Ordering::SeqCst) + 1;
MOST_DROPS_IN_FLIGHT.fetch_max(now, Ordering::SeqCst);
}
let result = super::cf_ops::timed(|| db.drop_cf(cf));
#[cfg(test)]
DROPS_IN_FLIGHT.fetch_sub(1, Ordering::SeqCst);
let keep_going = self.nap(start.elapsed().max(MIN_BREATHER));
(result, keep_going)
}
fn due_for_drop(&self, grace: Duration) -> Vec<String> {
self.scheduled_at
.iter()
.filter(|entry| entry.value().elapsed() >= grace)
.map(|entry| entry.key().clone())
.collect()
}
pub fn ensure_reaper(db: Arc<DB>, registry: Arc<Self>) {
if registry.reaper_started.swap(true, Ordering::SeqCst) {
return;
}
let grace = reuse_grace();
let registry_for_handle = Arc::clone(®istry);
let handle = std::thread::spawn(move || {
loop {
if !registry.nap(REAP_INTERVAL) {
return;
}
for cf in registry.due_for_drop(grace) {
if registry.shutting_down.load(Ordering::Relaxed) {
return;
}
if !registry.begin_drop(&cf) {
continue;
}
let mut keep_going = true;
if db.cf_handle(&cf).is_some() {
let (result, carry_on) = registry.drop_in_background(&db, &cf);
keep_going = carry_on;
if let Err(e) = result {
tracing::warn!("Reaping column family '{}' failed: {}", cf, e);
registry.release_claim(&cf);
if !keep_going {
return;
}
continue;
}
}
registry.complete(&db, &cf);
if !keep_going {
return;
}
}
}
});
let locked = registry_for_handle.droppers.lock();
if let Ok(mut handles) = locked {
handles.retain(|h| !h.is_finished());
handles.push(handle);
}
}
pub fn spawn_dropper(db: Arc<DB>, registry: Arc<Self>, cfs: Vec<String>) {
if cfs.is_empty() {
return;
}
let registry_for_handle = Arc::clone(®istry);
let handle = std::thread::spawn(move || {
let start = Instant::now();
let total = cfs.len();
let mut dropped = 0usize;
for cf in &cfs {
if registry.shutting_down.load(Ordering::Relaxed) {
return;
}
if !registry.begin_drop(cf) {
continue; }
let mut keep_going = true;
if db.cf_handle(cf).is_some() {
let (result, carry_on) = registry.drop_in_background(&db, cf);
keep_going = carry_on;
if let Err(e) = result {
tracing::warn!("Background drop of column family '{}' failed: {}", cf, e);
registry.release_claim(cf);
if !keep_going {
return;
}
continue;
}
dropped += 1;
}
registry.complete(&db, cf);
if !keep_going {
return;
}
}
tracing::info!(
"Background-dropped {}/{} column families in {:.2?}",
dropped,
total,
start.elapsed()
);
});
let locked = registry_for_handle.droppers.lock();
if let Ok(mut handles) = locked {
handles.retain(|h| !h.is_finished());
handles.push(handle);
}
}
pub fn join_droppers(&self) {
self.shutting_down.store(true, Ordering::Relaxed);
let handles = match self.droppers.lock() {
Ok(mut guard) => std::mem::take(&mut *guard),
Err(_) => return,
};
for handle in handles {
let _ = handle.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn due_for_drop_respects_the_reuse_grace() {
let registry = PendingCfDrops::default();
let grace = Duration::from_secs(300);
registry
.scheduled_at
.insert("db:fresh".to_string(), Instant::now());
registry
.scheduled_at
.insert("db:stale".to_string(), Instant::now() - grace);
let due = registry.due_for_drop(grace);
assert_eq!(due, vec!["db:stale".to_string()]);
}
#[test]
fn resumed_markers_are_immediately_due() {
let registry = PendingCfDrops::default();
let grace = reuse_grace();
registry
.scheduled_at
.insert("db:resumed".to_string(), Instant::now() - grace);
assert_eq!(registry.due_for_drop(grace), vec!["db:resumed".to_string()]);
}
#[test]
fn background_drops_never_overlap() {
use rust_rocksdb::Options;
use tempfile::TempDir;
let dir = TempDir::new().unwrap();
let mut opts = Options::default();
opts.create_if_missing(true);
opts.create_missing_column_families(true);
let names: Vec<String> = (0..8).map(|i| format!("gate_test:c{i}")).collect();
let mut families = vec![META_CF.to_string()];
families.extend(names.iter().cloned());
let db = Arc::new(DB::open_cf(&opts, dir.path(), &families).unwrap());
let registry = Arc::new(PendingCfDrops::default());
let threads: Vec<_> = names
.chunks(2)
.map(|pair| {
let (db, registry, pair) = (Arc::clone(&db), Arc::clone(®istry), pair.to_vec());
std::thread::spawn(move || {
for cf in &pair {
let (result, keep_going) = registry.drop_in_background(&db, cf);
result.unwrap();
assert!(keep_going);
}
})
})
.collect();
for thread in threads {
thread.join().unwrap();
}
assert!(names.iter().all(|cf| db.cf_handle(cf).is_none()));
assert_eq!(MOST_DROPS_IN_FLIGHT.load(Ordering::SeqCst), 1);
}
#[test]
fn nap_returns_early_once_shutdown_is_signalled() {
let registry = Arc::new(PendingCfDrops::default());
registry.shutting_down.store(true, Ordering::Relaxed);
let start = Instant::now();
assert!(!registry.nap(Duration::from_secs(30)));
assert!(start.elapsed() < Duration::from_secs(1));
}
}