use std::{
io::{self, Write},
sync::{
mpsc::{sync_channel, Receiver, SyncSender, TrySendError},
Arc, Condvar, Mutex, PoisonError,
},
thread,
time::{Duration, Instant},
};
use tracing_subscriber::fmt::MakeWriter;
#[cfg(test)]
mod tests;
const SHUTDOWN_ENQUEUE_TIMEOUT: Duration = Duration::from_millis(100);
const SHUTDOWN_FLUSH_TIMEOUT: Duration = Duration::from_secs(1);
const SHUTDOWN_ENQUEUE_RETRY_INTERVAL: Duration = Duration::from_millis(1);
#[derive(Debug)]
enum Message {
Line(Vec<u8>),
Shutdown,
}
#[derive(Clone, Debug)]
pub(super) struct NonBlocking {
sender: SyncSender<Message>,
}
#[must_use]
#[derive(Debug)]
pub(super) struct WorkerGuard {
sender: SyncSender<Message>,
finished: Finished,
}
#[derive(Clone, Debug, Default)]
struct Finished(Arc<(Mutex<bool>, Condvar)>);
impl Finished {
fn set(&self) {
let (finished, changed) = &*self.0;
*finished.lock().unwrap_or_else(PoisonError::into_inner) = true;
changed.notify_all();
}
fn wait_timeout(&self, timeout: Duration) -> bool {
let (finished, changed) = &*self.0;
let finished = finished.lock().unwrap_or_else(PoisonError::into_inner);
let (finished, _) = changed
.wait_timeout_while(finished, timeout, |finished| !*finished)
.unwrap_or_else(PoisonError::into_inner);
*finished
}
}
pub(super) fn non_blocking<W: Write + Send + 'static>(
writer: W,
buffer_limit: usize,
) -> io::Result<(NonBlocking, WorkerGuard)> {
let (sender, receiver) = sync_channel(buffer_limit);
let finished = Finished::default();
let worker_finished = finished.clone();
thread::Builder::new()
.name("zakura-log".to_owned())
.spawn(move || {
run_worker(writer, receiver);
worker_finished.set();
})?;
Ok((
NonBlocking {
sender: sender.clone(),
},
WorkerGuard { sender, finished },
))
}
impl Write for NonBlocking {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
let _ = self.sender.try_send(Message::Line(bytes.to_vec()));
Ok(bytes.len())
}
fn write_all(&mut self, bytes: &[u8]) -> io::Result<()> {
self.write(bytes).map(|_| ())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'a> MakeWriter<'a> for NonBlocking {
type Writer = Self;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
impl Drop for WorkerGuard {
fn drop(&mut self) {
if send_with_timeout(&self.sender, Message::Shutdown, SHUTDOWN_ENQUEUE_TIMEOUT) {
self.finished.wait_timeout(SHUTDOWN_FLUSH_TIMEOUT);
}
}
}
fn send_with_timeout<T>(sender: &SyncSender<T>, mut message: T, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
loop {
match sender.try_send(message) {
Ok(()) => return true,
Err(TrySendError::Disconnected(_)) => return false,
Err(TrySendError::Full(unsent)) => {
if Instant::now() >= deadline {
return false;
}
message = unsent;
thread::sleep(SHUTDOWN_ENQUEUE_RETRY_INTERVAL);
}
}
}
}
fn run_worker<W: Write>(mut writer: W, receiver: Receiver<Message>) {
'batches: while let Ok(mut message) = receiver.recv() {
loop {
match message {
Message::Line(bytes) => {
let _ = writer.write_all(&bytes);
}
Message::Shutdown => break 'batches,
}
match receiver.try_recv() {
Ok(next) => message = next,
Err(_) => break,
}
}
let _ = writer.flush();
}
let _ = writer.flush();
}