Buffer Ring
A latch-free I/O buffer ring implementation for concurrent log-structured storage. Built on io_uring for efficient asynchronous I/O operations on Linux systems. Intended to be the sole write path for Bloom_lfs.
Overview
This crate implements a fixed-size ring of 4-KILOBYTE-aligned buffers that amortizes individual I/O operations into larger, sequential writes before they are dispatched to stable storage. It provides latch-free concurrent access using a single packed atomic state word per buffer, making it suitable for high-throughput scenarios where multiple threads need to write to the same buffer simultaneously.
Key Features
- Latch-free writes: No global locks; all state is managed through atomic operations
- O_DIRECT compatible: All buffers are 4-KILOBYTE aligned for direct I/O
- Concurrent amortization: Multiple threads fill one buffer before flush
- Flexible flushing: Automatic or manual control over when buffers are dispatched
- Ring-based rotation: Seamlessly rotates to the next buffer when sealed
System Architecture
State Word Layout
All per-buffer metadata is packed into a single AtomicUsize, ensuring self-consistent snapshots:
┌────────────────┬────────────────┬──────────────────┬───────────────────┬──────────┐
│ Bits 63..32 │ Bits 31..8 │ Bits 7..2 │ Bit 1 │ Bit 0 │
│ write offset │ writer count │ (reserved) │ flush-in-prog │ sealed │
└────────────────┴────────────────┴──────────────────┴───────────────────┴──────────┘
Flush Protocol
The ring implements the flush protocol from the LLAMA paper without global locks:
- Identify the page state to write
- Seize space in the active buffer via atomic fetch-and-add
- Check atomically whether reservation succeeded; if the buffer is full, seal and rotate
- Write payload into reserved range while flush-in-progress bit prevents premature dispatch
Usage
Basic Setup with Automatic Flushing
For most applications, automatic flushing should be enabled by default:
use Arc;
use ;
// Create a ring with 4 buffers, 1 MB each, auto-flushing enabled
let options = BufferRingOptions ;
let ring = with_options;
Manual Flushing for Custom Protocols
If you need to implement custom buffer protocols or have specific flushing requirements, opt out of automatic flushing:
use Arc;
use ;
let flusher = new;
// Create a ring with MANUAL flushing
let options = BufferRingOptions ;
let ring = with_options;
// Now you have full control over when buffers are flushed
Key Methods for Manual Flushing
When auto_flush is false, use these methods to control flushing:
// Check if the current buffer is sealed (full)
let current = ring.current_buffer;
if current.is_sealed
// Or flush the current buffer explicitly at any time
let current = ring.current_buffer;
ring.flush;
// Or flush a specific buffer
ring.flush;
Builder Configuration
BufferRingOptions provides configuration via struct fields:
let options = BufferRingOptions ;
Configuration Details
| Field | Type | Default | Description |
|---|---|---|---|
capacity |
usize |
0 | Number of buffers in the ring |
buffer_size |
usize |
0 | Size of each buffer (must be multiple of 4096) |
io_instance |
Option<Arc<QuikIO>> |
None | I/O dispatcher (test mode if None) |
auto_flush |
bool |
true | Automatically flush when buffer sealed |
auto_rotate |
bool |
true | Automatically rotate to next buffer |
Note: Buffer size is fixed at multiples of 4096 for
O_DIRECTcompatibility. The constantONE_MEGABYTE_BLOCK(1 MB) is recommended.
When to Use Manual Flushing
Choose manual flushing (auto_flush: false) when:
- Implementing custom buffer protocols or serialization formats
- You need explicit control over flush timing for performance tuning
- You must batch multiple buffers before dispatching to storage
- Your workload has specific flush semantics beyond simple "on seal" behavior
Choose automatic flushing (auto_flush: true, the default) when:
- You want simplicity and predictable, automatic I/O dispatch
- Standard log-structured storage semantics are sufficient
- Thread safety and lock-free concurrency are your priorities