use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::time::Duration;
use crate::thread_pool::{BackgroundTaskHandle, StorageThreadPool};
use super::context::GraphStorageContext;
use super::GraphStorage;
pub(crate) struct TtlPurgeScheduler {
thread_pool: Arc<StorageThreadPool>,
handle: Option<BackgroundTaskHandle>,
}
impl TtlPurgeScheduler {
pub(crate) fn new(thread_pool: Arc<StorageThreadPool>) -> Self {
Self {
thread_pool,
handle: None,
}
}
pub(crate) fn start(&mut self, interval: Duration, ctx: &GraphStorageContext) {
if self.handle.is_some() || interval.is_zero() {
return;
}
let storage = GraphStorage {
ctx: Arc::new(ctx.clone_for_ttl_purge()),
};
let running = Arc::new(AtomicBool::new(false));
let handle = self.thread_pool.spawn_periodic(
running,
interval,
Duration::from_millis(10),
move || purge_all_spaces(&storage),
);
self.handle = Some(handle);
}
pub(crate) fn stop(&mut self) {
if let Some(handle) = self.handle.take() {
handle.stop();
handle.join();
}
}
pub(crate) fn is_running(&self) -> bool {
self.handle.as_ref().is_some_and(|h| h.is_running())
}
}
impl Drop for TtlPurgeScheduler {
fn drop(&mut self) {
self.stop();
}
}
fn purge_all_spaces(storage: &GraphStorage) {
let spaces = match storage.ctx.schema_manager().list_spaces() {
Ok(spaces) => spaces,
Err(error) => {
log::warn!("TTL background purge failed to list spaces: {}", error);
return;
}
};
let mut removed_total: u64 = 0;
let mut failed_spaces: u64 = 0;
for space in &spaces {
match storage.purge_expired(&space.space_name, None) {
Ok(entries) => {
for entry in &entries {
if entry.removed > 0 {
removed_total += entry.removed as u64;
log::info!(
"TTL background purge removed {} row(s) from {} '{}' in space '{}'",
entry.removed,
if entry.is_edge { "edge type" } else { "tag" },
entry.schema_name,
space.space_name,
);
}
}
}
Err(error) => {
failed_spaces += 1;
log::warn!(
"TTL background purge failed for space '{}': {}",
space.space_name,
error
);
}
}
}
log::info!(
"TTL background purge finished: {} space(s), {} row(s) removed, {} space(s) failed",
spaces.len(),
removed_total,
failed_spaces,
);
if let Some(stats) = storage.ctx.stats_manager() {
stats.record_ttl_purge(1, removed_total);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{PropertyGraphConfig, StorageReader, StorageSchemaOps, StorageWriter};
use linkrs_core::types::{PropertyDef, SpaceInfo, TagInfo, VertexId};
use linkrs_core::vertex_edge_path::Vertex;
use linkrs_core::{DataType, Tag, Value};
use std::collections::HashMap;
fn ttl_storage() -> GraphStorage {
GraphStorage::new_with_config(PropertyGraphConfig::test()).expect("in-memory storage")
}
fn tagged_vertex(vid: i64, ts: i64) -> Vertex {
let mut props: HashMap<std::sync::Arc<str>, Value> = HashMap::new();
props.insert(std::sync::Arc::<str>::from("sid"), Value::BigInt(vid));
props.insert(std::sync::Arc::<str>::from("ts"), Value::BigInt(ts));
Vertex::new(
VertexId::try_from_int64(vid).expect("vertex id"),
Tag::new("S".to_string(), props),
)
}
#[test]
fn sweep_reclaims_expired_rows_and_reports_running_state() {
let storage = ttl_storage();
let mut handle = storage.clone();
let mut space = SpaceInfo::new("sweep".to_string()).with_vid_type(DataType::BigInt);
StorageSchemaOps::create_space(&mut handle, &mut space).expect("create space");
let tag = TagInfo::new("S".to_string())
.with_properties(vec![
PropertyDef::new("sid".to_string(), DataType::BigInt),
PropertyDef::new("ts".to_string(), DataType::BigInt),
])
.with_ttl(Some(100), Some("ts".to_string()));
StorageSchemaOps::create_tag(&mut handle, "sweep", &tag).expect("create tag");
StorageWriter::insert_vertex(&mut handle, "sweep", tagged_vertex(1, 0))
.expect("insert expired vertex");
StorageWriter::insert_vertex(&mut handle, "sweep", tagged_vertex(2, i64::MAX))
.expect("insert fresh vertex");
let ctx = &storage.ctx;
assert!(!ctx.is_ttl_purge_running());
ctx.start_ttl_purge(Duration::ZERO);
assert!(!ctx.is_ttl_purge_running());
ctx.start_ttl_purge(Duration::from_millis(20));
assert!(ctx.is_ttl_purge_running());
let mut visible = Vec::new();
for _ in 0..100 {
visible = storage
.scan_vertices_by_tag("sweep", "S")
.expect("scan tag");
if visible.len() == 1 {
break;
}
std::thread::sleep(Duration::from_millis(20));
}
ctx.stop_ttl_purge();
assert!(!ctx.is_ttl_purge_running());
assert_eq!(visible.len(), 1, "only the expired row may be reclaimed");
assert_eq!(visible[0].vid, tagged_vertex(2, i64::MAX).vid);
}
}