Expand description
A breadcrumb ring in shared memory, keyed to the artifact instead of the process.
§Why a second ring exists
The in-process flight recorder (crate::breadcrumbs) can only ever show
what this process did. But the corruption that motivated this crate is
caused by one process’s writes and detected by a different process’s open —
so the detecting process’s trail is, structurally, the wrong trail. It shows
the reopen that noticed the damage and nothing about the writes that did it.
A SharedRing is a fixed-size ring buffer in a memory-mapped file that
lives beside the artifact (the store), so every process that touches that
store appends to the same trail. A corruption report can then carry the last
writes from whichever process made them.
§Crash safety
Every writer that touches this file is, by assumption, a process that may be killed mid-write — that is the entire scenario. So the ring uses no locks that could be left held:
- A writer claims a slot with one atomic
fetch_addon a shared ticket counter. No lock, so nothing to leak. - Each slot is a seqlock: the sequence number is set odd before the body is written and even after. A reader that sees an odd sequence — or a sequence that changed while it was reading — skips that slot.
- A process dying mid-slot therefore costs exactly one breadcrumb. It cannot wedge, poison, or corrupt the ring for anyone else.
Slots are fixed-size and messages are truncated to fit, so recording never allocates and never blocks.
§Trust boundary
The ring file is shared mutable state. Only processes already trusted with the artifact should be able to write it, so it is created owner-only and a deployment that genuinely shares a store between accounts must widen it deliberately. A hostile writer can garble the trail (it cannot escape the mapping: every read is bounds-checked and every string is validated UTF-8), but a hostile writer with access to the store has better things to corrupt.
§Why every byte is an atomic
Other processes write this mapping while we read it. Forming a &[u8] or a
&mut [u8] over memory that can change underneath the reference is undefined
behaviour in Rust — a data race — no matter how carefully the seqlock
sequences the logical reads and writes. The seqlock decides which crumbs
are trustworthy; it cannot make a racing non-atomic access defined.
So the mapping is never viewed as a slice. Payload bytes are read and written
one relaxed AtomicU8 at a time, and a reader copies the slot into local
memory before parsing it, so all the shape-checking happens on bytes nothing
else can touch. Relaxed suffices because the sequence number’s
release/acquire pair supplies the ordering.
Structs§
- Shared
Ring - A bounded breadcrumb trail shared by every process that opens the same file.
Constants§
- MAX_
CAPACITY - The largest ring this crate will create: 1M slots, a 256 MiB mapping.
SharedRing::openclamps to it.