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