use std::future::Future;
use std::pin::Pin;
use std::time::Duration;
use futures_util::TryStreamExt;
use taquba::{Clock, Queue, SettlementEffects};
use tokio_util::sync::CancellationToken;
use tracing::warn;
use crate::error::Result;
use crate::keys::{RunId, parse_timestamped_kv_key};
const SWEEP_PAGE_SIZE: usize = 256;
type ClearError = Box<dyn std::error::Error + Send + Sync>;
type ClearFuture<'a> =
Pin<Box<dyn Future<Output = std::result::Result<Vec<Vec<u8>>, ClearError>> + Send + 'a>>;
pub(crate) trait Clearable: Send + Sync + 'static {
type Error: Into<ClearError>;
fn clear(
&self,
id: &RunId,
) -> impl Future<Output = std::result::Result<Vec<Vec<u8>>, Self::Error>> + Send;
}
trait DynClearable: Send + Sync {
fn clear_dyn<'a>(&'a self, id: &'a RunId) -> ClearFuture<'a>;
}
impl<C: Clearable> DynClearable for C {
fn clear_dyn<'a>(&'a self, id: &'a RunId) -> ClearFuture<'a> {
Box::pin(async move { self.clear(id).await.map_err(Into::into) })
}
}
pub(crate) struct Sweep {
prefix: &'static [u8],
retention: Duration,
store: Box<dyn DynClearable>,
}
impl Sweep {
pub(crate) fn new(prefix: &'static [u8], retention: Duration, store: impl Clearable) -> Self {
assert!(
retention >= Duration::from_millis(1),
"retention must be at least 1ms",
);
Self {
prefix,
retention,
store: Box::new(store),
}
}
pub(crate) async fn run(&self, queue: &Queue, clock: &dyn Clock, stop: CancellationToken) {
run_periodically(self.retention, &stop, (), |()| async move {
if let Err(err) = self.pass(queue, clock).await {
warn!(prefix = %String::from_utf8_lossy(self.prefix), "retention sweep failed: {err}");
}
})
.await;
}
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 markers = std::pin::pin!(queue.kv_entries(self.prefix, SWEEP_PAGE_SIZE));
while let Some((key, _)) = markers.try_next().await? {
let Some((id, ts_ms)) = parse_timestamped_kv_key(self.prefix, &key) 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 {
break;
}
let mut kv_deletes = match self.store.clear_dyn(&id).await {
Ok(keys) => keys,
Err(err) => {
warn!(id = %id, "clear failed during sweep: {err}");
continue;
}
};
kv_deletes.push(key);
if let Err(err) = queue
.commit_effects(SettlementEffects::default().kv_deletes(kv_deletes))
.await
{
warn!(id = %id, "marker delete failed during sweep: {err}");
continue;
}
cleared += 1;
}
Ok(cleared)
}
}
pub(crate) async fn run_periodically<S, Fut>(
interval: Duration,
stop: &CancellationToken,
mut state: S,
mut pass: impl FnMut(S) -> Fut,
) where
Fut: Future<Output = S>,
{
loop {
state = pass(state).await;
tokio::select! {
_ = stop.cancelled() => return,
_ = tokio::time::sleep(interval) => {}
}
}
}