use std::sync::Arc;
use std::time::Duration;
use aion_store::OutboxStore;
use chrono::Utc;
use tokio::sync::watch;
use tracing::{error, info};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OutboxReconcilerConfig {
pub interval: Duration,
pub stale_after: Duration,
pub batch_size: u32,
}
pub struct OutboxReconciler {
store: Arc<dyn OutboxStore>,
config: OutboxReconcilerConfig,
}
impl std::fmt::Debug for OutboxReconciler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OutboxReconciler")
.field("config", &self.config)
.finish_non_exhaustive()
}
}
impl OutboxReconciler {
#[must_use]
pub fn new(store: Arc<dyn OutboxStore>, config: OutboxReconcilerConfig) -> Self {
Self { store, config }
}
pub async fn run(self, mut shutdown: watch::Receiver<bool>) {
info!(
interval_ms = self.config.interval.as_millis(),
stale_after_ms = self.config.stale_after.as_millis(),
batch_size = self.config.batch_size,
"outbox reconciler started"
);
let mut interval = tokio::time::interval(self.config.interval);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
tokio::select! {
_ = interval.tick() => {
if *shutdown.borrow() {
break;
}
self.sweep_once().await;
}
changed = shutdown.changed() => {
if changed.is_err() || *shutdown.borrow() {
break;
}
}
}
}
info!("outbox reconciler stopped");
}
async fn sweep_once(&self) {
let now = Utc::now();
let Some(stale_after) = chrono::Duration::from_std(self.config.stale_after).ok() else {
error!("outbox reconciler stale_after duration is out of chrono range");
return;
};
let older_than = now - stale_after;
match self
.store
.rearm_stale_claimed_outbox_rows(older_than, now, self.config.batch_size)
.await
{
Ok(rows) if rows.is_empty() => {}
Ok(rows) => {
info!(
rearmed = rows.len(),
older_than = %older_than,
"outbox reconciler re-armed stale claimed rows"
);
}
Err(error) => {
error!(%error, "outbox reconciler failed to re-arm stale claimed rows");
}
}
}
}