-
Notifications
You must be signed in to change notification settings - Fork 0
/
MongoStaticRaft.tla
201 lines (169 loc) · 7.6 KB
/
MongoStaticRaft.tla
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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
---- MODULE MongoStaticRaft ----
\*
\* Basic, static version of MongoDB Raft protocol. No reconfiguration is allowed.
\*
EXTENDS Naturals, Integers, FiniteSets, Sequences, TLC, Defs
CONSTANTS Server
CONSTANTS Secondary, Primary, Nil
VARIABLE currentTerm
VARIABLE state
VARIABLE log
VARIABLE config
VARIABLE committed
vars == <<currentTerm, state, log, committed, config>>
\*
\* Helper operators.
\*
\* Is log entry e = <<index, term>> in the log of node 'i'.
InLog(e, i) == \E x \in DOMAIN log[i] : x = e[1] /\ log[i][x] = e[2]
\* The term of the last entry in a log, or 0 if the log is empty.
LastTerm(xlog) == IF Len(xlog) = 0 THEN 0 ELSE xlog[Len(xlog)]
LastEntry(xlog) == <<Len(xlog),xlog[Len(xlog)]>>
GetTerm(xlog, index) == IF index = 0 THEN 0 ELSE xlog[index]
LogTerm(i, index) == GetTerm(log[i], index)
IsPrefix(s, t) ==
(**************************************************************************)
(* TRUE iff the sequence s is a prefix of the sequence t, s.t. *)
(* \E u \in Seq(Range(t)) : t = s \o u. In other words, there exists *)
(* a suffix u that with s prepended equals t. *)
(**************************************************************************)
Len(s) <= Len(t) /\ SubSeq(s, 1, Len(s)) = SubSeq(t, 1, Len(s))
CanRollback(i, j) ==
/\ LastTerm(log[i]) < LastTerm(log[j])
/\ ~IsPrefix(log[i],log[j])
\* Can node 'i' currently cast a vote for node 'j' in term 'term'.
CanVoteForOplog(i, j, term) ==
LET logOk ==
\/ LastTerm(log[j]) > LastTerm(log[i])
\/ /\ LastTerm(log[j]) = LastTerm(log[i])
/\ Len(log[j]) >= Len(log[i]) IN
/\ currentTerm[i] < term
/\ logOk
\* Is a log entry 'e'=<<i, t>> immediately committed in term 't' with a quorum 'Q'.
ImmediatelyCommitted(e, Q) ==
LET eind == e[1]
eterm == e[2] IN
\A s \in Q :
/\ Len(log[s]) >= eind
/\ InLog(e, s) \* they have the entry.
/\ currentTerm[s] = eterm \* they are in the same term as the log entry.
\* Helper operator for actions that propagate the term between two nodes.
UpdateTermsExpr(i, j) ==
/\ currentTerm[i] > currentTerm[j]
/\ currentTerm' = [currentTerm EXCEPT ![j] = currentTerm[i]]
/\ state' = [state EXCEPT ![j] = Secondary]
--------------------------------------------------------------------------------
\*
\* Next state actions.
\*
\* Node 'i', a primary, handles a new client request and places the entry
\* in its log.
ClientRequest(i) ==
/\ state[i] = Primary
/\ log' = [log EXCEPT ![i] = Append(log[i], currentTerm[i])]
/\ UNCHANGED <<currentTerm, state, committed, config>>
\* Node 'i' gets a new log entry from node 'j'.
GetEntries(i, j) ==
/\ state[i] = Secondary
\* Node j must have more entries than node i.
/\ Len(log[j]) > Len(log[i])
\* Ensure that the entry at the last index of node i's log must match the entry at
\* the same index in node j's log. If the log of node i is empty, then the check
\* trivially passes. This is the essential 'log consistency check'.
/\ LET logOk == IF Empty(log[i])
THEN TRUE
ELSE log[j][Len(log[i])] = log[i][Len(log[i])] IN
/\ logOk \* log consistency check
\* If the log of node i is empty, then take the first entry from node j's log.
\* Otherwise take the entry following the last index of node i.
/\ LET newEntryIndex == IF Empty(log[i]) THEN 1 ELSE Len(log[i]) + 1
newEntry == log[j][newEntryIndex]
newLog == Append(log[i], newEntry) IN
/\ log' = [log EXCEPT ![i] = newLog]
/\ UNCHANGED <<committed, currentTerm, state, config>>
\* Node 'i' rolls back against the log of node 'j'.
RollbackEntries(i, j) ==
/\ state[i] = Secondary
/\ CanRollback(i, j)
\* Roll back one log entry.
/\ log' = [log EXCEPT ![i] = SubSeq(log[i], 1, Len(log[i])-1)]
/\ UNCHANGED <<committed, currentTerm, state, config>>
\* Node 'i' gets elected as a primary.
BecomeLeader(i, voteQuorum) ==
\* Primaries make decisions based on their current configuration.
LET newTerm == currentTerm[i] + 1 IN
/\ i \in voteQuorum \* The new leader should vote for itself.
/\ \A v \in voteQuorum : CanVoteForOplog(v, i, newTerm)
\* Update the terms of each voter.
/\ currentTerm' = [s \in Server |-> IF s \in voteQuorum THEN newTerm ELSE currentTerm[s]]
/\ state' = [s \in Server |->
IF s = i THEN Primary
ELSE IF s \in voteQuorum THEN Secondary \* All voters should revert to secondary state.
ELSE state[s]]
/\ UNCHANGED <<log, config, committed>>
\* Primary 'i' commits its latest log entry.
CommitEntry(i, commitQuorum) ==
LET ind == Len(log[i]) IN
\* Must have some entries to commit.
/\ ind > 0
\* This node is leader.
/\ state[i] = Primary
\* The entry was written by this leader.
/\ log[i][ind] = currentTerm[i]
\* all nodes have this log entry and are in the term of the leader.
/\ ImmediatelyCommitted(<<ind,currentTerm[i]>>, commitQuorum)
\* Don't mark an entry as committed more than once.
/\ ~\E c \in committed : c.entry = <<ind, currentTerm[i]>>
/\ committed' = committed \cup
{[ entry |-> <<ind, currentTerm[i]>>,
term |-> currentTerm[i]]}
/\ UNCHANGED <<currentTerm, state, log, config>>
\* Action that exchanges terms between two nodes and step down the primary if
\* needed. This can be safely specified as a separate action, rather than
\* occurring atomically on other replication actions that involve communication
\* between two nodes. This makes it clearer to see where term propagation is
\* strictly necessary for guaranteeing safety.
UpdateTerms(i, j) ==
/\ UpdateTermsExpr(i, j)
/\ UNCHANGED <<log, config, committed>>
Init ==
/\ currentTerm = [i \in Server |-> 0]
/\ state = [i \in Server |-> Secondary]
/\ log = [i \in Server |-> <<>>]
/\ \E initConfig \in SUBSET Server :
/\ initConfig # {} \* configs should be non-empty.
/\ config = [i \in Server |-> initConfig]
/\ committed = {}
Next ==
\/ \E s \in Server : ClientRequest(s)
\/ \E s, t \in Server : GetEntries(s, t)
\/ \E s, t \in Server : RollbackEntries(s, t)
\/ \E s \in Server : \E Q \in Quorums(config[s]) : BecomeLeader(s, Q)
\/ \E s \in Server : \E Q \in Quorums(config[s]) : CommitEntry(s, Q)
\/ \E s,t \in Server : UpdateTerms(s, t)
Spec == Init /\ [][Next]_vars
--------------------------------------------------------------------------------
\*
\* Correctness properties
\*
OnePrimaryPerTerm ==
\A s,t \in Server :
(/\ state[s] = Primary
/\ state[t] = Primary
/\ currentTerm[s] = currentTerm[t]) => (s = t)
LeaderAppendOnly ==
[][\A s \in Server : state[s] = Primary => Len(log'[s]) >= Len(log[s])]_vars
\* <<index, term>> pairs uniquely identify log prefixes.
LogMatching ==
\A s,t \in Server :
\A i \in DOMAIN log[s] :
(\E j \in DOMAIN log[t] : i = j /\ log[s][i] = log[t][j]) =>
(SubSeq(log[s],1,i) = SubSeq(log[t],1,i)) \* prefixes must be the same.
\* When a node gets elected as primary it contains all entries committed in previous terms.
LeaderCompleteness ==
\A s \in Server : (state[s] = Primary) =>
\A c \in committed : (c.term < currentTerm[s] => InLog(c.entry, s))
\* If two entries are committed at the same index, they must be the same entry.
StateMachineSafety ==
\A c1, c2 \in committed : (c1.entry[1] = c2.entry[1]) => (c1 = c2)
=============================================================================