open-wal 0.2.1

An embedded Write-Ahead Log (WAL) library for Rust.
Documentation
# Single-writer by construction

`open-wal` allows exactly one writer per log directory. This is not a
limitation to be worked around — it is a load-bearing design decision, and the
crate spends type-system and OS machinery making it impossible to violate by
accident.

## Why single-writer

A multi-writer log needs interior locking, write coordination, and a much
larger correctness argument — precisely the machinery an LMAX-style system
exists to avoid. With one writer:

- The hot path has no locks, no atomics, no contention — `append` is a plain
  memory write into a buffer owned by one thread.
- LSN assignment is trivially dense and ordered.
- The crash-recovery story stays tractable: every on-disk state is the result
  of one sequential writer stopping at some point, which is what makes the
  [recovery]recovery.md classification (torn tail vs. real corruption)
  sound.

If you have multiple producers, put a queue in front of the single writer
thread — that is the intended integration shape, not a workaround.

## Two layers of enforcement

**Within a process: the type system.** `Wal` is `Send` but deliberately
**not `Sync`**, and every write method takes `&mut self`. You can move the
handle to another thread, but you cannot share it between two: `Arc<Wal>`
gives you no way to call `append`, and `&Wal` cannot cross threads. Concurrent
writers are a *compile error*. (This is verified in CI with a compile-fail
test.)

**Between processes: an OS lock.** `open` takes an exclusive advisory `flock`
on a `LOCK` file in the WAL directory and holds it for the handle's lifetime.
A second process (or a second handle in the same process) gets a clean error:

```rust,ignore
let (wal, _) = Wal::open(dir, WalConfig::default())?;
match Wal::open(dir, WalConfig::default()) {
    Err(open_wal::WalError::Locked) => { /* expected: one writer at a time */ }
    Err(other) => panic!("expected Locked, got {other:?}"),
    Ok(_) => panic!("expected Locked, got a second writer"),
}
drop(wal); // releases the lock; now a new writer may open
let (_wal, _) = Wal::open(dir, WalConfig::default())?;
```

One operational wrinkle: after a writer process *crashes*, the OS may release
its lock a beat after the process disappears, so an immediate restart can see
a transient `Locked`. Retry briefly (up to ~1 s) before concluding another
writer is alive — see [Recovery](recovery.md#one-operational-note-reopening-after-a-crash).

Note that read-only access is not writer-locked: `Reader`s borrow the writer
in-process, and external readers attach without touching the exclusive lock
(see [External readers](external-access.md)).

## What the integrator owns

Single-writer scoping means the crate deliberately stops at the durability
boundary. Around it, you own:

- **The publish/ack barrier.** The WAL returns `durable_lsn`; gating
  downstream consumers on it (an atomic cursor, a channel — the LMAX
  daisy-chain) is your code. The
  [`DurabilityObserver`]external-access.md#publishing-the-watermark-durabilityobserver
  hook exists to feed it.
- **Serialization.** Payloads are opaque bytes; encode/decode however you
  like, subject only to `max_record_size`.
- **Snapshots and the checkpoint trigger.** The WAL reclaims what you tell it
  to; knowing what is safe to reclaim requires your snapshot — see
  [Checkpointing]checkpointing.md.
- **Replication transport and failover**, if any — see
  [External readers]external-access.md.

A common deployment runs **two independent instances** — an input journal and
an output journal. Each has its own directory, lock, LSN space, and recovery;
the WAL neither knows nor cares that the other exists.