# How tui-logger’s Dual Circular Buffer Architecture Prevents Logging Deadlocks

> Learn how tui-logger's dual circular buffer architecture prevents logging deadlocks by isolating writes and atomically swapping buffers. Maintain high performance and avoid lock contention.

- Repository: [gin66/tui-logger](https://github.com/gin66/tui-logger)
- Tags: architecture
- Published: 2026-03-02

---

**tui-logger prevents logging deadlocks by isolating fast-path log writes to a small, short-locked hot buffer while a background thread atomically swaps and drains it to a larger main buffer, ensuring the two mutexes are never held simultaneously.**

The `gin66/tui-logger` crate implements a high-performance logging backend for terminal user interfaces that must handle concurrent log production without blocking the main application thread. By employing a **dual circular buffer architecture** with separate hot and main buffers, the library eliminates the circular wait conditions that typically cause logging deadlocks in multi-threaded Rust applications.

## The Hot Buffer Fast Path

Every log invocation travels through the `raw_log` method in [`src/logger/inner.rs`](https://github.com/gin66/tui-logger/blob/main/src/logger/inner.rs), which interacts exclusively with the **hot circular buffer** (`hot_log.events`).

This buffer is protected by a lightweight `Mutex` that is held only for the duration of a single `push` operation. According to the source at [`inner.rs#L94-L99`](https://github.com/gin66/tui-logger/blob/master/src/logger/inner.rs#L94-L99), the critical section is minimal:

- The calling thread acquires the hot buffer lock
- The `ExtLogRecord` is pushed onto the circular buffer
- The lock is released immediately

Because the hot buffer is sized for short-term buffering (default 1000 entries, configurable via `set_hot_buffer_depth`), this operation completes in constant time regardless of the total log volume, preventing priority inversion scenarios where a logging thread would block waiting for buffer compaction or flush operations.

## Atomic Swapping via the Background Thread

The deadlock prevention mechanism centers on the background thread spawned by `init_logger` in [`src/logger/api.rs`](https://github.com/gin66/tui-logger/blob/main/src/logger/api.rs), which periodically invokes `move_events`. This routine performs an atomic swap that disconnects the producer and consumer concerns.

At [`inner.rs#L34-L40`](https://github.com/gin66/tui-logger/blob/master/src/logger/inner.rs#L34-L40), the implementation follows this sequence:

1. Lock the hot buffer mutex
2. Execute `mem::replace` to atomically swap the full hot buffer with a fresh, empty circular buffer
3. Release the hot buffer lock immediately
4. Process the swapped-out buffer

This approach ensures that the hot buffer mutex is never held during the potentially slower operation of draining records into the main buffer. The atomic swap operation completes in nanoseconds, minimizing contention windows even under extreme load.

## Main Buffer Processing Without Lock Contention

Once the hot buffer is swapped out, the background thread drains its contents into the **main circular buffer** (`inner.events`) without holding any locks on the hot buffer. This separation is critical: the two buffers use distinct mutexes (`hot_log` vs `inner`), and the architecture ensures these locks are never acquired simultaneously by any single thread.

If a new log record arrives while the background thread is processing the old hot buffer, it is written to the freshly swapped-in hot buffer immediately. This **decouples fast logging from slower batch processing**, eliminating the classic deadlock scenario where a thread cannot proceed because it needs a lock held by a thread waiting for I/O to complete.

## Configuration Tuning for Throughput

The hot buffer depth is exposed through the public API as `set_hot_buffer_depth`, allowing applications to tune the architecture for their specific workload characteristics:

- **High-frequency logging**: Increase depth to reduce the probability that a logging call must wait for the background thread to complete a swap cycle
- **Memory-constrained environments**: Decrease depth to reduce RAM usage while accepting higher contention risk

Proper sizing ensures that the background thread can keep pace with the production rate, further reducing the chance that a logging call blocks long enough to cause contention or trigger application-level timeouts.

## Implementation Walkthrough

The dual-buffer strategy spans three core files in the repository:

- [`src/circular.rs`](https://github.com/gin66/tui-logger/blob/main/src/circular.rs): Generic circular buffer implementation used for both hot and main storage
- [`src/logger/inner.rs`](https://github.com/gin66/tui-logger/blob/main/src/logger/inner.rs): Core logger struct, hot-buffer handling, `raw_log`, and `move_events` logic
- [`src/logger/api.rs`](https://github.com/gin66/tui-logger/blob/main/src/logger/api.rs): Public API initialization and the background mover thread management

The following example demonstrates typical initialization:

```rust
use tui_logger::{self, LevelFilter};

fn main() -> Result<(), tui_logger::TuiLoggerError> {
    // Spawn the background mover thread
    tui_logger::init_logger(LevelFilter::Info)?;

    // Configure hot buffer depth for your workload
    tui_logger::set_hot_buffer_depth(2000);

    // These macros only touch the hot buffer
    log::info!("Application initialized");
    log::debug!("Debug vector: {:?}", vec![1, 2, 3]);

    // Background thread automatically moves events to main buffer
    Ok(())
}

```

In this implementation, `log::info!` and `log::debug!` calls only acquire the hot buffer lock briefly, while the background thread handles the transfer to the main buffer independently.

## Summary

- **Hot buffer isolation**: Log writes only lock the lightweight hot buffer (`hot_log.events`) for microseconds via `raw_log`
- **Atomic swapping**: `move_events` uses `mem::replace` at `inner.rs#L34-L40` to swap buffers while holding the lock, then releases immediately
- **Lock separation**: The hot buffer and main buffer (`inner.events`) use distinct mutexes that are never held simultaneously
- **Background processing**: Draining to the main buffer occurs without blocking new log writes
- **Tunable capacity**: `set_hot_buffer_depth` allows workload-specific optimization to prevent overflow

## Frequently Asked Questions

### How does tui-logger ensure that logging never blocks the main thread?

The library maintains a small, dedicated hot buffer that absorbs log writes with minimal locking. Because the hot buffer is swapped atomically by a background thread, application code never waits for disk I/O, TUI rendering, or buffer consolidation when calling standard log macros.

### What happens if the hot buffer fills up before the background thread swaps it?

If the hot buffer reaches capacity before `move_events` completes a swap cycle, subsequent log calls will block briefly until the swap occurs. This is why `set_hot_buffer_depth` exists—to allow applications to size the buffer appropriately for their peak logging throughput and prevent back-pressure on application threads.

### Why does the dual buffer architecture prevent deadlocks specifically?

Deadlocks require circular wait conditions where Thread A holds Lock 1 and waits for Lock 2 while Thread B holds Lock 2 and waits for Lock 1. In tui-logger, the hot buffer lock is always released before the main buffer lock is acquired, and the background thread never holds both simultaneously. The atomic swap at `inner.rs#L34-L40` breaks the dependency chain that would otherwise create a circular wait.

### Can I disable the background thread and manage buffer flushing manually?

No, the `init_logger` function spawns the mover thread automatically and it runs for the lifetime of the application. This design is fundamental to the deadlock-prevention guarantees; manual flushing would reintroduce the risk of holding multiple locks simultaneously or blocking application threads during I/O operations.