use super::types::QueuedUserMessage;
use crate::db::NotifyQueueRepository;
use uuid::Uuid;
const STALE_ROW_SECS: i64 = 24 * 60 * 60;
pub(crate) const MAX_ROW_AGE_SECS: i64 = 3 * STALE_ROW_SECS;
fn now_unix() -> i64 {
chrono::Utc::now().timestamp()
}
fn repo() -> Option<NotifyQueueRepository> {
crate::db::global_pool().map(|p| NotifyQueueRepository::new(p.clone()))
}
fn spawn_if_runtime<F>(future: F, what: &'static str)
where
F: std::future::Future<Output = ()> + Send + 'static,
{
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
handle.spawn(future);
}
Err(_) => {
tracing::debug!(
target: "background_task",
"No tokio runtime live: skipping {what} (durable parking unavailable)"
);
}
}
}
pub(crate) fn persist(session_id: Uuid, msg: &QueuedUserMessage) {
let Some(repo) = repo() else {
return;
};
let (context_text, display_text) = (msg.context_text.clone(), msg.display_text.clone());
let (origin, bg_meta) = (msg.origin, msg.bg_meta.clone());
spawn_if_runtime(
async move {
if let Err(e) = repo
.record(
Uuid::new_v4(),
session_id,
&context_text,
&display_text,
origin,
bg_meta.as_ref(),
)
.await
{
tracing::error!(
target: "background_task",
"Could not persist undelivered push for session {session_id}: it rides \
the in-memory queue alone and the next restart will lose it: {e:#}"
);
}
},
"notify_queue persist",
);
}
pub(crate) async fn redeliver_persisted() -> usize {
let Some(repo) = repo() else {
return 0;
};
match repo.clear_dead_sessions().await {
Ok(n) if n > 0 => tracing::info!(
target: "background_task",
"Boot notify queue: reaped={n} row(s) whose session no longer exists"
),
Ok(_) => {}
Err(e) => tracing::warn!(
target: "background_task",
"Could not reap notify-queue rows for dead sessions: {e:#}"
),
}
let rows = match repo.all().await {
Ok(rows) => rows,
Err(e) => {
tracing::error!(
target: "background_task",
"Could not read persisted notify-queue rows: {e:#}"
);
return 0;
}
};
let mut count = 0usize;
for row in rows {
let msg = QueuedUserMessage {
context_text: row.context_text.clone(),
display_text: row.display_text.clone(),
origin: row.origin,
bg_meta: row.bg_meta.clone(),
};
if super::restart_recovery::deliver_or_park(row.session_id, msg)
&& let Err(e) = repo.clear(row.id).await
{
tracing::error!(
target: "background_task",
"Delivered persisted push {} but could not clear its row; it may be \
re-delivered after the next restart: {e:#}",
row.id
);
}
count += 1;
}
reap_stale_after_redelivery(&repo).await;
count
}
async fn reap_stale_after_redelivery(repo: &NotifyQueueRepository) {
let now = now_unix();
let cutoff = now.saturating_sub(MAX_ROW_AGE_SECS);
let max_age_hours = MAX_ROW_AGE_SECS / 3600;
match repo.reap_stale_unclaimed(cutoff).await {
Ok(reaped) if !reaped.is_empty() => {
tracing::info!(
target: "background_task",
"Boot notify queue: reaped {} stale unclaimed push(es) older than {}h",
reaped.len(),
max_age_hours
);
for row in reaped {
let age_h = now.saturating_sub(row.created_at) / 3600;
let clean_text = row.context_text.replace(['\n', '\r'], " ");
let preview: String = clean_text.trim().chars().take(120).collect();
tracing::error!(
target: "background_task",
"Notify queue reaper: dropped undeliverable push {} for unclaimed session {} (age {}h > {}h, origin={:?}): {}",
row.id,
row.session_id,
age_h,
max_age_hours,
row.origin,
preview
);
}
}
Ok(_) => {}
Err(e) => tracing::warn!(
target: "background_task",
"Could not reap stale unclaimed notify-queue rows: {e:#}"
),
}
}
pub(crate) fn clear_on_delivery(session_id: Uuid, msg: &QueuedUserMessage) {
let Some(repo) = repo() else {
return;
};
let (context_text, display_text) = (msg.context_text.clone(), msg.display_text.clone());
spawn_if_runtime(
async move {
if let Err(e) = repo
.clear_matching(session_id, &context_text, &display_text)
.await
{
tracing::warn!(
target: "background_task",
"Could not clear delivered push from the durable notify queue for \
session {session_id}: next boot may redeliver it (a duplicate, never \
a loss): {e:#}"
);
}
},
"notify_queue clear",
);
}