///|
/// A write-ahead log over object storage, coordinated by nothing but
/// compare-and-swap.
///
/// The shape is waltier's: the whole live log is **one small object** — the
/// WAL image — holding a snapshot pointer plus the live entries, rewritten by
/// compare-and-swap on every commit. Large payloads are separate immutable
/// objects that entries only reference. See `docs/scalable-git-server.md`.
///
/// Keeping the entries *inside* the image is what makes the design safe
/// rather than merely simple. A log made of one object per sequence number
/// has to answer what an entry that was written but never committed means:
/// skipping it lets a later writer commit a range that contains it, and
/// waiting for it deadlocks on a writer that died. Here an entry does not
/// exist until the image that contains it lands, so there is no such state.
///
/// Nothing in this module knows about Git.
///|
/// What an entry records. The engine does not interpret `payload_keys` or
/// `data`; it guarantees only their ordering and durability.
pub(all) struct WalEntry {
/// Position in the log, assigned by `append`. Ignored on input.
seq : Int64
/// Caller-defined discriminator, e.g. `push`, `settings`.
kind : String
/// Content-addressed payload keys this entry refers to.
payload_keys : Array[String]
/// Opaque caller data. The engine treats it as text.
data : String
/// Distinguishes a re-sent entry from a genuinely new one.
///
/// This is what makes recovery from an indeterminate write safe: after an
/// `Unknown` the writer re-reads the image and looks for its own key rather
/// than guessing, so entries are never applied twice.
idempotency_key : String
} derive(Eq)
///|
pub fn WalEntry::new(
kind : String,
data : String,
payload_keys? : Array[String] = [],
idempotency_key? : String = "",
) -> WalEntry {
{ seq: 0L, kind, payload_keys, data, idempotency_key }
}
///|
/// The WAL image: the only object whose rewrite makes anything visible.
pub(all) struct WalImage {
version : Int
/// Snapshot object key, or empty when nothing has been folded yet.
snapshot : String
/// The sequence the snapshot folds up to, inclusive.
snapshot_seq : Int64
/// Payload keys the snapshot still needs. Live entries carry their own.
snapshot_payloads : Array[String]
/// Entries after `snapshot_seq`, in order.
entries : Array[WalEntry]
/// Caller-defined configuration, carried here so a replica picks it up with
/// the same conditional read that tells it the log moved.
settings : String
} derive(Eq)
///|
pub fn WalImage::empty() -> WalImage {
{
version: WAL_FORMAT_VERSION,
snapshot: "",
snapshot_seq: 0L,
snapshot_payloads: [],
entries: [],
settings: "",
}
}
///|
/// Sequence of the last entry, folded or live.
pub fn WalImage::head_seq(self : WalImage) -> Int64 {
if self.entries.length() == 0 {
self.snapshot_seq
} else {
self.entries[self.entries.length() - 1].seq
}
}
///|
/// Every payload key the image still depends on. Anything else under the
/// payload prefix is garbage.
pub fn WalImage::live_payloads(self : WalImage) -> Array[String] {
let seen : Map[String, Bool] = Map([])
let out : Array[String] = []
for key in self.snapshot_payloads {
if !seen.contains(key) {
seen[key] = true
out.push(key)
}
}
for entry in self.entries {
for key in entry.payload_keys {
if !seen.contains(key) {
seen[key] = true
out.push(key)
}
}
}
out
}
///|
/// Bumped only for a change an older reader could not safely ignore.
pub const WAL_FORMAT_VERSION : Int = 1
///|
/// Outcome of a commit.
pub(all) enum CommitOutcome {
/// Entries are durable and visible. Carries the new head sequence.
Committed(Int64)
/// Another writer won every attempt within the retry budget. The log is
/// unchanged by this call.
Contended
} derive(Eq)
///|
pub impl Show for CommitOutcome with fn output(self, logger) {
match self {
Committed(seq) => logger.write_string("Committed(\{seq})")
Contended => logger.write_string("Contended")
}
}
///|
/// How hard `append` tries before reporting contention. Each attempt costs a
/// read and a conditional write, so this is a cost ceiling as much as a
/// correctness one.
pub const DEFAULT_MAX_ATTEMPTS : Int = 8
///|
/// Refuse to write an image larger than this.
///
/// waltier's equivalent default is 64 MiB. An image is rewritten in full on
/// every commit, so its size is the per-commit cost: letting it grow without
/// bound turns every push into a multi-megabyte round trip. Exceeding it
/// means compaction is overdue, which is a caller error rather than a
/// storage failure, so it is reported as one.
pub const DEFAULT_MAX_IMAGE_BYTES : Int = 64 * 1024 * 1024