use super::{DeletionPolicy, DeletionPolicyRegistry, Tombstone};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
pub enum ResurrectionPolicy {
Allow,
#[default]
ReDelete,
Reject,
}
impl ResurrectionPolicy {
pub fn default_for_collection(collection: &str) -> Self {
match collection {
"beacons" | "platforms" | "tracks" => Self::Allow,
"nodes" | "cells" | "commands" | "contact_reports" => Self::ReDelete,
"alerts" => Self::Reject,
_ => Self::ReDelete,
}
}
}
#[derive(Debug, Clone)]
pub struct GcConfig {
pub gc_interval: Duration,
pub tombstone_batch_size: usize,
pub document_batch_size: usize,
pub debug_logging: bool,
pub resurrection_policies: HashMap<String, ResurrectionPolicy>,
}
impl Default for GcConfig {
fn default() -> Self {
Self {
gc_interval: Duration::from_secs(300), tombstone_batch_size: 1000,
document_batch_size: 500,
debug_logging: true,
resurrection_policies: HashMap::new(),
}
}
}
impl GcConfig {
pub fn with_interval(interval: Duration) -> Self {
Self {
gc_interval: interval,
..Default::default()
}
}
pub fn set_resurrection_policy(&mut self, collection: &str, policy: ResurrectionPolicy) {
self.resurrection_policies
.insert(collection.to_string(), policy);
}
pub fn resurrection_policy(&self, collection: &str) -> ResurrectionPolicy {
self.resurrection_policies
.get(collection)
.copied()
.unwrap_or_else(|| ResurrectionPolicy::default_for_collection(collection))
}
}
#[derive(Debug, Clone, Default)]
pub struct GcResult {
pub tombstones_collected: usize,
pub documents_collected: usize,
pub resurrections_detected: usize,
pub resurrections_redeleted: usize,
pub resurrections_allowed: usize,
pub resurrections_rejected: usize,
pub duration: Duration,
pub errors: Vec<String>,
}
impl GcResult {
pub fn had_work(&self) -> bool {
self.tombstones_collected > 0
|| self.documents_collected > 0
|| self.resurrections_detected > 0
}
}
#[derive(Debug, Clone, Default)]
pub struct GcStats {
pub total_runs: u64,
pub total_tombstones_collected: u64,
pub total_documents_collected: u64,
pub total_resurrections: u64,
pub last_run: Option<SystemTime>,
pub last_duration: Option<Duration>,
}
pub struct GarbageCollector<S> {
store: Arc<S>,
policy_registry: Arc<DeletionPolicyRegistry>,
config: GcConfig,
running: Arc<AtomicBool>,
stats_tombstones: Arc<AtomicU64>,
stats_documents: Arc<AtomicU64>,
stats_resurrections: Arc<AtomicU64>,
stats_runs: Arc<AtomicU64>,
}
pub trait GcStore: Send + Sync {
fn get_all_tombstones(&self) -> anyhow::Result<Vec<Tombstone>>;
fn remove_tombstone(&self, collection: &str, document_id: &str) -> anyhow::Result<bool>;
fn has_tombstone(&self, collection: &str, document_id: &str) -> anyhow::Result<bool>;
fn get_expired_documents(
&self,
collection: &str,
cutoff: SystemTime,
) -> anyhow::Result<Vec<String>>;
fn hard_delete(&self, collection: &str, document_id: &str) -> anyhow::Result<()>;
fn list_collections(&self) -> anyhow::Result<Vec<String>>;
}
impl<S: GcStore> GarbageCollector<S> {
pub fn new(store: Arc<S>, config: GcConfig) -> Self {
Self {
store,
policy_registry: Arc::new(DeletionPolicyRegistry::new()),
config,
running: Arc::new(AtomicBool::new(false)),
stats_tombstones: Arc::new(AtomicU64::new(0)),
stats_documents: Arc::new(AtomicU64::new(0)),
stats_resurrections: Arc::new(AtomicU64::new(0)),
stats_runs: Arc::new(AtomicU64::new(0)),
}
}
pub fn with_policy_registry(
store: Arc<S>,
policy_registry: Arc<DeletionPolicyRegistry>,
config: GcConfig,
) -> Self {
Self {
store,
policy_registry,
config,
running: Arc::new(AtomicBool::new(false)),
stats_tombstones: Arc::new(AtomicU64::new(0)),
stats_documents: Arc::new(AtomicU64::new(0)),
stats_resurrections: Arc::new(AtomicU64::new(0)),
stats_runs: Arc::new(AtomicU64::new(0)),
}
}
pub fn run_gc(&self) -> anyhow::Result<GcResult> {
let start = std::time::Instant::now();
let mut result = GcResult::default();
match self.collect_tombstones() {
Ok(count) => {
result.tombstones_collected = count;
self.stats_tombstones
.fetch_add(count as u64, Ordering::Relaxed);
}
Err(e) => {
result.errors.push(format!("Tombstone GC error: {}", e));
}
}
match self.collect_expired_documents() {
Ok(count) => {
result.documents_collected = count;
self.stats_documents
.fetch_add(count as u64, Ordering::Relaxed);
}
Err(e) => {
result.errors.push(format!("Document GC error: {}", e));
}
}
result.duration = start.elapsed();
self.stats_runs.fetch_add(1, Ordering::Relaxed);
if self.config.debug_logging && result.had_work() {
tracing::info!(
"GC completed: {} tombstones, {} documents in {:?}",
result.tombstones_collected,
result.documents_collected,
result.duration
);
}
Ok(result)
}
fn collect_tombstones(&self) -> anyhow::Result<usize> {
let now = SystemTime::now();
let mut collected = 0;
let tombstones = self.store.get_all_tombstones()?;
for tombstone in tombstones.iter().take(self.config.tombstone_batch_size) {
let policy = self.policy_registry.get(&tombstone.collection);
if let DeletionPolicy::Tombstone { tombstone_ttl, .. } = policy {
if let Ok(age) = now.duration_since(tombstone.deleted_at) {
if age > tombstone_ttl
&& self
.store
.remove_tombstone(&tombstone.collection, &tombstone.document_id)?
{
collected += 1;
if self.config.debug_logging {
tracing::debug!(
"GC: Removed expired tombstone for {}:{} (age: {:?}, ttl: {:?})",
tombstone.collection,
tombstone.document_id,
age,
tombstone_ttl
);
}
}
}
}
}
Ok(collected)
}
fn collect_expired_documents(&self) -> anyhow::Result<usize> {
let now = SystemTime::now();
let mut collected = 0;
let collections = self.store.list_collections()?;
for collection in collections {
let policy = self.policy_registry.get(&collection);
if let DeletionPolicy::ImplicitTTL { ttl, .. } = policy {
let cutoff = now - ttl;
let expired_ids = self.store.get_expired_documents(&collection, cutoff)?;
for doc_id in expired_ids.iter().take(self.config.document_batch_size) {
if let Err(e) = self.store.hard_delete(&collection, doc_id) {
tracing::warn!(
"GC: Failed to hard delete {}:{}: {}",
collection,
doc_id,
e
);
continue;
}
collected += 1;
if self.config.debug_logging {
tracing::debug!(
"GC: Hard deleted expired document {}:{} (ttl: {:?})",
collection,
doc_id,
ttl
);
}
}
}
}
Ok(collected)
}
pub fn check_resurrection(
&self,
collection: &str,
document_id: &str,
_document_timestamp: SystemTime,
) -> anyhow::Result<Option<ResurrectionPolicy>> {
if self.store.has_tombstone(collection, document_id)? {
return Ok(None);
}
Ok(None)
}
pub fn handle_resurrection(
&self,
collection: &str,
document_id: &str,
) -> anyhow::Result<ResurrectionPolicy> {
let policy = self.config.resurrection_policy(collection);
self.stats_resurrections.fetch_add(1, Ordering::Relaxed);
tracing::info!(
"Resurrection detected for {}:{}, policy: {:?}",
collection,
document_id,
policy
);
Ok(policy)
}
pub fn stats(&self) -> GcStats {
GcStats {
total_runs: self.stats_runs.load(Ordering::Relaxed),
total_tombstones_collected: self.stats_tombstones.load(Ordering::Relaxed),
total_documents_collected: self.stats_documents.load(Ordering::Relaxed),
total_resurrections: self.stats_resurrections.load(Ordering::Relaxed),
last_run: None, last_duration: None,
}
}
pub fn is_running(&self) -> bool {
self.running.load(Ordering::Relaxed)
}
pub fn interval(&self) -> Duration {
self.config.gc_interval
}
pub fn stop(&self) {
self.running.store(false, Ordering::SeqCst);
}
}
pub fn start_periodic_gc<S: GcStore + 'static>(
gc: Arc<GarbageCollector<S>>,
) -> tokio::task::JoinHandle<()> {
let interval = gc.interval();
gc.running.store(true, Ordering::SeqCst);
tokio::spawn(async move {
let mut ticker = tokio::time::interval(interval);
ticker.tick().await;
while gc.running.load(Ordering::SeqCst) {
ticker.tick().await;
if !gc.running.load(Ordering::SeqCst) {
break;
}
match gc.run_gc() {
Ok(result) => {
if result.had_work() {
tracing::info!(
"Periodic GC: collected {} tombstones, {} documents in {:?}",
result.tombstones_collected,
result.documents_collected,
result.duration
);
} else {
tracing::debug!("Periodic GC: no work to do");
}
}
Err(e) => {
tracing::error!("Periodic GC failed: {}", e);
}
}
}
tracing::info!("Periodic GC task stopped");
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resurrection_policy_defaults() {
assert_eq!(
ResurrectionPolicy::default_for_collection("beacons"),
ResurrectionPolicy::Allow
);
assert_eq!(
ResurrectionPolicy::default_for_collection("platforms"),
ResurrectionPolicy::Allow
);
assert_eq!(
ResurrectionPolicy::default_for_collection("tracks"),
ResurrectionPolicy::Allow
);
assert_eq!(
ResurrectionPolicy::default_for_collection("nodes"),
ResurrectionPolicy::ReDelete
);
assert_eq!(
ResurrectionPolicy::default_for_collection("cells"),
ResurrectionPolicy::ReDelete
);
assert_eq!(
ResurrectionPolicy::default_for_collection("alerts"),
ResurrectionPolicy::Reject
);
assert_eq!(
ResurrectionPolicy::default_for_collection("unknown"),
ResurrectionPolicy::ReDelete
);
}
#[test]
fn test_gc_config_default() {
let config = GcConfig::default();
assert_eq!(config.gc_interval, Duration::from_secs(300));
assert_eq!(config.tombstone_batch_size, 1000);
assert_eq!(config.document_batch_size, 500);
assert!(config.debug_logging);
}
#[test]
fn test_gc_config_with_interval() {
let config = GcConfig::with_interval(Duration::from_secs(60));
assert_eq!(config.gc_interval, Duration::from_secs(60));
}
#[test]
fn test_gc_config_resurrection_policy_override() {
let mut config = GcConfig::default();
config.set_resurrection_policy("beacons", ResurrectionPolicy::ReDelete);
assert_eq!(
config.resurrection_policy("beacons"),
ResurrectionPolicy::ReDelete
);
assert_eq!(
config.resurrection_policy("tracks"),
ResurrectionPolicy::Allow
);
}
#[test]
fn test_gc_result_had_work() {
let empty = GcResult::default();
assert!(!empty.had_work());
let with_tombstones = GcResult {
tombstones_collected: 1,
..Default::default()
};
assert!(with_tombstones.had_work());
let with_documents = GcResult {
documents_collected: 1,
..Default::default()
};
assert!(with_documents.had_work());
let with_resurrections = GcResult {
resurrections_detected: 1,
..Default::default()
};
assert!(with_resurrections.had_work());
}
struct MockGcStore {
tombstones: Vec<Tombstone>,
collections: Vec<String>,
}
impl MockGcStore {
fn new() -> Self {
Self {
tombstones: Vec::new(),
collections: Vec::new(),
}
}
fn with_tombstones(tombstones: Vec<Tombstone>) -> Self {
Self {
tombstones,
collections: Vec::new(),
}
}
}
impl GcStore for MockGcStore {
fn get_all_tombstones(&self) -> anyhow::Result<Vec<Tombstone>> {
Ok(self.tombstones.clone())
}
fn remove_tombstone(&self, _collection: &str, _document_id: &str) -> anyhow::Result<bool> {
Ok(true)
}
fn has_tombstone(&self, collection: &str, document_id: &str) -> anyhow::Result<bool> {
Ok(self
.tombstones
.iter()
.any(|t| t.collection == collection && t.document_id == document_id))
}
fn get_expired_documents(
&self,
_collection: &str,
_cutoff: SystemTime,
) -> anyhow::Result<Vec<String>> {
Ok(Vec::new())
}
fn hard_delete(&self, _collection: &str, _document_id: &str) -> anyhow::Result<()> {
Ok(())
}
fn list_collections(&self) -> anyhow::Result<Vec<String>> {
Ok(self.collections.clone())
}
}
#[test]
fn test_gc_with_mock_store() {
let store = Arc::new(MockGcStore::new());
let gc = GarbageCollector::new(store, GcConfig::default());
let result = gc.run_gc().unwrap();
assert_eq!(result.tombstones_collected, 0);
assert_eq!(result.documents_collected, 0);
assert!(!result.had_work());
}
#[test]
fn test_gc_stats() {
let store = Arc::new(MockGcStore::new());
let gc = GarbageCollector::new(store, GcConfig::default());
let stats = gc.stats();
assert_eq!(stats.total_runs, 0);
assert_eq!(stats.total_tombstones_collected, 0);
gc.run_gc().unwrap();
let stats = gc.stats();
assert_eq!(stats.total_runs, 1);
}
#[test]
fn test_gc_collect_expired_tombstones() {
let old_time = SystemTime::now() - Duration::from_secs(86400 * 2);
let mut tombstone = Tombstone::new("node-1", "nodes", "test-node", 1);
tombstone.deleted_at = old_time;
let store = Arc::new(MockGcStore::with_tombstones(vec![tombstone]));
let gc = GarbageCollector::new(store, GcConfig::default());
let result = gc.run_gc().unwrap();
assert_eq!(result.tombstones_collected, 1);
}
#[test]
fn test_gc_does_not_collect_fresh_tombstones() {
let tombstone = Tombstone::new("node-1", "nodes", "test-node", 1);
let store = Arc::new(MockGcStore::with_tombstones(vec![tombstone]));
let gc = GarbageCollector::new(store, GcConfig::default());
let result = gc.run_gc().unwrap();
assert_eq!(result.tombstones_collected, 0);
}
#[test]
fn test_gc_tombstone_batch_size_limit() {
let old_time = SystemTime::now() - Duration::from_secs(86400 * 2);
let tombstones: Vec<Tombstone> = (0..10)
.map(|i| {
let mut t = Tombstone::new(format!("node-{}", i), "nodes", "test-node", i as u64);
t.deleted_at = old_time;
t
})
.collect();
let store = Arc::new(MockGcStore::with_tombstones(tombstones));
let config = GcConfig {
tombstone_batch_size: 3, ..Default::default()
};
let gc = GarbageCollector::new(store, config);
let result = gc.run_gc().unwrap();
assert_eq!(result.tombstones_collected, 3);
}
#[test]
fn test_gc_stats_accumulate() {
let old_time = SystemTime::now() - Duration::from_secs(86400 * 2);
let mut tombstone = Tombstone::new("node-1", "nodes", "test-node", 1);
tombstone.deleted_at = old_time;
let store = Arc::new(MockGcStore::with_tombstones(vec![tombstone]));
let gc = GarbageCollector::new(store, GcConfig::default());
gc.run_gc().unwrap();
gc.run_gc().unwrap();
let stats = gc.stats();
assert_eq!(stats.total_runs, 2);
assert_eq!(stats.total_tombstones_collected, 2);
}
#[test]
fn test_gc_is_running_and_stop() {
let store = Arc::new(MockGcStore::new());
let gc = GarbageCollector::new(store, GcConfig::default());
assert!(!gc.is_running());
gc.stop();
assert!(!gc.is_running());
}
#[test]
fn test_gc_interval() {
let config = GcConfig::with_interval(Duration::from_secs(120));
let store = Arc::new(MockGcStore::new());
let gc = GarbageCollector::new(store, config);
assert_eq!(gc.interval(), Duration::from_secs(120));
}
#[test]
fn test_gc_with_policy_registry() {
let store = Arc::new(MockGcStore::new());
let registry = Arc::new(DeletionPolicyRegistry::new());
let gc = GarbageCollector::with_policy_registry(store, registry, GcConfig::default());
let result = gc.run_gc().unwrap();
assert!(!result.had_work());
}
#[test]
fn test_gc_handle_resurrection() {
let store = Arc::new(MockGcStore::new());
let gc = GarbageCollector::new(store, GcConfig::default());
let policy = gc.handle_resurrection("beacons", "doc-1").unwrap();
assert_eq!(policy, ResurrectionPolicy::Allow);
let policy = gc.handle_resurrection("nodes", "doc-2").unwrap();
assert_eq!(policy, ResurrectionPolicy::ReDelete);
let policy = gc.handle_resurrection("alerts", "doc-3").unwrap();
assert_eq!(policy, ResurrectionPolicy::Reject);
let stats = gc.stats();
assert_eq!(stats.total_resurrections, 3);
}
#[test]
fn test_gc_handle_resurrection_with_override() {
let store = Arc::new(MockGcStore::new());
let mut config = GcConfig::default();
config.set_resurrection_policy("beacons", ResurrectionPolicy::Reject);
let gc = GarbageCollector::new(store, config);
let policy = gc.handle_resurrection("beacons", "doc-1").unwrap();
assert_eq!(policy, ResurrectionPolicy::Reject);
}
#[test]
fn test_gc_check_resurrection_with_tombstone() {
let tombstone = Tombstone::new("doc-1", "nodes", "test-node", 1);
let store = Arc::new(MockGcStore::with_tombstones(vec![tombstone]));
let gc = GarbageCollector::new(store, GcConfig::default());
let result = gc
.check_resurrection("nodes", "doc-1", SystemTime::now())
.unwrap();
assert!(result.is_none());
}
#[test]
fn test_gc_check_resurrection_without_tombstone() {
let store = Arc::new(MockGcStore::new());
let gc = GarbageCollector::new(store, GcConfig::default());
let result = gc
.check_resurrection("nodes", "doc-1", SystemTime::now())
.unwrap();
assert!(result.is_none());
}
struct MockGcStoreWithCollections {
tombstones: Vec<Tombstone>,
collections: Vec<String>,
expired_docs: HashMap<String, Vec<String>>,
hard_delete_fails: bool,
}
impl MockGcStoreWithCollections {
fn new(collections: Vec<String>, expired_docs: HashMap<String, Vec<String>>) -> Self {
Self {
tombstones: Vec::new(),
collections,
expired_docs,
hard_delete_fails: false,
}
}
fn with_hard_delete_failure(mut self) -> Self {
self.hard_delete_fails = true;
self
}
}
impl GcStore for MockGcStoreWithCollections {
fn get_all_tombstones(&self) -> anyhow::Result<Vec<Tombstone>> {
Ok(self.tombstones.clone())
}
fn remove_tombstone(&self, _collection: &str, _document_id: &str) -> anyhow::Result<bool> {
Ok(true)
}
fn has_tombstone(&self, _collection: &str, _document_id: &str) -> anyhow::Result<bool> {
Ok(false)
}
fn get_expired_documents(
&self,
collection: &str,
_cutoff: SystemTime,
) -> anyhow::Result<Vec<String>> {
Ok(self
.expired_docs
.get(collection)
.cloned()
.unwrap_or_default())
}
fn hard_delete(&self, _collection: &str, _document_id: &str) -> anyhow::Result<()> {
if self.hard_delete_fails {
anyhow::bail!("hard delete failed");
}
Ok(())
}
fn list_collections(&self) -> anyhow::Result<Vec<String>> {
Ok(self.collections.clone())
}
}
#[test]
fn test_gc_collect_expired_documents_from_implicit_ttl() {
let mut expired = HashMap::new();
expired.insert(
"beacons".to_string(),
vec!["doc-1".to_string(), "doc-2".to_string()],
);
let store = Arc::new(MockGcStoreWithCollections::new(
vec!["beacons".to_string()],
expired,
));
let gc = GarbageCollector::new(store, GcConfig::default());
let result = gc.run_gc().unwrap();
assert_eq!(result.documents_collected, 2);
assert!(result.had_work());
}
#[test]
fn test_gc_document_batch_size_limit() {
let mut expired = HashMap::new();
let docs: Vec<String> = (0..10).map(|i| format!("doc-{}", i)).collect();
expired.insert("beacons".to_string(), docs);
let store = Arc::new(MockGcStoreWithCollections::new(
vec!["beacons".to_string()],
expired,
));
let config = GcConfig {
document_batch_size: 3,
..Default::default()
};
let gc = GarbageCollector::new(store, config);
let result = gc.run_gc().unwrap();
assert_eq!(result.documents_collected, 3);
}
#[test]
fn test_gc_hard_delete_failure_continues() {
let mut expired = HashMap::new();
expired.insert("beacons".to_string(), vec!["doc-1".to_string()]);
let store = Arc::new(
MockGcStoreWithCollections::new(vec!["beacons".to_string()], expired)
.with_hard_delete_failure(),
);
let gc = GarbageCollector::new(store, GcConfig::default());
let result = gc.run_gc().unwrap();
assert_eq!(result.documents_collected, 0);
}
#[test]
fn test_gc_non_implicit_ttl_collection_skipped() {
let mut expired = HashMap::new();
expired.insert("nodes".to_string(), vec!["doc-1".to_string()]);
let store = Arc::new(MockGcStoreWithCollections::new(
vec!["nodes".to_string()],
expired,
));
let gc = GarbageCollector::new(store, GcConfig::default());
let result = gc.run_gc().unwrap();
assert_eq!(result.documents_collected, 0);
}
struct FailingGcStore;
impl GcStore for FailingGcStore {
fn get_all_tombstones(&self) -> anyhow::Result<Vec<Tombstone>> {
anyhow::bail!("tombstone fetch failed")
}
fn remove_tombstone(&self, _: &str, _: &str) -> anyhow::Result<bool> {
Ok(false)
}
fn has_tombstone(&self, _: &str, _: &str) -> anyhow::Result<bool> {
Ok(false)
}
fn get_expired_documents(&self, _: &str, _: SystemTime) -> anyhow::Result<Vec<String>> {
Ok(Vec::new())
}
fn hard_delete(&self, _: &str, _: &str) -> anyhow::Result<()> {
Ok(())
}
fn list_collections(&self) -> anyhow::Result<Vec<String>> {
anyhow::bail!("collection list failed")
}
}
#[test]
fn test_gc_run_with_store_errors() {
let store = Arc::new(FailingGcStore);
let gc = GarbageCollector::new(store, GcConfig::default());
let result = gc.run_gc().unwrap();
assert!(!result.errors.is_empty());
assert!(result
.errors
.iter()
.any(|e| e.contains("Tombstone GC error")));
assert!(result
.errors
.iter()
.any(|e| e.contains("Document GC error")));
}
#[test]
fn test_gc_result_duration() {
let store = Arc::new(MockGcStore::new());
let gc = GarbageCollector::new(store, GcConfig::default());
let result = gc.run_gc().unwrap();
let _ = result.duration;
}
#[test]
fn test_gc_config_no_debug_logging() {
let store = Arc::new(MockGcStore::new());
let config = GcConfig {
debug_logging: false,
..Default::default()
};
let gc = GarbageCollector::new(store, config);
let result = gc.run_gc().unwrap();
assert!(!result.had_work());
}
#[test]
fn test_gc_collect_tombstones_with_no_debug_logging() {
let old_time = SystemTime::now() - Duration::from_secs(86400 * 2);
let mut tombstone = Tombstone::new("node-1", "nodes", "test-node", 1);
tombstone.deleted_at = old_time;
let store = Arc::new(MockGcStore::with_tombstones(vec![tombstone]));
let config = GcConfig {
debug_logging: false,
..Default::default()
};
let gc = GarbageCollector::new(store, config);
let result = gc.run_gc().unwrap();
assert_eq!(result.tombstones_collected, 1);
}
#[test]
fn test_gc_stats_default() {
let stats = GcStats::default();
assert_eq!(stats.total_runs, 0);
assert_eq!(stats.total_tombstones_collected, 0);
assert_eq!(stats.total_documents_collected, 0);
assert_eq!(stats.total_resurrections, 0);
assert!(stats.last_run.is_none());
assert!(stats.last_duration.is_none());
}
#[test]
fn test_gc_result_default_fields() {
let result = GcResult::default();
assert_eq!(result.tombstones_collected, 0);
assert_eq!(result.documents_collected, 0);
assert_eq!(result.resurrections_detected, 0);
assert_eq!(result.resurrections_redeleted, 0);
assert_eq!(result.resurrections_allowed, 0);
assert_eq!(result.resurrections_rejected, 0);
assert!(result.errors.is_empty());
}
#[test]
fn test_gc_config_debug_clone() {
let config = GcConfig::default();
let cloned = config.clone();
assert_eq!(cloned.gc_interval, config.gc_interval);
let _ = format!("{:?}", config);
}
#[test]
fn test_gc_result_debug_clone() {
let result = GcResult {
tombstones_collected: 5,
documents_collected: 3,
resurrections_detected: 1,
..Default::default()
};
let cloned = result.clone();
assert_eq!(cloned.tombstones_collected, 5);
let _ = format!("{:?}", result);
}
#[test]
fn test_resurrection_policy_serde() {
let policy = ResurrectionPolicy::Allow;
let json = serde_json::to_string(&policy).unwrap();
let deserialized: ResurrectionPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, ResurrectionPolicy::Allow);
let policy2 = ResurrectionPolicy::Reject;
let json2 = serde_json::to_string(&policy2).unwrap();
let deserialized2: ResurrectionPolicy = serde_json::from_str(&json2).unwrap();
assert_eq!(deserialized2, ResurrectionPolicy::Reject);
}
#[test]
fn test_resurrection_policy_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(ResurrectionPolicy::Allow);
set.insert(ResurrectionPolicy::ReDelete);
set.insert(ResurrectionPolicy::Reject);
assert_eq!(set.len(), 3);
}
#[test]
fn test_gc_stats_debug_clone() {
let stats = GcStats {
total_runs: 5,
total_tombstones_collected: 10,
total_documents_collected: 3,
total_resurrections: 1,
last_run: Some(SystemTime::now()),
last_duration: Some(Duration::from_millis(50)),
};
let cloned = stats.clone();
assert_eq!(cloned.total_runs, 5);
let _ = format!("{:?}", stats);
}
#[test]
fn test_resurrection_policy_default_trait() {
let policy = ResurrectionPolicy::default();
assert_eq!(policy, ResurrectionPolicy::ReDelete);
}
#[test]
fn test_resurrection_policy_commands_and_contacts() {
assert_eq!(
ResurrectionPolicy::default_for_collection("commands"),
ResurrectionPolicy::ReDelete
);
assert_eq!(
ResurrectionPolicy::default_for_collection("contact_reports"),
ResurrectionPolicy::ReDelete
);
}
}