///|
/// The body of a message exchanged between nodes. A heartbeat is modelled as an
/// `Append` with no entries, so the same replication path carries both. Keeping
/// every request paired with its response in one enum lets a transport move
/// Raft traffic without knowing what any particular message means.
pub(all) enum Payload {
PreVote(RequestVoteArgs)
PreVoteResp(RequestVoteReply)
Vote(RequestVoteArgs)
VoteResp(RequestVoteReply)
Append(AppendEntriesArgs)
AppendResp(AppendEntriesReply)
Heartbeat(HeartbeatArgs)
HeartbeatResp(HeartbeatReply)
// A follower answers a `Snapshot` with an `AppendResp`, not a dedicated reply:
// etcd's `handleSnapshot` (raft.go:1840) responds to `MsgSnap` with a
// `MsgAppResp` carrying `lastIndex()` on install or `committed` when the
// snapshot is refused, so the leader's ordinary append-response path folds it
// back into the follower's progress.
Snapshot(InstallSnapshotArgs)
TimeoutNow(UInt64)
// Routable client-request messages (etcd's local, term-0 messages): a follower
// that receives one forwards it to the leader rather than dropping it, so a
// client may address any server. `Propose` carries the entries to append;
// `ReadIndex` the read's opaque context; `TransferLeader` the target's id;
// `ForgetLeader` nothing. The step function stamps them with no term.
Propose(Array[Entry])
ReadIndex(Bytes)
ReadIndexResp(ReadIndexResp)
TransferLeader(String)
ForgetLeader
}
///|
/// The leader's answer to a forwarded `ReadIndex` (etcd's MsgReadIndexResp): the
/// commit index the read is anchored at, echoed back with the caller's context to
/// the server that originated the read. Unlike the request it carries the
/// leader's term, so it is term-checked like any other reply.
pub(all) struct ReadIndexResp {
term : UInt64
index : UInt64
context : Bytes
} derive(Eq)
///|
/// A routed message: a payload with the ids of the sender and intended
/// recipient. The step function consumes these and produces more of them; a
/// transport (real or simulated) is only responsible for delivery.
pub(all) struct Message {
from : String
to : String
payload : Payload
// A leadership-transfer campaign stamps its vote solicitations `force` so a
// recipient still leased to the outgoing leader grants them, letting the
// handover complete (etcd carries `campaignTransfer` in the message context to
// the same effect, raft.go:1102). Off for every ordinary message; a disruptive
// candidate that merely timed out cannot set it.
force : Bool
}
///|
/// Build a routed message. `force` is set only for leadership-transfer votes.
pub fn Message::new(
from : String,
to : String,
payload : Payload,
force? : Bool = false,
) -> Message {
{ from, to, payload, force }
}
///|
/// The term the message was stamped with. Every payload carries the sender's
/// term; a receiver uses it to decide whether to step down or reject.
pub fn Message::term(self : Message) -> UInt64 {
match self.payload {
PreVote(a) => a.term
PreVoteResp(r) => r.term
Vote(a) => a.term
VoteResp(r) => r.term
Append(a) => a.term
AppendResp(r) => r.term
Heartbeat(a) => a.term
HeartbeatResp(r) => r.term
Snapshot(a) => a.term
TimeoutNow(t) => t
// Local, routable messages carry no term (etcd sends MsgProp/MsgReadIndex
// with Term 0); the read-index reply does carry the leader's term.
ReadIndexResp(r) => r.term
Propose(_) | ReadIndex(_) | TransferLeader(_) | ForgetLeader => 0
}
}
///|
/// Whether this message is a response rather than a request. The simulator uses
/// it only for readable traces.
pub fn Message::is_response(self : Message) -> Bool {
match self.payload {
PreVoteResp(_)
| VoteResp(_)
| AppendResp(_)
| HeartbeatResp(_)
| ReadIndexResp(_) => true
_ => false
}
}
///|
/// A short, stable tag for the payload kind, for logs and scenario traces.
pub fn Message::kind(self : Message) -> String {
match self.payload {
PreVote(_) => "PreVote"
PreVoteResp(_) => "PreVoteResp"
Vote(_) => "Vote"
VoteResp(_) => "VoteResp"
Append(a) => if a.entries.is_empty() { "Probe" } else { "Append" }
AppendResp(_) => "AppendResp"
Heartbeat(_) => "Heartbeat"
HeartbeatResp(_) => "HeartbeatResp"
Snapshot(_) => "Snapshot"
TimeoutNow(_) => "TimeoutNow"
Propose(_) => "Propose"
ReadIndex(_) => "ReadIndex"
ReadIndexResp(_) => "ReadIndexResp"
TransferLeader(_) => "TransferLeader"
ForgetLeader => "ForgetLeader"
}
}
///|
/// etcd classifies a handful of message types as node-local — MsgHup, MsgBeat,
/// MsgSnapStatus, MsgCheckQuorum and the storage-thread messages — which never
/// travel over the transport. This port has no such messages: those triggers are
/// direct method calls (tick / campaign / step), not `Payload`s. Every
/// transportable `Payload` is therefore, by construction, a network message.
pub fn Payload::is_local(self : Payload) -> Bool {
match self {
_ => false
}
}