// Copyright 2015 The etcd Authors
// Copyright 2026 Leo Cheng
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Ported from etcd-io/raft (Apache-2.0): quorum/majority.go, quorum/joint.go.
// CommittedIndex / VoteResult for majority and joint-consensus quorums.
///|
/// The outcome of counting votes against a configuration: still pending (no
/// majority either way), won (a majority granted), or lost (a majority denied).
pub(all) enum VoteState {
VoteWon
VoteLost
VotePending
} derive(Eq)
///|
/// The largest UInt64, standing in for "no constraint": the committed index of
/// an empty voter set, so that in a joint quorum an empty half defers entirely
/// to the other half.
const MAX_U64 : UInt64 = 18446744073709551615UL
///|
/// The commit index a single majority quorum agrees on, given each voter's
/// acknowledged index (0 when a voter has not reported). This is the largest
/// index stored on a majority: sort the acked indices and take the one a
/// majority is at or above. An empty set imposes no constraint.
fn majority_committed(
voters : Array[String],
acked : Map[String, UInt64],
) -> UInt64 {
let n = voters.length()
if n == 0 {
return MAX_U64
}
let srt = Array::new(capacity=n)
for v in voters {
srt.push(acked.get(v).unwrap_or(0))
}
srt.sort()
// From the end, move n/2+1 to the left; that position is acked by a majority.
let pos = n - (n / 2 + 1)
srt[pos]
}
///|
/// The committed index of a (possibly joint) configuration. `cfg` is the
/// incoming half and `cfgj` the outgoing half; an empty half is the zero
/// quorum. A joint configuration can only commit an index that *both* halves'
/// majorities agree on, so the result is the smaller of the two (Raft §6).
pub fn committed_index(
cfg : Array[String],
cfgj : Array[String],
acked : Map[String, UInt64],
) -> UInt64 {
let a = majority_committed(cfg, acked)
let b = majority_committed(cfgj, acked)
if a < b {
a
} else {
b
}
}
///|
priv struct DescTup {
id : String
idx : UInt64
ok : Bool
mut bar : Int
}
///|
fn repeat_char(c : String, n : Int) -> String {
let mut s = ""
for _i in 0.. String {
let s = v.to_string()
if s.length() >= 5 {
s
} else {
repeat_char(" ", 5 - s.length()) + s
}
}
///|
/// A multi-line ASCII bar chart of each voter's acknowledged index (etcd's
/// `MajorityConfig.Describe`), longest bar for the highest index. Diagnostics
/// only — it has no bearing on consensus, but makes a quorum's commit state
/// legible in a dump.
pub fn describe(voters : Array[String], acked : Map[String, UInt64]) -> String {
let n = voters.length()
if n == 0 {
return ""
}
let info : Array[DescTup] = []
for id in voters {
let (idx, ok) = match acked.get(id) {
Some(i) => (i, true)
None => (0, false)
}
info.push({ id, idx, ok, bar: 0 })
}
// Sort by (idx, id) to assign bar lengths, longest bar = highest index.
insertion_sort(info, fn(a, b) {
if a.idx != b.idx {
a.idx < b.idx
} else {
a.id < b.id
}
})
for i in 1.." + repeat_char(" ", n - t.bar)
}
buf = buf + " " + pad5(t.idx) + " (id=" + t.id + ")\n"
}
buf
}
///|
fn insertion_sort(
a : Array[DescTup],
less : (DescTup, DescTup) -> Bool,
) -> Unit {
let mut i = 1
while i < a.length() {
let key = a[i]
let mut j = i - 1
while j >= 0 && less(key, a[j]) {
a[j + 1] = a[j]
j = j - 1
}
a[j + 1] = key
i = i + 1
}
}
///|
/// The vote outcome for one majority quorum. An empty set has, by convention,
/// already won — which makes a half-populated joint quorum behave like a plain
/// majority quorum.
fn majority_vote(
voters : Array[String],
votes : Map[String, Bool],
) -> VoteState {
let n = voters.length()
if n == 0 {
return VoteWon
}
let mut yes = 0
let mut missing = 0
for id in voters {
match votes.get(id) {
None => missing = missing + 1
Some(true) => yes = yes + 1
Some(false) => ()
}
}
let q = n / 2 + 1
if yes >= q {
VoteWon
} else if yes + missing >= q {
VotePending
} else {
VoteLost
}
}
///|
/// The vote outcome for a (possibly joint) configuration. A joint vote is won
/// only when both halves win, lost as soon as either half loses, and pending
/// otherwise — the discipline that keeps a membership change from splitting the
/// cluster's decision (Raft §6).
pub fn vote_result(
cfg : Array[String],
cfgj : Array[String],
votes : Map[String, Bool],
) -> VoteState {
let r1 = majority_vote(cfg, votes)
let r2 = majority_vote(cfgj, votes)
if r1 == VoteWon && r2 == VoteWon {
VoteWon
} else if r1 == VoteLost || r2 == VoteLost {
VoteLost
} else {
VotePending
}
}