use std::io::{self, Write};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
use std::sync::Arc;
use std::thread::JoinHandle;
use std::time::Duration;
const QUEUE_CAPACITY: usize = 8192;
const FLUSH_TIMEOUT: Duration = Duration::from_secs(2);
enum Message {
Line(Vec<u8>),
Flush(SyncSender<()>),
}
#[derive(Clone)]
pub struct LossyWriter {
tx: SyncSender<Message>,
dropped: Arc<AtomicU64>,
}
impl LossyWriter {
pub fn dropped(&self) -> u64 {
self.dropped.load(Ordering::Relaxed)
}
}
impl Write for LossyWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.tx.try_send(Message::Line(buf.to_vec())).is_err() {
self.dropped.fetch_add(1, Ordering::Relaxed);
}
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for LossyWriter {
type Writer = LossyWriter;
fn make_writer(&'a self) -> Self::Writer {
self.clone()
}
}
pub struct WriterGuard {
tx: SyncSender<Message>,
handle: Option<JoinHandle<()>>,
}
impl Drop for WriterGuard {
fn drop(&mut self) {
let (ack_tx, ack_rx) = sync_channel(1);
if self.tx.try_send(Message::Flush(ack_tx)).is_ok() {
let _ = ack_rx.recv_timeout(FLUSH_TIMEOUT);
}
if let Some(handle) = self.handle.take() {
drop(handle);
}
}
}
pub fn spawn<W: Write + Send + 'static>(mut sink: W) -> (LossyWriter, WriterGuard) {
let (tx, rx): (SyncSender<Message>, Receiver<Message>) = sync_channel(QUEUE_CAPACITY);
let dropped = Arc::new(AtomicU64::new(0));
let handle = std::thread::Builder::new()
.name("dig-logging-writer".into())
.spawn(move || {
while let Ok(message) = rx.recv() {
match message {
Message::Line(bytes) => {
let _ = sink.write_all(&bytes);
}
Message::Flush(ack) => {
let _ = sink.flush();
let _ = ack.send(());
}
}
}
})
.expect("spawn dig-logging writer thread");
let writer = LossyWriter {
tx: tx.clone(),
dropped,
};
let guard = WriterGuard {
tx,
handle: Some(handle),
};
(writer, guard)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[derive(Clone, Default)]
struct VecSink(Arc<Mutex<Vec<u8>>>);
impl Write for VecSink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
#[test]
fn lines_reach_the_sink() {
let sink = VecSink::default();
let (mut writer, guard) = spawn(sink.clone());
writer.write_all(b"hello\n").unwrap();
writer.write_all(b"world\n").unwrap();
drop(guard); assert_eq!(&*sink.0.lock().unwrap(), b"hello\nworld\n");
}
#[test]
fn full_queue_drops_and_counts() {
struct BlockingSink;
impl Write for BlockingSink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
std::thread::sleep(Duration::from_secs(3600));
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
let (mut writer, _guard) = spawn(BlockingSink);
for _ in 0..(QUEUE_CAPACITY + 200) {
writer.write_all(b"x\n").unwrap();
}
assert!(
writer.dropped() > 0,
"a wedged sink must drop lines, not block"
);
}
}