use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use taquba::{Clock, Queue};
use tokio_util::sync::CancellationToken;
use tracing::warn;
use crate::error::Result;
use crate::keys::{parse_timestamped_kv_key, validate_run_id};
const SWEEP_PAGE_SIZE: usize = 256;
type ClearError = Box<dyn std::error::Error + Send + Sync>;
type ClearFuture = Pin<Box<dyn Future<Output = std::result::Result<(), ClearError>> + Send>>;
type ClearFn = Arc<dyn Fn(String) -> ClearFuture + Send + Sync>;
pub(crate) struct Sweep {
prefix: &'static [u8],
retention: Duration,
clear: ClearFn,
}
impl Sweep {
pub(crate) fn new<F, Fut, E>(prefix: &'static [u8], retention: Duration, clear: F) -> Self
where
F: Fn(String) -> Fut + Send + Sync + 'static,
Fut: Future<Output = std::result::Result<(), E>> + Send + 'static,
E: Into<ClearError>,
{
assert!(
retention >= Duration::from_millis(1),
"retention must be at least 1ms",
);
Self {
prefix,
retention,
clear: Arc::new(move |id| {
let fut = clear(id);
Box::pin(async move { fut.await.map_err(Into::into) })
}),
}
}
pub(crate) async fn run(&self, queue: &Queue, clock: &dyn Clock, stop: CancellationToken) {
let mut ticker = tokio::time::interval(self.retention);
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
loop {
tokio::select! {
_ = stop.cancelled() => return,
_ = ticker.tick() => {
if let Err(err) = self.pass(queue, clock).await {
warn!(prefix = %String::from_utf8_lossy(self.prefix), "retention sweep failed: {err}");
}
}
}
}
}
pub(crate) async fn pass(&self, queue: &Queue, clock: &dyn Clock) -> Result<usize> {
let cutoff_ms = clock
.now_ms()
.saturating_sub(self.retention.as_millis() as u64);
let mut cleared = 0usize;
let mut cursor: Option<Vec<u8>> = None;
loop {
let page = queue
.kv_scan(self.prefix, cursor.as_deref(), SWEEP_PAGE_SIZE)
.await?;
let exhausted = page.next_cursor.is_none();
cursor = page.next_cursor;
for (key, _) in page.entries {
let parsed = parse_timestamped_kv_key(self.prefix, &key)
.filter(|(id, _)| validate_run_id(id).is_ok());
let Some((id, ts_ms)) = parsed else {
warn!(
key = %String::from_utf8_lossy(&key),
"malformed marker; deleting without clearing",
);
if let Err(err) = queue.kv_delete(&key).await {
warn!(
key = %String::from_utf8_lossy(&key),
"malformed marker delete failed during sweep: {err}",
);
}
continue;
};
if ts_ms >= cutoff_ms {
return Ok(cleared);
}
if let Err(err) = (self.clear)(id.clone()).await {
warn!(id = %id, "clear failed during sweep: {err}");
continue;
}
if let Err(err) = queue.kv_delete(&key).await {
warn!(id = %id, "marker delete failed during sweep: {err}");
continue;
}
cleared += 1;
}
if exhausted {
return Ok(cleared);
}
}
}
}