use alloc::collections::VecDeque;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::{Arc, Condvar, Mutex, PoisonError};
use std::thread::JoinHandle;
use bacnet_types::error::Error;
use super::{Event, SaveTicket, SaveWait};
use crate::prelude::*;
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
type SaveFn<S> = dyn Fn(&S) -> Result<(), Error> + Send + Sync;
type DoneFn<S> = dyn Fn(&S, &Result<(), Error>) + Send + Sync;
pub(crate) struct SaveWriter<S: Send + 'static> {
shared: Arc<Shared<S>>,
thread: Option<AssertUnwindSafe<JoinHandle<()>>>,
name: String,
}
struct Shared<S> {
queue: Mutex<Queue<S>>,
work: Condvar,
save: AssertUnwindSafe<Box<SaveFn<S>>>,
done: AssertUnwindSafe<Box<DoneFn<S>>>,
}
struct Queue<S> {
jobs: VecDeque<Job<S>>,
running: bool,
closed: bool,
idle_waits: Vec<Arc<Event>>,
}
struct Job<S> {
snapshot: S,
ticket: Option<SaveTicket>,
}
impl<S: Send + 'static> SaveWriter<S> {
pub(crate) fn new(
name: String,
save: impl Fn(&S) -> Result<(), Error> + Send + Sync + 'static,
done: impl Fn(&S, &Result<(), Error>) + Send + Sync + 'static,
) -> Self {
Self {
shared: Arc::new(Shared {
queue: Mutex::new(Queue {
jobs: VecDeque::new(),
running: false,
closed: false,
idle_waits: Vec::new(),
}),
work: Condvar::new(),
save: AssertUnwindSafe(Box::new(save)),
done: AssertUnwindSafe(Box::new(done)),
}),
thread: None,
name,
}
}
pub(crate) fn submit(&mut self, snapshot: S) -> SaveTicket {
let ticket = SaveTicket::default();
self.enqueue(Job {
snapshot,
ticket: Some(ticket.clone()),
});
ticket
}
pub(crate) fn submit_coalescing(&mut self, snapshot: S) {
{
let mut queue = lock(&self.shared.queue);
if let Some(last) = queue.jobs.back_mut().filter(|job| job.ticket.is_none()) {
last.snapshot = snapshot;
return;
}
}
self.enqueue(Job {
snapshot,
ticket: None,
});
}
pub(crate) fn idle(&self) -> SaveWait {
let event = Arc::new(Event::default());
let mut queue = lock(&self.shared.queue);
if queue.running || !queue.jobs.is_empty() {
queue.idle_waits.push(Arc::clone(&event));
} else {
drop(queue);
event.set();
}
SaveWait::new(event)
}
pub(crate) fn wait_idle(&self) {
self.idle().block();
}
pub(crate) fn put_back(&mut self, served: S) {
if let Err(error) = self.submit(served).take_outcome() {
log_warn!(
writer = %self.name,
%error,
"Could not put storage back to the served state as the object closed; \
the next start serves the state staged for a request that never finished"
);
}
}
fn enqueue(&mut self, job: Job<S>) {
if self.thread.is_none() {
let shared = Arc::clone(&self.shared);
match std::thread::Builder::new()
.name(self.name.clone())
.spawn(move || run(&shared))
{
Ok(handle) => self.thread = Some(AssertUnwindSafe(handle)),
Err(error) => {
log_warn!(%error, writer = %self.name, "Could not start a save thread");
run_job(&self.shared, job);
return;
}
}
}
lock(&self.shared.queue).jobs.push_back(job);
self.shared.work.notify_one();
}
}
impl<S: Send + 'static> Drop for SaveWriter<S> {
fn drop(&mut self) {
lock(&self.shared.queue).closed = true;
self.shared.work.notify_one();
if let Some(AssertUnwindSafe(thread)) = self.thread.take() {
let _ = thread.join();
}
}
}
fn run<S>(shared: &Shared<S>) {
loop {
let job = {
let mut queue = lock(&shared.queue);
loop {
if let Some(job) = queue.jobs.pop_front() {
queue.running = true;
break job;
}
if queue.closed {
return;
}
queue = shared
.work
.wait(queue)
.unwrap_or_else(PoisonError::into_inner);
}
};
run_job(shared, job);
let idle = {
let mut queue = lock(&shared.queue);
queue.running = false;
if queue.jobs.is_empty() {
core::mem::take(&mut queue.idle_waits)
} else {
Vec::new()
}
};
for event in idle {
event.set();
}
}
}
fn run_job<S>(shared: &Shared<S>, job: Job<S>) {
let Job { snapshot, ticket } = job;
let result = catch_unwind(AssertUnwindSafe(|| (shared.save.0)(&snapshot)))
.unwrap_or_else(|_| Err(Error::Encoding("persistence panicked during a save".into())));
let _ = catch_unwind(AssertUnwindSafe(|| (shared.done.0)(&snapshot, &result)));
drop(snapshot);
if let Some(ticket) = ticket {
ticket.complete(result);
}
}