use tokio::sync::mpsc;
use std::{sync::Arc, time::Duration};
use crate::{
handle::{OwnedTaskHandle, spawn_supervised},
sequence::EventSequence,
tables::MailboxTables,
};
pub(crate) struct PersistentNotifier {
tx: mpsc::UnboundedSender<(EventSequence, EventSequence)>,
_handle: Arc<OwnedTaskHandle>,
}
impl Clone for PersistentNotifier {
fn clone(&self) -> Self {
Self {
tx: self.tx.clone(),
_handle: self._handle.clone(),
}
}
}
#[derive(serde::Serialize)]
struct NotificationPayload {
min_sequence: EventSequence,
max_sequence: EventSequence,
}
impl PersistentNotifier {
pub fn spawn<Tables: MailboxTables>(pool: &sqlx::PgPool, debounce: Duration) -> Self {
let (tx, rx) = mpsc::unbounded_channel();
let pool = pool.clone();
let channel = Tables::persistent_outbox_events_channel();
let handle = spawn_supervised(
"obix::persistent_notifier",
Self::run(pool, channel, debounce, rx),
);
Self {
tx,
_handle: Arc::new(OwnedTaskHandle::new(handle)),
}
}
pub fn report_sender(&self) -> mpsc::UnboundedSender<(EventSequence, EventSequence)> {
self.tx.clone()
}
async fn run(
pool: sqlx::PgPool,
channel: &'static str,
debounce: Duration,
mut rx: mpsc::UnboundedReceiver<(EventSequence, EventSequence)>,
) {
let mut pending: Option<(EventSequence, EventSequence)> = None;
loop {
if pending.is_none() {
match rx.recv().await {
Some(report) => pending = Some(report),
None => return,
}
}
Self::drain(&mut rx, &mut pending);
tokio::time::sleep(debounce).await;
Self::drain(&mut rx, &mut pending);
let (min, max) = pending.expect("pending set before emit");
match Self::emit(&pool, channel, min, max).await {
Ok(()) => pending = None,
Err(error) => record_notify_emit_failed(&error),
}
}
}
fn drain(
rx: &mut mpsc::UnboundedReceiver<(EventSequence, EventSequence)>,
pending: &mut Option<(EventSequence, EventSequence)>,
) {
while let Ok((min, max)) = rx.try_recv() {
*pending = Some(match *pending {
Some((lo, hi)) => (lo.min(min), hi.max(max)),
None => (min, max),
});
}
}
async fn emit(
pool: &sqlx::PgPool,
channel: &str,
min_sequence: EventSequence,
max_sequence: EventSequence,
) -> Result<(), sqlx::Error> {
let payload = serde_json::to_string(&NotificationPayload {
min_sequence,
max_sequence,
})
.expect("Could not serialize notification payload");
sqlx::query("SELECT set_config('synchronous_commit', 'off', true), pg_notify($1, $2)")
.bind(channel)
.bind(payload)
.execute(pool)
.await?;
Ok(())
}
}
#[tracing::instrument(
name = "obix.persistent_notifier.emit_failed",
level = "warn",
skip_all,
fields(error = %error),
)]
fn record_notify_emit_failed(error: &sqlx::Error) {}