use std::io;
use std::sync::{Arc, PoisonError, RwLock, RwLockReadGuard};
use tracing_appender::non_blocking::{NonBlocking, NonBlockingBuilder, WorkerGuard};
use tracing_subscriber::fmt::MakeWriter;
type SpawnWorker = dyn Fn() -> Generation + Send + Sync;
struct Generation {
writer: NonBlocking,
_guard: WorkerGuard,
}
#[derive(Clone)]
pub struct FlushableWriter {
current: Arc<RwLock<Generation>>,
spawn: Arc<SpawnWorker>,
submitted: Arc<std::sync::atomic::AtomicUsize>,
written: Arc<std::sync::atomic::AtomicUsize>,
retired_drops: Arc<std::sync::atomic::AtomicUsize>,
}
impl FlushableWriter {
pub fn new<W, F>(make_sink: F, queue_size: usize, lossy: bool) -> Self
where
W: io::Write + Send + 'static,
F: Fn() -> W + Send + Sync + 'static,
{
let written = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let submitted_counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let written_for_sink = written.clone();
let spawn: Arc<SpawnWorker> = Arc::new(move || {
let (writer, guard) = NonBlockingBuilder::default()
.lossy(lossy)
.buffered_lines_limit(queue_size)
.finish(CountingSink {
inner: make_sink(),
written: written_for_sink.clone(),
});
Generation {
writer,
_guard: guard,
}
});
let current = Arc::new(RwLock::new(spawn()));
Self {
current,
spawn,
submitted: submitted_counter,
written,
retired_drops: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
pub fn drain(&self) {
let mut current = self.current.write().unwrap_or_else(PoisonError::into_inner);
let retired = std::mem::replace(&mut *current, (self.spawn)());
self.retired_drops.fetch_add(
retired.writer.error_counter().dropped_lines(),
std::sync::atomic::Ordering::Relaxed,
);
drop(retired);
}
}
impl std::fmt::Debug for FlushableWriter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FlushableWriter")
.field("shared_by", &Arc::strong_count(&self.current))
.finish()
}
}
struct CountingSink<W> {
inner: W,
written: Arc<std::sync::atomic::AtomicUsize>,
}
impl<W: io::Write> io::Write for CountingSink<W> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
let count = self.inner.write(buf)?;
self.written
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Ok(count)
}
fn flush(&mut self) -> io::Result<()> {
self.inner.flush()
}
}
impl<'a> MakeWriter<'a> for FlushableWriter {
type Writer = QueuedWriter<'a>;
fn make_writer(&'a self) -> Self::Writer {
let current = self.current.read().unwrap_or_else(PoisonError::into_inner);
let writer = current.writer.clone();
self.submitted
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
QueuedWriter {
_current: current,
writer,
}
}
}
pub struct QueuedWriter<'a> {
_current: RwLockReadGuard<'a, Generation>,
writer: NonBlocking,
}
impl io::Write for QueuedWriter<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.writer.write(buf)
}
fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
self.writer.write_all(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.writer.flush()
}
}
impl FlushableWriter {
pub fn queue_depth(&self) -> usize {
let submitted = self.submitted.load(std::sync::atomic::Ordering::Relaxed);
let written = self.written.load(std::sync::atomic::Ordering::Relaxed);
submitted
.saturating_sub(written)
.saturating_sub(self.dropped_lines())
}
pub fn dropped_lines(&self) -> usize {
let live = self
.current
.read()
.map(|generation| generation.writer.error_counter().dropped_lines())
.unwrap_or(0);
live + self
.retired_drops
.load(std::sync::atomic::Ordering::Relaxed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write as _;
use std::sync::Mutex;
use std::thread;
use std::time::Duration;
type Recorder = Arc<Mutex<Vec<String>>>;
struct SlowSink {
delay: Duration,
recorded: Recorder,
}
impl io::Write for SlowSink {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
thread::sleep(self.delay);
self.recorded
.lock()
.expect("recorder poisoned")
.push(String::from_utf8_lossy(buf).into_owned());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn slow_writer(delay_ms: u64) -> (FlushableWriter, Recorder) {
let recorded: Recorder = Arc::new(Mutex::new(Vec::new()));
let sink = Arc::clone(&recorded);
let writer = FlushableWriter::new(
move || SlowSink {
delay: Duration::from_millis(delay_ms),
recorded: Arc::clone(&sink),
},
8_192,
false,
);
(writer, recorded)
}
fn emit(writer: &FlushableWriter, count: usize) {
for i in 0..count {
writer
.make_writer()
.write_all(format!("line {i}\n").as_bytes())
.expect("enqueue failed");
}
}
fn recorded_len(recorder: &Recorder) -> usize {
recorder.lock().expect("recorder poisoned").len()
}
#[test]
fn drain_blocks_until_every_queued_line_reaches_the_sink() {
let (writer, recorded) = slow_writer(1);
emit(&writer, 64);
writer.drain();
assert_eq!(recorded_len(&recorded), 64);
}
#[test]
fn the_writer_still_works_after_a_drain() {
let (writer, recorded) = slow_writer(1);
emit(&writer, 16);
writer.drain();
let after_first = recorded_len(&recorded);
emit(&writer, 16);
writer.drain();
assert_eq!(after_first, 16);
assert_eq!(recorded_len(&recorded), 32);
}
#[test]
fn draining_an_empty_queue_is_harmless() {
let (writer, recorded) = slow_writer(0);
writer.drain();
writer.drain();
emit(&writer, 1);
writer.drain();
assert_eq!(recorded_len(&recorded), 1);
}
#[test]
fn a_clone_drains_the_same_queue() {
let (writer, recorded) = slow_writer(1);
let clone = writer.clone();
emit(&writer, 32);
clone.drain();
assert_eq!(recorded_len(&recorded), 32);
}
#[test]
fn a_clone_keeps_the_worker_alive_after_the_original_is_dropped() {
let (writer, recorded) = slow_writer(1);
let clone = writer.clone();
drop(writer);
emit(&clone, 16);
clone.drain();
assert_eq!(recorded_len(&recorded), 16);
}
#[test]
fn lines_written_during_a_concurrent_drain_are_not_lost() {
let (writer, recorded) = slow_writer(0);
let total = 200;
let producer = {
let writer = writer.clone();
thread::spawn(move || emit(&writer, total))
};
for _ in 0..10 {
writer.drain();
}
producer.join().expect("producer panicked");
writer.drain();
assert_eq!(recorded_len(&recorded), total);
}
}