use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use taquba::{Expired, SettlementEffects};
use crate::partition::Partition;
use crate::records::{self, Expiring, GraphRunRecord, RequestRecord};
use crate::request::RequestId;
use crate::scheduler::{Error, Scheduler};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ExpireReport {
pub runs: usize,
pub requests: usize,
}
impl Scheduler {
pub async fn expire(&self, retention: Duration) -> Result<ExpireReport, Error> {
let runs = AtomicUsize::new(0);
let requests = AtomicUsize::new(0);
let failure: Mutex<Option<Error>> = Mutex::new(None);
let (runs, requests, failure) = (&runs, &requests, &failure);
let now = self.clock.now_ms();
self.expiry
.pass(&self.queue, now, retention, |time_ms, suffix| async move {
if failure.lock().expect("the failure is not poisoned").is_some() {
return Expired::Keep;
}
let Some(expiring) = Expiring::parse(&suffix) else {
tracing::warn!(suffix = %String::from_utf8_lossy(&suffix), "expiry index entry removed: not an entry");
return Expired::Delete(SettlementEffects::default());
};
let (expired, count) = match &expiring {
Expiring::Run { graph, partition } => {
(self.expire_run(time_ms, graph, partition).await, runs)
}
Expiring::Request(id) => (self.expire_request(time_ms, id).await, requests),
};
match expired {
Ok(expired) => {
if matches!(expired, Expired::DeleteIf { .. }) {
count.fetch_add(1, Ordering::Relaxed);
}
expired
}
Err(e) => {
*failure.lock().expect("the failure is not poisoned") = Some(e);
Expired::Keep
}
}
})
.await?;
if let Some(e) = failure.lock().expect("the failure is not poisoned").take() {
return Err(e);
}
Ok(ExpireReport {
runs: runs.load(Ordering::Relaxed),
requests: requests.load(Ordering::Relaxed),
})
}
async fn expire_run(
&self,
time_ms: u64,
graph: &str,
partition: &Partition,
) -> Result<Expired, Error> {
let run_key = records::graph_run_key(graph, partition);
let Some(bytes) = self.queue.view().kv_get(&run_key).await? else {
return Ok(stale());
};
let run = records::parse::<GraphRunRecord>(&run_key, &bytes)?;
if run.settled_at_ms != Some(time_ms) {
return Ok(stale());
}
let definition = match self.definitions().get(&run.definition).await {
Ok(Some(definition)) => definition,
Ok(None) => {
tracing::warn!(%graph, %partition, definition = %run.definition, "graph run records an unknown definition");
return Ok(Expired::Keep);
}
Err(e) => {
tracing::warn!(%graph, %partition, definition = %run.definition, error = %e, "the definition of a graph run does not load");
return Ok(Expired::Keep);
}
};
let mut effects = SettlementEffects::default().kv_delete(run_key.clone());
for node in definition.nodes() {
effects = effects.kv_delete(records::node_record_key(graph, partition, node));
}
tracing::info!(%graph, %partition, "graph run expired");
Ok(Expired::DeleteIf {
key: run_key,
expected: Some(bytes.to_vec()),
effects,
})
}
async fn expire_request(&self, time_ms: u64, id: &RequestId) -> Result<Expired, Error> {
let key = records::request_key(id);
let Some(bytes) = self.queue.view().kv_get(&key).await? else {
return Ok(stale());
};
if records::parse::<RequestRecord>(&key, &bytes)?.handled_at_ms != time_ms {
return Ok(stale());
}
Ok(Expired::DeleteIf {
key: key.clone(),
expected: Some(bytes.to_vec()),
effects: SettlementEffects::default().kv_delete(key),
})
}
}
fn stale() -> Expired {
Expired::Delete(SettlementEffects::default())
}