buffer_ring 0.2.1

A high-performance, latch-free log-structured storage layer built for modern flash storage and multi-core systems.
Documentation

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:

  1. Identify the page state to write
  2. Seize space in the active buffer via atomic fetch-and-add
  3. Check atomically whether reservation succeeded; if the buffer is full, seal and rotate
  4. 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 std::sync::Arc;
use buffer_ring::{BufferRing, BufferRingOptions, QuikIO};

// Create a ring with 4 buffers, 1 MB each, auto-flushing enabled
let options = BufferRingOptions {
    capacity: 4,
    buffer_size: buffer_ring::ONE_MEGABYTE_BLOCK,
    io_instance: Some(Arc::new(QuikIO::new(file))),
    auto_flush: true,
    auto_rotate: true,
};

let ring = BufferRing::with_options(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 std::sync::Arc;
use buffer_ring::{BufferRing, BufferRingOptions, QuikIO};

let flusher = Arc::new(QuikIO::new(file));

// Create a ring with MANUAL flushing
let options = BufferRingOptions {
    capacity: 4,
    buffer_size: buffer_ring::ONE_MEGABYTE_BLOCK,
    io_instance: Some(flusher),
    auto_flush: false,  // Disable automatic flushing
    auto_rotate: true,
};

let ring = BufferRing::with_options(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(Ordering::Acquire);
if current.is_sealed() {
    // Implement your custom flushing logic here
    ring.flush(&current);
}

// Or flush the current buffer explicitly at any time
let current = ring.current_buffer(Ordering::Acquire);
ring.flush(&current);

// Or flush a specific buffer
ring.flush(&my_buffer);

Builder Configuration

BufferRingOptions provides configuration via struct fields:

let options = BufferRingOptions {
    capacity: 8,                      // Number of buffers in the ring
    buffer_size: buffer_ring::ONE_MEGABYTE_BLOCK,  // Size of each buffer
    io_instance: Some(flusher),       // I/O dispatcher
    auto_flush: true,                 // Enable/disable auto-flush
    auto_rotate: true,                // Enable/disable auto-rotate
};

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_DIRECT compatibility. The constant ONE_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