Shale
View SourceShale is Bedrock's local-disk transaction log. It provides ordered, fsync-durable transaction storage, exclusive range pulls for log recovery, and a Demux tree that turns the replicated transaction stream into per-shard object-storage chunks.
Location: lib/bedrock/data_plane/log/shale/
WAL Segments
Shale writes into preallocated 64 MiB segment files. A segment rolls when it is full or when a transaction crosses the Demux's deterministic cut boundary. The active segment is never trimmed; completed segments can be recycled once their last transaction is at or below the replica-local object-storage durability floor.
The versioned BED1 segment header contains the eight-byte
previous_version that was the WAL tip when the segment was created. Entries
then contain the commit version, payload length, original encoded transaction,
and CRC32, followed by an EOF marker. The header is twelve bytes:
BED1 | previous_versionThe first append writes the entry and EOF marker and fsyncs them together with the header before acknowledging. Thus an older segment cannot become trim-eligible before its successor durably records the exact predecessor cursor. An empty recovery range explicitly fsyncs the header by itself.
Cold start also reads non-empty legacy BED0 segments. Because versions are
unsigned integers, one less than the first retained version is an unambiguous
synthetic exclusive cursor; version zero remains zero. Empty BED0 segments
have no first retained version from which to derive that cursor and fail closed.
All newly written segments use BED1.
Ordered Appends
A push names the previous committed version. If it matches the WAL tip, Shale appends immediately. If it is greater, Shale parks the transaction until its predecessor arrives; if it is older, Shale rejects it. Commit versions may have arbitrary numeric gaps—the explicit predecessor chain defines order.
An append is acknowledged only after WAL fsync. The exact encoded binary that was appended is then passed unchanged to Demux. Demux alone slices mutations by shard, so crossing the process boundary does not require rebuilding the transaction binary.
Known committed version (KCV) is a separate global monotonic watermark. Demux
accumulates it with max, including while a future transaction is parked, but
does not confuse KCV progress with transaction high-water.
Pull and Availability Semantics
Log.pull/3 returns transactions in (start_after, last_inclusive]. Recovery
is its only WAL consumer; materializers stream from ShardServers instead.
Shale exposes both:
available_after: the persisted exclusive cursor after which all retained WAL transactions are available;oldest_version: informational data about the first retained transaction.
They are intentionally different. Using oldest_version as an exclusive
cursor would skip the transaction it names.
Recovery
Cold start enumerates segment files, reads every BED1 predecessor, and derives
available_after from the oldest retained segment. The WAL tip is the newest
transaction, or the header cursor for an empty baseline.
Log-to-log recovery has one range: (replay_after, last_inclusive]. It resets
the destination to logical position replay_after without appending a sentinel,
copies each real transaction byte-for-byte, and sends it through a fresh Demux
exactly once. Success requires observing last_inclusive; an empty response
before it is an error. When the range is empty, Shale persists only a header
baseline, which survives restart and anchors the next push.
WAL Trimming
Each Demux commands version-time cuts gated by KCV. Its ShardServers confirm a
cut only after their deterministic chunks are present in object storage. The
minimum confirmation from that log's own children advances its trim floor.
Shale recycles only completed segments fully behind the floor and recomputes
available_after from the oldest segment that remains.
Trim-floor telemetry reports lag and segment counts. Growth is
unbounded-with-alerting by default. The optional hard limit is an epoch-fatal
safety fuse, not retryable per-push backpressure. Shale checks each prospective
commit version, including queued successors when a predecessor gap closes. If
the version-time lag exceeds the limit, every affected caller is released with
{:error, {:recovery_required, {:wal_limit_exceeded, details}}} and the commit
proxy stops so the Director can recover the known-committed prefix. Continuing
the epoch would be unsafe because the sequencer and resolvers have already
incorporated the refused version, and some log replicas may already have
fsynced it.
The fuse emits [:bedrock, :log, :wal_limit_exceeded] at error severity with
the floor, current tip, prospective version, lag, configured limit, and queued
count. Recovery replay is exempt because it copies history already selected as
committed.
Code References
writer.ex: versioned headers, entry encoding, fsyncsegment.exandcold_starting.ex: segment metadata and restartpushing.exandpulling.ex: predecessor-chain appends and exclusive pullsrecovery.ex: endpoint-proven log-to-log replayserver.ex: lifecycle, facts, durability-floor trimming