Skip to main content

log_io/sink/
non_blocking.rs

1//! Background-thread sink with a bounded queue.
2
3use core::cell::Cell;
4use std::io::{self, BufWriter, Write};
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError, TrySendError};
7use std::sync::{Arc, Mutex, PoisonError, RwLock, RwLockReadGuard, TryLockError};
8use std::thread::JoinHandle;
9
10use super::{Sink, lock_recover, trim_buffer};
11use crate::error::{Error, Result};
12use crate::format::Format;
13use crate::record::Record;
14
15/// What [`NonBlocking`] does when its queue is full.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
17#[non_exhaustive]
18pub enum Overflow {
19    /// Wait for space. No record is ever dropped; under sustained
20    /// overload the logging threads slow down to the writer's pace.
21    ///
22    /// A record at or above the logger's flush level (see
23    /// [`crate::LoggerBuilder::flush_on`], default `Error`) waits until
24    /// everything queued before it has been written and the writer
25    /// flushed, so it has reached the OS when the logging call returns.
26    #[default]
27    Block,
28    /// Drop the record that did not fit and count it (see
29    /// [`NonBlocking::dropped`]).
30    ///
31    /// Logging never waits on the writer: a record at or above the
32    /// logger's flush level only queues a flush request for the worker
33    /// (skipped if the queue is full, since the worker flushes as soon
34    /// as it drains the queue), so it is not yet durable when the call
35    /// returns. Call [`crate::Logger::flush`] to wait. The one wait left
36    /// is on shutdown: while [`Sink::shutdown`] drains the queue,
37    /// records from other threads wait for it to finish and are then
38    /// written synchronously, so they are not lost.
39    DropNewest,
40}
41
42/// Handler for write errors that happen on the background thread.
43pub type WorkerErrorHandler = Arc<dyn Fn(&io::Error) + Send + Sync>;
44
45enum Message {
46    Record(String),
47    Flush(SyncSender<io::Result<()>>),
48    /// Flush without acknowledging; errors are reported like write
49    /// errors.
50    FlushRequest,
51    Shutdown,
52}
53
54thread_local! {
55    /// Identity of the [`NonBlocking`] sink whose worker runs on this
56    /// thread (the address of its shared state), or `0`.
57    static WORKER_OF: Cell<usize> = const { Cell::new(0) };
58}
59
60/// `true` on the worker thread of any [`NonBlocking`] sink. The panic
61/// hook uses it to stay away from a sink whose worker is the thread
62/// that is panicking.
63pub(crate) fn on_background_worker() -> bool {
64    WORKER_OF.try_with(Cell::get).unwrap_or(0) != 0
65}
66
67struct Shared {
68    dropped: AtomicU64,
69    write_errors: AtomicU64,
70    /// First write error since the last flush; reported by `flush`.
71    deferred: Mutex<Option<io::Error>>,
72    on_error: Option<WorkerErrorHandler>,
73}
74
75/// State guarded by the shutdown lock. Producers hold the read side
76/// while enqueueing, so no record can be queued after the shutdown
77/// message.
78struct Channel {
79    sender: Option<SyncSender<Message>>,
80    worker: Option<JoinHandle<Box<dyn Write + Send>>>,
81}
82
83/// A sink that formats on the calling thread and writes on a
84/// background thread.
85///
86/// Records are formatted into a `String`, queued on a bounded channel,
87/// and written by a dedicated thread through an internal write buffer.
88/// The worker flushes whenever the queue drains, so records reach the
89/// OS promptly once logging pauses, and in large batches under load.
90/// This keeps file and socket I/O off request threads and async
91/// executor threads.
92///
93/// * **Bounded:** at most `capacity` records are queued. What happens
94///   when the queue is full is set by [`Overflow`]; the default,
95///   [`Overflow::Block`], never drops a record.
96/// * **Flush:** [`Sink::flush`] waits until every record queued before
97///   it has been written and the writer flushed.
98/// * **Shutdown:** [`Sink::shutdown`] (called by
99///   [`crate::Logger::shutdown`], a [`crate::FlushGuard`], or dropping
100///   the last logger clone) drains the queue, flushes, and joins the
101///   thread. Records logged afterwards are written synchronously on the
102///   caller's thread, so late records are not lost.
103/// * **Errors:** a write error on the worker cannot be returned to the
104///   caller that logged the record. It is counted
105///   ([`NonBlocking::write_errors`]), passed to the optional
106///   [`NonBlockingBuilder::on_error`] handler, and returned by the next
107///   `flush`.
108/// * **Allocation:** queued strings are recycled through a return
109///   channel, so a single producer allocates only while the pool warms
110///   up, which can take up to `capacity` records when the producer
111///   outruns the writer; with heavy multi-thread contention some
112///   records allocate. The pool keeps at most `capacity` buffers of up
113///   to 64 KiB each.
114///
115/// As with any buffering sink, records still queued when the process
116/// aborts are lost; see [`super::FileSink`] for the exit paths that
117/// skip destructors.
118///
119/// * **Flush level:** a record at or above the logger's flush level
120///   waits for the queue to drain under [`Overflow::Block`] and does
121///   not wait under [`Overflow::DropNewest`]; see [`Overflow`].
122///
123/// The writer and the worker's `on_error` handler may log through a
124/// logger that feeds this same sink, but the worker never waits for
125/// itself: on the worker thread, a record that does not fit in the
126/// queue is dropped and counted (even under [`Overflow::Block`]), a
127/// flush-level flush is skipped (the worker flushes once it drains the
128/// queue), and [`Sink::flush`] and [`Sink::shutdown`] return
129/// [`Error::Closed`] at once. Records written that way are delayed
130/// behind the current batch, and they feed the writer again, so keep
131/// such logging rare.
132///
133/// # Examples
134///
135#[cfg_attr(all(feature = "json"), doc = "```")]
136#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
137/// use log_io::format::JsonFormat;
138/// use log_io::sink::{NonBlocking, Overflow};
139/// use log_io::{Level, Logger};
140///
141/// let sink = NonBlocking::builder(std::io::sink(), JsonFormat::new())
142///     .capacity(4096)
143///     .overflow(Overflow::DropNewest)
144///     .build()?;
145/// let logger = Logger::builder().level(Level::Info).with_sink(sink).build();
146/// log_io::info!(logger, "off the hot path", worker = 3_u32);
147/// logger.shutdown()?; // drain and join
148/// # Ok::<(), log_io::Error>(())
149/// ```
150pub struct NonBlocking<F: Format> {
151    format: F,
152    overflow: Overflow,
153    channel: RwLock<Channel>,
154    /// The writer handed back by the worker at shutdown. Records that
155    /// arrive later are written to it synchronously. Set while the
156    /// channel's write lock is held, so any producer that observes a
157    /// closed channel also observes the fallback.
158    fallback: Mutex<Option<Box<dyn Write + Send>>>,
159    pool: Mutex<Receiver<String>>,
160    shared: Arc<Shared>,
161}
162
163impl<F: Format> NonBlocking<F> {
164    /// Start a background writer with default options: capacity 8192,
165    /// [`Overflow::Block`].
166    ///
167    /// # Errors
168    ///
169    /// Returns [`Error::Io`] if the worker thread cannot be spawned.
170    pub fn new<W: Write + Send + 'static>(writer: W, format: F) -> Result<Self> {
171        Self::builder(writer, format).build()
172    }
173
174    /// Start configuring a background writer.
175    pub fn builder<W: Write + Send + 'static>(writer: W, format: F) -> NonBlockingBuilder<F> {
176        NonBlockingBuilder {
177            writer: Box::new(writer),
178            format,
179            capacity: 8192,
180            overflow: Overflow::Block,
181            buffer_capacity: 64 * 1024,
182            thread_name: String::from("log-io-writer"),
183            on_error: None,
184        }
185    }
186
187    /// Records dropped because the queue was full under
188    /// [`Overflow::DropNewest`].
189    #[must_use]
190    pub fn dropped(&self) -> u64 {
191        self.shared.dropped.load(Ordering::Relaxed)
192    }
193
194    /// Write errors encountered by the background thread.
195    #[must_use]
196    pub fn write_errors(&self) -> u64 {
197        self.shared.write_errors.load(Ordering::Relaxed)
198    }
199
200    fn take_buffer(&self) -> String {
201        // `try_lock`: under contention, allocating is cheaper than
202        // queueing on the pool lock.
203        self.pool
204            .try_lock()
205            .ok()
206            .and_then(|rx| rx.try_recv().ok())
207            .unwrap_or_default()
208    }
209
210    /// `true` when called on this sink's own worker thread.
211    fn on_own_worker(&self) -> bool {
212        let me = Arc::as_ptr(&self.shared).addr();
213        WORKER_OF.try_with(Cell::get).is_ok_and(|w| w == me)
214    }
215
216    fn count_drop(&self) {
217        let _previous = self.shared.dropped.fetch_add(1, Ordering::Relaxed);
218    }
219
220    /// The channel state, or `None` when a shutdown in progress holds
221    /// it and waiting is not allowed.
222    fn channel(&self, may_wait: bool) -> Option<RwLockReadGuard<'_, Channel>> {
223        match self.channel.try_read() {
224            Ok(channel) => Some(channel),
225            Err(TryLockError::Poisoned(p)) => Some(p.into_inner()),
226            Err(TryLockError::WouldBlock) if may_wait => {
227                Some(self.channel.read().unwrap_or_else(PoisonError::into_inner))
228            }
229            Err(TryLockError::WouldBlock) => None,
230        }
231    }
232
233    /// Queue a flush for the worker without waiting for it.
234    fn queue_flush(&self) -> Result<()> {
235        // A shutdown in progress flushes everything anyway.
236        let Some(channel) = self.channel(false) else {
237            return Ok(());
238        };
239        if let Some(sender) = &channel.sender {
240            match sender.try_send(Message::FlushRequest) {
241                // Full: the worker is busy and flushes as soon as it
242                // drains the queue.
243                Ok(()) | Err(TrySendError::Full(_)) => {}
244                Err(TrySendError::Disconnected(_)) => return Err(Error::Closed),
245            }
246            drop(channel);
247            return self.take_deferred();
248        }
249        drop(channel);
250        // Shut down: late records are written synchronously.
251        if let Some(writer) = lock_recover(&self.fallback).as_mut() {
252            writer.flush()?;
253        }
254        self.take_deferred()
255    }
256
257    fn take_deferred(&self) -> Result<()> {
258        match lock_recover(&self.shared.deferred).take() {
259            Some(e) => Err(Error::Io(e)),
260            None => Ok(()),
261        }
262    }
263}
264
265impl<F: Format> Sink for NonBlocking<F> {
266    fn write_record(&self, record: &Record<'_>) -> Result<()> {
267        let mut text = self.take_buffer();
268        text.clear();
269        self.format
270            .write_record_generic(record, &mut text)
271            .map_err(Error::Format)?;
272
273        // The worker must never wait for itself: not for a shutdown that
274        // is joining it, and not for space in its own queue.
275        let Some(channel) = self.channel(!self.on_own_worker()) else {
276            self.count_drop();
277            return Ok(());
278        };
279        if let Some(sender) = &channel.sender {
280            return match sender.try_send(Message::Record(text)) {
281                Ok(()) => Ok(()),
282                Err(TrySendError::Full(message)) => {
283                    if matches!(self.overflow, Overflow::Block) && !self.on_own_worker() {
284                        sender.send(message).map_err(|_| Error::Closed)
285                    } else {
286                        self.count_drop();
287                        Ok(())
288                    }
289                }
290                Err(TrySendError::Disconnected(_)) => Err(Error::Closed),
291            };
292        }
293        drop(channel);
294        // Shut down: write synchronously so late records are kept.
295        match lock_recover(&self.fallback).as_mut() {
296            Some(writer) => writer.write_all(text.as_bytes()).map_err(Error::from),
297            None => Err(Error::Closed),
298        }
299    }
300
301    fn flush(&self) -> Result<()> {
302        if self.on_own_worker() {
303            // Waiting for the worker from the worker would never end.
304            return Err(Error::Closed);
305        }
306        let channel = self.channel.read().unwrap_or_else(PoisonError::into_inner);
307        if let Some(sender) = &channel.sender {
308            let (ack_tx, ack_rx) = mpsc::sync_channel(1);
309            sender
310                .send(Message::Flush(ack_tx))
311                .map_err(|_| Error::Closed)?;
312            drop(channel);
313            let flushed = ack_rx.recv().map_err(|_| Error::Closed)?;
314            self.take_deferred()?;
315            return flushed.map_err(Error::from);
316        }
317        drop(channel);
318        if let Some(writer) = lock_recover(&self.fallback).as_mut() {
319            writer.flush()?;
320        }
321        self.take_deferred()
322    }
323
324    fn request_flush(&self) -> Result<()> {
325        if self.on_own_worker() {
326            // The worker flushes once it has drained the queue.
327            return Ok(());
328        }
329        match self.overflow {
330            Overflow::DropNewest => self.queue_flush(),
331            // `Block` may wait by contract: make the record durable.
332            _ => self.flush(),
333        }
334    }
335
336    fn shutdown(&self) -> Result<()> {
337        if self.on_own_worker() {
338            // Joining the worker from the worker would never end.
339            return Err(Error::Closed);
340        }
341        let mut channel = self.channel.write().unwrap_or_else(PoisonError::into_inner);
342        let Some(sender) = channel.sender.take() else {
343            drop(channel);
344            return self.flush();
345        };
346        // Producers enqueue while holding the read lock, so nothing can
347        // be queued behind this message.
348        let sent = sender.send(Message::Shutdown);
349        drop(sender);
350        let joined = channel.worker.take().map(JoinHandle::join);
351        match joined {
352            Some(Ok(writer)) => *lock_recover(&self.fallback) = Some(writer),
353            Some(Err(_)) => return Err(Error::Closed),
354            None => {}
355        }
356        drop(channel);
357        sent.map_err(|_| Error::Closed)?;
358        self.take_deferred()
359    }
360}
361
362impl<F: Format> Drop for NonBlocking<F> {
363    fn drop(&mut self) {
364        // The logger shuts its sinks down (and reports errors) before
365        // dropping them; this covers sinks used without a logger.
366        let _shutdown_in_drop = self.shutdown();
367    }
368}
369
370impl<F: Format + core::fmt::Debug> core::fmt::Debug for NonBlocking<F> {
371    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
372        f.debug_struct("NonBlocking")
373            .field("format", &self.format)
374            .field("overflow", &self.overflow)
375            .field("dropped", &self.dropped())
376            .field("write_errors", &self.write_errors())
377            .finish_non_exhaustive()
378    }
379}
380
381/// Options for a [`NonBlocking`] sink. Create with
382/// [`NonBlocking::builder`].
383#[must_use = "call `build()` to start the background writer"]
384pub struct NonBlockingBuilder<F> {
385    writer: Box<dyn Write + Send>,
386    format: F,
387    capacity: usize,
388    overflow: Overflow,
389    buffer_capacity: usize,
390    thread_name: String,
391    on_error: Option<WorkerErrorHandler>,
392}
393
394/// Largest accepted queue capacity. The queue and the buffer pool
395/// preallocate one slot per record, so a larger value would only
396/// reserve memory (or overflow the allocation size).
397const MAX_CAPACITY: usize = 1 << 20;
398
399impl<F: Format> NonBlockingBuilder<F> {
400    /// Maximum number of queued records. Default 8192. Values are
401    /// clamped to `1..=1_048_576` (2^20); the queue preallocates a slot
402    /// per record.
403    pub fn capacity(mut self, records: usize) -> Self {
404        self.capacity = records.clamp(1, MAX_CAPACITY);
405        self
406    }
407
408    /// Behaviour when the queue is full. Default [`Overflow::Block`].
409    pub fn overflow(mut self, overflow: Overflow) -> Self {
410        self.overflow = overflow;
411        self
412    }
413
414    /// Size of the worker's write buffer in bytes (`0` = unbuffered).
415    /// Default 64 KiB.
416    pub fn buffer_capacity(mut self, bytes: usize) -> Self {
417        self.buffer_capacity = bytes;
418        self
419    }
420
421    /// Name of the background thread. Default `log-io-writer`. The name
422    /// must not contain a NUL byte; [`Self::build`] rejects one.
423    pub fn thread_name(mut self, name: impl Into<String>) -> Self {
424        self.thread_name = name.into();
425        self
426    }
427
428    /// Called on the background thread for every write error.
429    pub fn on_error(mut self, handler: WorkerErrorHandler) -> Self {
430        self.on_error = Some(handler);
431        self
432    }
433
434    /// Spawn the background thread and build the sink.
435    ///
436    /// # Errors
437    ///
438    /// Returns [`Error::Configuration`] if the thread name contains a
439    /// NUL byte, and [`Error::Io`] if the thread cannot be spawned.
440    pub fn build(self) -> Result<NonBlocking<F>> {
441        if self.thread_name.contains('\0') {
442            return Err(Error::Configuration(
443                "NonBlocking thread name must not contain a NUL byte",
444            ));
445        }
446        let (tx, rx) = mpsc::sync_channel::<Message>(self.capacity);
447        let (pool_tx, pool_rx) = mpsc::sync_channel::<String>(self.capacity);
448        let shared = Arc::new(Shared {
449            dropped: AtomicU64::new(0),
450            write_errors: AtomicU64::new(0),
451            deferred: Mutex::new(None),
452            on_error: self.on_error,
453        });
454        let worker_shared = Arc::clone(&shared);
455        let identity = Arc::as_ptr(&shared).addr();
456        let writer = BufWriter::with_capacity(self.buffer_capacity, self.writer);
457        let worker = std::thread::Builder::new()
458            .name(self.thread_name)
459            .spawn(move || {
460                WORKER_OF.with(|w| w.set(identity));
461                run_worker(writer, &rx, &pool_tx, &worker_shared)
462            })?;
463        Ok(NonBlocking {
464            format: self.format,
465            overflow: self.overflow,
466            channel: RwLock::new(Channel {
467                sender: Some(tx),
468                worker: Some(worker),
469            }),
470            fallback: Mutex::new(None),
471            pool: Mutex::new(pool_rx),
472            shared,
473        })
474    }
475}
476
477impl<F: core::fmt::Debug> core::fmt::Debug for NonBlockingBuilder<F> {
478    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
479        f.debug_struct("NonBlockingBuilder")
480            .field("format", &self.format)
481            .field("capacity", &self.capacity)
482            .field("overflow", &self.overflow)
483            .field("thread_name", &self.thread_name)
484            .finish_non_exhaustive()
485    }
486}
487
488fn report(shared: &Shared, error: io::Error) {
489    let _previous = shared.write_errors.fetch_add(1, Ordering::Relaxed);
490    if let Some(handler) = &shared.on_error {
491        handler(&error);
492    }
493    let mut deferred = lock_recover(&shared.deferred);
494    if deferred.is_none() {
495        *deferred = Some(error);
496    }
497}
498
499fn run_worker(
500    mut writer: BufWriter<Box<dyn Write + Send>>,
501    rx: &Receiver<Message>,
502    pool: &SyncSender<String>,
503    shared: &Shared,
504) -> Box<dyn Write + Send> {
505    let mut dirty = false;
506    loop {
507        let message = match rx.try_recv() {
508            Ok(m) => m,
509            Err(TryRecvError::Empty) => {
510                // Queue drained: push what we have to the OS before
511                // sleeping, so records are not held back while idle.
512                if dirty {
513                    if let Err(e) = writer.flush() {
514                        report(shared, e);
515                    }
516                }
517                match rx.recv() {
518                    Ok(m) => m,
519                    Err(_) => break,
520                }
521            }
522            Err(TryRecvError::Disconnected) => break,
523        };
524        match message {
525            Message::Record(mut text) => {
526                if let Err(e) = writer.write_all(text.as_bytes()) {
527                    report(shared, e);
528                }
529                dirty = true;
530                trim_buffer(&mut text);
531                // Recycle the buffer; if the pool is full it is freed.
532                let _recycled = pool.try_send(text);
533            }
534            Message::Flush(ack) => {
535                dirty = false;
536                let _requester_may_be_gone = ack.send(writer.flush());
537            }
538            Message::FlushRequest => {
539                dirty = false;
540                if let Err(e) = writer.flush() {
541                    report(shared, e);
542                }
543            }
544            Message::Shutdown => break,
545        }
546    }
547    if let Err(e) = writer.flush() {
548        report(shared, e);
549    }
550    match writer.into_inner() {
551        Ok(inner) => inner,
552        Err(e) => {
553            let (error, writer) = e.into_parts();
554            report(shared, error);
555            // Hand back the unflushed buffer too; the synchronous
556            // fallback path retries through it.
557            Box::new(writer)
558        }
559    }
560}
561
562#[cfg(all(test, feature = "json"))]
563mod tests {
564    use super::*;
565    use crate::format::JsonFormat;
566    use crate::{Level, Metadata};
567    use std::sync::Barrier;
568
569    #[derive(Clone, Default)]
570    struct Capture(Arc<Mutex<Vec<u8>>>);
571    impl Write for Capture {
572        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
573            self.0.lock().unwrap().extend_from_slice(buf);
574            Ok(buf.len())
575        }
576        fn flush(&mut self) -> io::Result<()> {
577            Ok(())
578        }
579    }
580    impl Capture {
581        fn lines(&self) -> usize {
582            String::from_utf8(self.0.lock().unwrap().clone())
583                .unwrap()
584                .lines()
585                .count()
586        }
587    }
588
589    fn record(msg: &str) -> Record<'_> {
590        Record::new(Metadata::new(Level::Info, "t"), msg, &[])
591    }
592
593    #[test]
594    fn test_non_blocking_flush_waits_for_queued_records() {
595        let out = Capture::default();
596        let sink = NonBlocking::new(out.clone(), JsonFormat::new()).unwrap();
597        for _ in 0..1_000 {
598            sink.write_record(&record("x")).unwrap();
599        }
600        sink.flush().unwrap();
601        assert_eq!(out.lines(), 1_000);
602    }
603
604    #[test]
605    fn test_non_blocking_shutdown_drains_then_writes_synchronously() {
606        let out = Capture::default();
607        let sink = NonBlocking::new(out.clone(), JsonFormat::new()).unwrap();
608        for _ in 0..100 {
609            sink.write_record(&record("queued")).unwrap();
610        }
611        sink.shutdown().unwrap();
612        assert_eq!(out.lines(), 100);
613        // Late record after shutdown: written inline, not lost.
614        sink.write_record(&record("late")).unwrap();
615        sink.flush().unwrap();
616        sink.shutdown().unwrap();
617        assert_eq!(out.lines(), 101);
618    }
619
620    #[test]
621    fn test_non_blocking_drop_newest_counts_drops() {
622        // A writer that blocks until released, so the queue fills.
623        struct Gate(Arc<Barrier>, Capture, bool);
624        impl Write for Gate {
625            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
626                if !self.2 {
627                    self.2 = true;
628                    let _leader = self.0.wait();
629                }
630                self.1.write(buf)
631            }
632            fn flush(&mut self) -> io::Result<()> {
633                Ok(())
634            }
635        }
636        let gate = Arc::new(Barrier::new(2));
637        let out = Capture::default();
638        let sink = NonBlocking::builder(Gate(gate.clone(), out.clone(), false), JsonFormat::new())
639            .capacity(2)
640            .overflow(Overflow::DropNewest)
641            .buffer_capacity(0)
642            .build()
643            .unwrap();
644        for _ in 0..50 {
645            sink.write_record(&record("burst")).unwrap();
646        }
647        let _leader = gate.wait();
648        sink.flush().unwrap();
649        let written = out.lines() as u64;
650        assert!(sink.dropped() > 0, "the queue must have overflowed");
651        assert_eq!(
652            written + sink.dropped(),
653            50,
654            "every record is written or counted"
655        );
656    }
657
658    #[test]
659    fn test_non_blocking_write_errors_are_counted_and_deferred_to_flush() {
660        struct Failing;
661        impl Write for Failing {
662            fn write(&mut self, _: &[u8]) -> io::Result<usize> {
663                Err(io::Error::other("disk full"))
664            }
665            fn flush(&mut self) -> io::Result<()> {
666                Ok(())
667            }
668        }
669        let seen = Arc::new(AtomicU64::new(0));
670        let seen2 = Arc::clone(&seen);
671        let sink = NonBlocking::builder(Failing, JsonFormat::new())
672            .buffer_capacity(0)
673            .thread_name("log-io-test-writer")
674            .on_error(Arc::new(move |_| {
675                let _n = seen2.fetch_add(1, Ordering::Relaxed);
676            }))
677            .build()
678            .unwrap();
679        sink.write_record(&record("lost")).unwrap();
680        sink.write_record(&record("lost too")).unwrap();
681        let err = sink.flush().unwrap_err();
682        assert!(matches!(err, Error::Io(_)), "{err}");
683        assert_eq!(sink.write_errors(), 2);
684        assert_eq!(seen.load(Ordering::Relaxed), 2);
685        assert!(sink.flush().is_ok(), "the deferred error is reported once");
686        assert!(format!("{sink:?}").contains("write_errors: 2"));
687    }
688
689    #[test]
690    fn test_non_blocking_many_producers_lose_nothing_with_block() {
691        let out = Capture::default();
692        let sink = Arc::new(
693            NonBlocking::builder(out.clone(), JsonFormat::new())
694                .capacity(16)
695                .build()
696                .unwrap(),
697        );
698        std::thread::scope(|s| {
699            for _ in 0..8 {
700                let sink = Arc::clone(&sink);
701                let _handle = s.spawn(move || {
702                    for _ in 0..500 {
703                        sink.write_record(&record("p")).unwrap();
704                    }
705                });
706            }
707        });
708        sink.shutdown().unwrap();
709        assert_eq!(out.lines(), 8 * 500);
710        assert_eq!(sink.dropped(), 0);
711    }
712
713    #[test]
714    fn test_non_blocking_worker_panic_reports_closed() {
715        struct Panics;
716        impl Write for Panics {
717            fn write(&mut self, _: &[u8]) -> io::Result<usize> {
718                panic!("writer exploded");
719            }
720            fn flush(&mut self) -> io::Result<()> {
721                Ok(())
722            }
723        }
724        let sink = NonBlocking::builder(Panics, JsonFormat::new())
725            .buffer_capacity(0)
726            .build()
727            .unwrap();
728        let _queued = sink.write_record(&record("boom"));
729        assert!(matches!(sink.shutdown(), Err(Error::Closed)));
730        assert!(matches!(
731            sink.write_record(&record("after")),
732            Err(Error::Closed)
733        ));
734        let builder = NonBlocking::builder(Vec::<u8>::new(), JsonFormat::new()).capacity(0);
735        assert!(format!("{builder:?}").contains("capacity: 1"));
736    }
737
738    #[test]
739    fn test_non_blocking_builder_rejects_bad_options_without_panicking() {
740        // LOG-R07: both used to panic inside `build`.
741        let err = NonBlocking::builder(Vec::<u8>::new(), JsonFormat::new())
742            .thread_name("a\0b")
743            .build()
744            .unwrap_err();
745        assert!(matches!(err, Error::Configuration(_)), "{err}");
746        let builder =
747            NonBlocking::builder(Vec::<u8>::new(), JsonFormat::new()).capacity(usize::MAX / 2);
748        assert!(format!("{builder:?}").contains("capacity: 1048576"));
749    }
750
751    /// Blocks its first write until `release` is signalled.
752    struct Stall(Option<mpsc::Receiver<()>>, Capture);
753    impl Write for Stall {
754        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
755            if let Some(gate) = self.0.take() {
756                let _released = gate.recv();
757            }
758            self.1.write(buf)
759        }
760        fn flush(&mut self) -> io::Result<()> {
761            Ok(())
762        }
763    }
764
765    #[test]
766    fn test_non_blocking_drop_newest_flush_level_record_does_not_wait() {
767        // LOG-R03: with the default flush level, one error record used
768        // to wait until the whole queue was written.
769        let (release, gate) = mpsc::channel();
770        let out = Capture::default();
771        let sink = NonBlocking::builder(Stall(Some(gate), out.clone()), JsonFormat::new())
772            .capacity(4)
773            .overflow(Overflow::DropNewest)
774            .buffer_capacity(0)
775            .build()
776            .unwrap();
777        let logger = crate::Logger::builder()
778            .level(Level::Trace)
779            .no_timestamps()
780            .with_sink(sink)
781            .build();
782        for _ in 0..20 {
783            logger.log(Level::Info, "queued", &[]);
784        }
785        let (done, returned) = mpsc::channel();
786        let error_logger = logger.clone();
787        let handle = std::thread::spawn(move || {
788            error_logger.log(Level::Error, "error", &[]);
789            let _sent = done.send(());
790        });
791        let waited = returned.recv_timeout(std::time::Duration::from_secs(10));
792        release.send(()).unwrap();
793        handle.join().unwrap();
794        assert!(
795            waited.is_ok(),
796            "a flush-level record must not wait for the writer under DropNewest"
797        );
798        // An explicit flush still waits for everything queued.
799        logger.flush().unwrap();
800        assert!(out.lines() >= 1);
801    }
802
803    #[test]
804    fn test_non_blocking_worker_logging_into_its_own_sink_never_deadlocks() {
805        // The writer logs (at the flush level) through a logger that
806        // feeds this same sink, with a one-slot `Block` queue.
807        struct Echo {
808            logger: Arc<std::sync::OnceLock<crate::Logger>>,
809            echoes: usize,
810            flush_from_worker: Arc<Mutex<Option<bool>>>,
811        }
812        impl Write for Echo {
813            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
814                if let Some(logger) = self.logger.get() {
815                    if self.echoes < 50 {
816                        self.echoes += 1;
817                        logger.log(Level::Error, "echo from the writer", &[]);
818                        let closed = matches!(logger.flush(), Err(Error::Closed));
819                        *self.flush_from_worker.lock().unwrap() = Some(closed);
820                    }
821                }
822                Ok(buf.len())
823            }
824            fn flush(&mut self) -> io::Result<()> {
825                Ok(())
826            }
827        }
828        let cell = Arc::new(std::sync::OnceLock::new());
829        let flush_from_worker = Arc::new(Mutex::new(None));
830        let sink = NonBlocking::builder(
831            Echo {
832                logger: Arc::clone(&cell),
833                echoes: 0,
834                flush_from_worker: Arc::clone(&flush_from_worker),
835            },
836            JsonFormat::new(),
837        )
838        .capacity(1)
839        .buffer_capacity(0)
840        .build()
841        .unwrap();
842        let logger = crate::Logger::builder()
843            .level(Level::Trace)
844            .no_timestamps()
845            .with_sink(sink)
846            .build();
847        let _set = cell.set(logger.clone());
848        let (done, finished) = mpsc::channel();
849        let _detached = std::thread::spawn(move || {
850            for _ in 0..200 {
851                logger.log(Level::Info, "from a producer", &[]);
852            }
853            let shut = logger.shutdown();
854            let _sent = done.send(shut.is_ok());
855        });
856        let outcome = finished.recv_timeout(std::time::Duration::from_secs(30));
857        assert_eq!(outcome, Ok(true), "the worker waited for itself");
858        assert_eq!(
859            *flush_from_worker.lock().unwrap(),
860            Some(true),
861            "a flush from the worker thread reports `Closed` instead of waiting"
862        );
863        // The writer (now the sink's synchronous fallback) holds the
864        // logger that holds the sink: a deliberate cycle, leaked here.
865        drop(cell);
866    }
867
868    #[test]
869    fn test_non_blocking_drop_without_logger_drains_the_queue() {
870        let out = Capture::default();
871        let sink = NonBlocking::new(out.clone(), JsonFormat::new()).unwrap();
872        for _ in 0..100 {
873            sink.write_record(&record("x")).unwrap();
874        }
875        drop(sink);
876        assert_eq!(out.lines(), 100);
877    }
878}