use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use tokio::sync::{Mutex as AsyncMutex, Notify, watch};
use crate::log::{LogValue, Logger, fields};
pub type TaskError = Box<dyn std::error::Error + Send + Sync>;
pub type OutboxTask =
Box<dyn Fn() -> Pin<Box<dyn Future<Output = Result<(), TaskError>> + Send>> + Send>;
pub const DEFAULT_MAX_BUFFERED: usize = 500;
pub type AnnounceDrops =
Arc<dyn Fn(usize) -> Pin<Box<dyn Future<Output = Result<(), TaskError>> + Send>> + Send + Sync>;
struct Inner {
buffer: VecDeque<OutboxTask>,
connected: bool,
dropped: usize,
announced: usize,
closed: bool,
}
pub struct Outbox {
state: Arc<Mutex<Inner>>,
closed_signal: Arc<Notify>,
draining: Arc<AsyncMutex<()>>,
log: Logger,
announce_drops: AnnounceDrops,
max_buffered: usize,
}
impl Clone for Outbox {
fn clone(&self) -> Self {
Self {
state: Arc::clone(&self.state),
closed_signal: Arc::clone(&self.closed_signal),
draining: Arc::clone(&self.draining),
log: self.log.clone(),
announce_drops: Arc::clone(&self.announce_drops),
max_buffered: self.max_buffered,
}
}
}
impl Outbox {
pub fn new(log: Logger, announce_drops: AnnounceDrops, max_buffered: usize) -> Self {
Self {
state: Arc::new(Mutex::new(Inner {
buffer: VecDeque::new(),
connected: true,
dropped: 0,
announced: 0,
closed: false,
})),
closed_signal: Arc::new(Notify::new()),
draining: Arc::new(AsyncMutex::new(())),
log,
announce_drops,
max_buffered,
}
}
#[allow(
dead_code,
reason = "read by this module's tests, which assert on state the daemon never asks for"
)]
pub fn pending(&self) -> usize {
self.state.lock().expect("the outbox lock").buffer.len()
}
pub fn is_closed(&self) -> bool {
self.state.lock().expect("the outbox lock").closed
}
#[allow(
dead_code,
reason = "read by this module's tests, which assert on state the daemon never asks for"
)]
pub fn dropped_count(&self) -> usize {
self.state.lock().expect("the outbox lock").dropped
}
pub fn enqueue(&self, task: OutboxTask) {
let mut state = self.state.lock().expect("the outbox lock");
if state.closed {
return;
}
while state.buffer.len() >= self.max_buffered {
state.buffer.pop_front();
state.dropped += 1;
}
state.buffer.push_back(task);
drop(state);
self.drain_soon();
}
pub fn set_connected(&self, connected: bool) {
let was_connected = {
let mut state = self.state.lock().expect("the outbox lock");
std::mem::replace(&mut state.connected, connected)
};
if connected && !was_connected {
self.drain_soon();
}
}
pub fn follow(&self, mut connection: watch::Receiver<bool>) {
let this = self.clone();
let closed = Arc::clone(&self.closed_signal);
tokio::spawn(async move {
loop {
tokio::select! {
() = closed.notified() => return,
changed = connection.changed() => {
if changed.is_err() {
return;
}
}
}
if this.is_closed() {
return;
}
let connected = *connection.borrow();
this.set_connected(connected);
}
});
}
pub async fn flush(&self) {
let _guard = self.draining.lock().await;
self.drain().await;
}
pub fn close(&self) {
{
let mut state = self.state.lock().expect("the outbox lock");
state.closed = true;
state.buffer.clear();
}
self.closed_signal.notify_waiters();
}
fn drain_soon(&self) {
if !self.state.lock().expect("the outbox lock").connected {
return;
}
let this = self.clone();
tokio::spawn(async move {
let _guard = this.draining.lock().await;
this.drain().await;
});
}
async fn drain(&self) {
loop {
let gap = {
let state = self.state.lock().expect("the outbox lock");
if state.closed || !state.connected {
return;
}
if state.dropped > state.announced {
Some(state.dropped - state.announced)
} else {
None
}
};
if let Some(gap) = gap {
match (self.announce_drops)(gap).await {
Ok(()) => {
let mut state = self.state.lock().expect("the outbox lock");
state.announced = state.dropped;
}
Err(error) => {
self.log.warn(
"reporting dropped messages failed",
&fields([("detail", LogValue::from(error.to_string()))]),
);
self.state.lock().expect("the outbox lock").connected = false;
}
}
continue;
}
let head = self
.state
.lock()
.expect("the outbox lock")
.buffer
.pop_front();
let Some(task) = head else { return };
if task().await.is_ok() {
continue;
}
self.log.warn("a thread action failed", &fields([]));
{
let mut state = self.state.lock().expect("the outbox lock");
state.connected = false;
state.buffer.push_front(task);
}
return;
}
}
}
#[cfg(test)]
mod tests;