use std::ops::ControlFlow;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use backon::{BackoffBuilder, ExponentialBuilder};
use sayiir_persistence::TaskWakeupHint;
use sqlx::PgPool;
use sqlx::postgres::PgListener;
use tokio_util::sync::CancellationToken;
static WAKEUP_DROPS: AtomicU64 = AtomicU64::new(0);
#[must_use]
pub fn wakeup_drops_total() -> u64 {
WAKEUP_DROPS.load(Ordering::Relaxed)
}
pub(crate) const TASK_READY_CHANNEL: &str = "sayiir_task_ready";
const WAKEUP_CHANNEL_CAPACITY: usize = 16384;
const RECONNECT_MIN_DELAY: Duration = Duration::from_secs(5);
const RECONNECT_MAX_DELAY: Duration = Duration::from_mins(1);
pub(crate) fn build_task_ready_payload(
snapshot: &sayiir_core::snapshot::WorkflowSnapshot,
) -> Option<String> {
let task_id = snapshot.current_task_id()?;
Some(
TaskWakeupHint {
instance_id: snapshot.instance_id.to_string(),
task_id: *task_id.as_bytes(),
definition_hash: *snapshot.definition_hash.as_bytes(),
tags: snapshot.current_task_tags().to_vec(),
}
.encode(),
)
}
pub(crate) struct WakeupListener {
rx: flume::Receiver<TaskWakeupHint>,
shutdown: CancellationToken,
}
impl WakeupListener {
pub(crate) fn spawn(pool: PgPool) -> std::sync::Arc<Self> {
let (tx, rx) = flume::bounded(WAKEUP_CHANNEL_CAPACITY);
let shutdown = CancellationToken::new();
tokio::spawn(run_listener(pool, tx, shutdown.clone()));
std::sync::Arc::new(Self { rx, shutdown })
}
pub(crate) async fn wait(&self, timeout: std::time::Duration) -> Option<TaskWakeupHint> {
match tokio::time::timeout(timeout, self.rx.recv_async()).await {
Ok(Ok(hint)) => Some(hint),
Ok(Err(flume::RecvError::Disconnected)) | Err(_) => None,
}
}
}
impl Drop for WakeupListener {
fn drop(&mut self) {
self.shutdown.cancel();
}
}
async fn run_listener(
pool: PgPool,
tx: flume::Sender<TaskWakeupHint>,
shutdown: CancellationToken,
) {
loop {
let listener = match connect_and_listen(&pool, &shutdown).await {
ControlFlow::Break(()) => break,
ControlFlow::Continue(l) => l,
};
match recv_until_error(listener, &tx, &shutdown).await {
ControlFlow::Break(()) => break,
ControlFlow::Continue(()) => {}
}
}
tracing::debug!("wakeup listener: exiting");
}
async fn connect_and_listen(
pool: &PgPool,
shutdown: &CancellationToken,
) -> ControlFlow<(), PgListener> {
let mut backoff = ExponentialBuilder::default()
.with_min_delay(RECONNECT_MIN_DELAY)
.with_max_delay(RECONNECT_MAX_DELAY)
.with_factor(2.0)
.with_jitter()
.without_max_times()
.build();
loop {
if shutdown.is_cancelled() || pool.is_closed() {
return ControlFlow::Break(());
}
let mut listener = match shutdown
.run_until_cancelled(PgListener::connect_with(pool))
.await
{
None | Some(Err(sqlx::Error::PoolClosed)) => return ControlFlow::Break(()),
Some(Ok(l)) => l,
Some(Err(e)) => {
tracing::warn!(error = %e, "wakeup listener: connect failed, retrying");
sleep_or_break(shutdown, &mut backoff).await?;
continue;
}
};
match shutdown
.run_until_cancelled(listener.listen(TASK_READY_CHANNEL))
.await
{
None => return ControlFlow::Break(()),
Some(Ok(())) => {
tracing::debug!(channel = TASK_READY_CHANNEL, "wakeup listener subscribed");
return ControlFlow::Continue(listener);
}
Some(Err(e)) => {
tracing::warn!(error = %e, "wakeup listener: LISTEN failed, retrying");
sleep_or_break(shutdown, &mut backoff).await?;
}
}
}
}
async fn recv_until_error(
mut listener: PgListener,
tx: &flume::Sender<TaskWakeupHint>,
shutdown: &CancellationToken,
) -> ControlFlow<(), ()> {
loop {
match shutdown.run_until_cancelled(listener.recv()).await {
None | Some(Err(sqlx::Error::PoolClosed)) => return ControlFlow::Break(()),
Some(Ok(notification)) => match TaskWakeupHint::decode(notification.payload()) {
Ok(hint) => match tx.try_send(hint) {
Ok(()) | Err(flume::TrySendError::Disconnected(_)) => {}
Err(flume::TrySendError::Full(_)) => {
WAKEUP_DROPS.fetch_add(1, Ordering::Relaxed);
tracing::warn!(
"wakeup channel full, dropping hint; worker falls back to poll",
);
}
},
Err(e) => {
tracing::warn!(
error = %e,
payload = notification.payload(),
"wakeup listener: dropping unparseable hint",
);
}
},
Some(Err(e)) => {
tracing::warn!(error = %e, "wakeup listener: recv failed, reconnecting");
return ControlFlow::Continue(());
}
}
}
}
async fn sleep_or_break<B: Iterator<Item = Duration>>(
shutdown: &CancellationToken,
backoff: &mut B,
) -> ControlFlow<()> {
let delay = backoff.next().unwrap_or(RECONNECT_MAX_DELAY);
match shutdown
.run_until_cancelled(tokio::time::sleep(delay))
.await
{
None => ControlFlow::Break(()),
Some(()) => ControlFlow::Continue(()),
}
}