1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
///|
/// 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
}
}