///|
/// A contiguous, well-formed slice of a raft log considered under a specific
/// leader `term`. `prev` is the entry immediately before `entries`. Mirrors
/// etcd's `logSlice`, whose invariants a well-formed append must satisfy:
/// entries are contiguous after `prev`, entry terms never regress, and no entry
/// carries a term newer than the leader term.
pub(all) struct LogSlice {
term : UInt64
prev : EntryId
entries : Array[Entry]
}
///|
/// The index of the last entry, or `prev.index` when the slice is empty.
pub fn LogSlice::last_index(self : LogSlice) -> UInt64 {
self.prev.index + self.entries.length().to_uint64()
}
///|
/// The identity of the last entry, or `prev` when the slice is empty.
pub fn LogSlice::last_entry_id(self : LogSlice) -> EntryId {
let n = self.entries.length()
if n != 0 {
self.entries[n - 1].id()
} else {
self.prev
}
}
///|
/// Whether the slice is well-formed: every entry follows the previous one by
/// exactly one index, entry terms never regress below the preceding entry, and
/// the last entry's term does not exceed the leader term. This is the "gateway"
/// check etcd runs on a slice sourced from a message or from storage.
pub fn LogSlice::valid(self : LogSlice) -> Bool {
let mut prev = self.prev
for e in self.entries {
let id = e.id()
if id.term < prev.term || id.index != prev.index + 1 {
return false
}
prev = id
}
self.term >= prev.term
}
///|
/// One nibble as a lowercase hex digit.
fn hex_digit(n : Int) -> Char {
if n < 10 {
('0'.to_int() + n).unsafe_to_char()
} else {
('a'.to_int() + (n - 10)).unsafe_to_char()
}
}
///|
/// Go `%q`-style quoting of a byte string: wrap in double quotes, escaping the
/// quote, backslash and the usual control characters, and rendering any other
/// non-printable byte as `\xHH`. This is the default `describe_entry` renderer.
fn quote_bytes(data : Bytes) -> String {
let buf = StringBuilder::new()
buf.write_char('"')
for i in 0..= 0x20 && b <= 0x7e {
buf.write_char(b.unsafe_to_char())
} else {
buf.write_string("\\x")
buf.write_char(hex_digit(b / 16))
buf.write_char(hex_digit(b % 16))
}
}
buf.write_char('"')
buf.to_string()
}
///|
/// A concise, human-readable description of an entry for debugging:
/// `term/index Type payload`. `format` renders the payload; when it is `None`
/// the default Go `%q`-style quoting is used. Mirrors etcd's `DescribeEntry`.
pub fn describe_entry(e : Entry, format : ((Bytes) -> String)?) -> String {
let formatted = match format {
Some(f) => f(e.command)
None => quote_bytes(e.command)
}
let type_name = match e.entry_type {
Normal => "EntryNormal"
ConfChange => "EntryConfChange"
}
let head = "\{e.term}/\{e.index} \{type_name}"
if formatted != "" {
head + " " + formatted
} else {
head
}
}
///|
/// Each entry described, one per line (etcd's `DescribeEntries`).
pub fn describe_entries(
entries : ArrayView[Entry],
format : ((Bytes) -> String)?,
) -> String {
let buf = StringBuilder::new()
for e in entries {
buf.write_string(describe_entry(e, format))
buf.write_char('\n')
}
buf.to_string()
}
///|
/// Render a set of ids the way Go's `%v` renders a slice: `[a b c]`.
fn describe_ids(ids : ArrayView[String]) -> String {
let buf = StringBuilder::new()
buf.write_char('[')
let mut first = true
for id in ids {
if !first {
buf.write_char(' ')
}
buf.write_string(id)
first = false
}
buf.write_char(']')
buf.to_string()
}
///|
/// A concise description of a HardState for debugging (etcd's
/// `DescribeHardState`): `Term:N [Vote:v ]Commit:N`, the vote shown only when a
/// vote was cast.
pub fn describe_hard_state(hs : HardState) -> String {
let vote = match hs.vote {
Some(v) => " Vote:\{v}"
None => ""
}
"Term:\{hs.term}\{vote} Commit:\{hs.commit}"
}
///|
/// A concise description of a ConfState (etcd's `DescribeConfState`).
pub fn describe_conf_state(cs : ConfState) -> String {
"Voters:\{describe_ids(cs.voters[:])} VotersOutgoing:\{describe_ids(cs.voters_outgoing[:])} Learners:\{describe_ids(cs.learners[:])} LearnersNext:\{describe_ids(cs.learners_next[:])} AutoLeave:\{cs.auto_leave}"
}
///|
/// A concise description of a Snapshot (etcd's `DescribeSnapshot`).
pub fn describe_snapshot(snap : Snapshot) -> String {
"Index:\{snap.last_index} Term:\{snap.last_term} ConfState:\{describe_conf_state(snap.conf_state)}"
}