nano-wal

A concurrent Write-Ahead Log (WAL) in Rust with lock-free segment rotation, vectored I/O, and coalesced batch reads. Designed for high-throughput, multi-threaded workloads where readers and writers must not block each other.
What's New in v1.0.0
v1.0.0 is a ground-up rewrite. The API has changed — this is not backward compatible with 0.5.x.
| Change | Benefit |
|---|---|
&self API with interior mutability |
Wal is Send + Sync — share via Arc<Wal> across threads with no external locking |
| CAS-based segment rotation | Lock-free rotation via ArcSwap. Multiple threads can trigger rotation without blocking each other |
| Dup'd read file descriptors | Readers use an independent fd via libc::dup() — zero contention with writers, no shared file position |
Coalesced preadv batch reads |
read_batch() groups reads by fd, detects contiguous runs, and coalesces up to 512 entries into a single syscall |
Vectored writes (writev) |
append_batch() writes multiple records in a single write_vectored call — one syscall for N records |
| Clock-aligned segment expiration | Segments rotate on predictable time boundaries, not ingestion time. Simplifies reasoning about retention |
| NANORC record framing | Each record has a 6-byte signature for boundary detection and corruption recovery |
| Structured cleanup results | cleanup() returns CleanupResult with deleted files, byte counts, and live segment count — wire up your own metrics |
| Startup recovery | Wal::new() scans for existing segments and reopens the latest non-expired one. Handles process restarts |
| Flat directory layout | Segments use a caller-provided prefix ({prefix}_{expiration}.seg). Multiple WAL instances can share a directory |
Dropped chrono dependency |
Timestamps are caller-provided i64 milliseconds. No runtime clock dependency |
Removed from 0.5.x
- Per-key hash-based segment routing (replaced by one WAL instance per stream)
enumerate_keys(),enumerate_records()(no key concept in v1)shutdown()that deletes all files (v1 shutdown is non-destructive)log_entry()convenience wrapperchronodependency
Installation
[]
= "1.0.0"
Quick Start
use ;
use Arc;
use Duration;
Concurrent Writers
Wal is Send + Sync. Share it across threads with Arc<Wal>:
use ;
use Arc;
use Duration;
let wal = new;
let mut handles = Vecnew;
for i in 0..4
for h in handles
Writers acquire a Mutex<File> for the active segment. Readers use an independent dup'd fd via preadv — no contention with writers.
Batch Operations
Write multiple records in a single writev syscall:
use ;
use Duration;
let wal = new.unwrap;
let entries = vec!;
let refs = wal.append_batch.unwrap;
assert_eq!;
Configuration
use WalOptions;
use Duration;
let options = WalOptions ;
Segments rotate on clock-aligned boundaries. Expiration is calculated as:
window_start = (ingestion_time / segment_duration) * segment_duration
expiration = window_start + segment_duration + retention
API Reference
Wal
| Method | Description |
|---|---|
new(dir, prefix, options) |
Create WAL, recover existing segments |
ensure_segment(ingestion_time) |
Get or create active segment (CAS rotation) |
append(header, content, ingestion_time, durable) |
Append single record |
append_batch(entries, ingestion_time, durable) |
Append multiple records in one writev |
read_at(segment, offset, size) |
Read single record via preadv |
cleanup() |
Delete expired segments, return CleanupResult |
sync() |
fdatasync the active segment |
shutdown() |
Sync and close. Further writes return WalError::Shutdown |
Segment
| Method | Description |
|---|---|
read_fd() |
Dup'd fd for lock-free preadv reads |
file_size() |
Total bytes written |
expiration_ms() |
Immutable expiration timestamp |
path() |
Filesystem path |
Free Function
| Function | Description |
|---|---|
read_batch(descriptors) |
Coalesced multi-entry read. Groups by fd, detects contiguous runs, minimal syscalls |
Types
EntryRef—{ file_offset: u64, byte_size: usize }returned from appendWriteEntry—{ header: Option<&[u8]>, content: &[u8] }for batch appendsReadDescriptor—{ read_fd: Arc<OwnedFd>, file_offset: u64, byte_size: usize }for batch readsRecord—{ header: Option<Bytes>, content: Bytes }returned from readsCleanupResult—{ deleted: Vec<DeletedSegment>, live_count: u64, bytes_reclaimed: u64 }
File Format
Segment file header (16 bytes)
[NANO-LOG: 8 bytes][expiration_ms: 8 bytes LE]
Record format
[NANORC: 6 bytes][header_len: 2 bytes LE][header: N bytes][content_len: 8 bytes LE][content: N bytes]
Filename convention
{prefix}_{expiration_ms}.seg
Error Handling
use WalError;
match result
Testing
License
MIT License. See LICENSE.