|
| 1 | +---------------------------- MODULE wal_replication --------------------------- |
| 2 | +\* A formal specification of write-ahead log (WAL) replication algorithm. |
| 3 | +\* |
| 4 | +\* The algorithm assumes the presence of a write-ahead log (WAL), like the one |
| 5 | +\* used in SQLite, where transactions append modified pages to a WAL. Each |
| 6 | +\* modified page within the WAL is referred to as a frame and is assigned a |
| 7 | +\* monotonically increasing frame index. |
| 8 | +\* |
| 9 | +\* A write is not guaranateed durability until it is backed up. Therefore, |
| 10 | +\* when recovering, primary and replicas revert back to the latest durable |
| 11 | +\* index. |
| 12 | + |
| 13 | +EXTENDS Naturals, FiniteSetsExt, Sequences, SequencesExt, Bags, Functions, TLC |
| 14 | + |
| 15 | +VARIABLE |
| 16 | + txID, |
| 17 | + commitIndex, |
| 18 | + commitServer, |
| 19 | + messages, |
| 20 | + logs, |
| 21 | + durableLog |
| 22 | + |
| 23 | +---------------------------------------------------------------------------------- |
| 24 | +\* Cluster topology. |
| 25 | + |
| 26 | +CONSTANT |
| 27 | + Servers |
| 28 | + |
| 29 | +CONSTANTS |
| 30 | + Node1, |
| 31 | + Node2, |
| 32 | + Node3 |
| 33 | + |
| 34 | +IsPrimary(d) == |
| 35 | + d = Node1 |
| 36 | + |
| 37 | +Primary == Node1 |
| 38 | + |
| 39 | +---------------------------------------------------------------------------------- |
| 40 | +\* Message passing. We assume an ordered networking with no duplicates. |
| 41 | + |
| 42 | +InitMessageVar == |
| 43 | + messages = [ s \in Servers |-> <<>>] |
| 44 | + |
| 45 | +WithMessage(m, msgs) == |
| 46 | + IF \E i \in 1..Len(msgs[m.dest]) : msgs[m.dest][i] = m THEN |
| 47 | + msgs |
| 48 | + ELSE |
| 49 | + [ msgs EXCEPT ![m.dest] = Append(@, m) ] |
| 50 | + |
| 51 | +WithoutMessage(m, msgs) == |
| 52 | + IF \E i \in 1..Len(msgs[m.dest]) : msgs[m.dest][i] = m THEN |
| 53 | + [ msgs EXCEPT ![m.dest] = RemoveAt(@, SelectInSeq(@, LAMBDA e: e = m)) ] |
| 54 | + ELSE |
| 55 | + msgs |
| 56 | + |
| 57 | +Messages == |
| 58 | + UNION { Range(messages[s]) : s \in Servers } |
| 59 | + |
| 60 | +MessagesTo(dest, source) == |
| 61 | + IF \E i \in 1..Len(messages[dest]) : messages[dest][i].source = source THEN |
| 62 | + {messages[dest][SelectInSeq(messages[dest], LAMBDA e: e.source = source)]} |
| 63 | + ELSE |
| 64 | + {} |
| 65 | + |
| 66 | +Send(m) == |
| 67 | + /\ messages' = WithMessage(m, messages) |
| 68 | + |
| 69 | +Discard(m) == |
| 70 | + messages' = WithoutMessage(m, messages) |
| 71 | + |
| 72 | +---------------------------------------------------------------------------------- |
| 73 | +\* Protocol |
| 74 | + |
| 75 | +\* Message types: |
| 76 | +CONSTANTS |
| 77 | + GetFramesMsg, |
| 78 | + ExecuteMsg |
| 79 | + |
| 80 | +RecoverLog(s) == |
| 81 | + /\ logs' = [logs EXCEPT ![s] = durableLog] |
| 82 | + |
| 83 | +Recover == |
| 84 | + /\ \A s \in Servers: RecoverLog(s) |
| 85 | + /\ commitIndex = IF Len(durableLog) > 0 THEN Max(ToSet(durableLog)) ELSE 0 |
| 86 | + /\ UNCHANGED(<<txID, commitIndex, commitServer, messages, durableLog>>) |
| 87 | + |
| 88 | +SyncLog(s) == |
| 89 | + /\ logs' = [logs EXCEPT ![s] = logs[Primary]] |
| 90 | + |
| 91 | +SyncDurable(l) == |
| 92 | + /\ durableLog' = durableLog \o l |
| 93 | + |
| 94 | +AppendToLog(s, i) == |
| 95 | + /\ logs' = [logs EXCEPT ![s] = Append(logs[s], i)] |
| 96 | + |
| 97 | +HandleExecuteMsg(m) == |
| 98 | + /\ IF IsPrimary(m.dest) THEN |
| 99 | + \* Append the write to the local WAL. |
| 100 | + /\ AppendToLog(m.dest, commitIndex + 1) |
| 101 | + ELSE |
| 102 | + \* Append the write to the WAL on the primary... |
| 103 | + /\ AppendToLog(Primary, commitIndex + 1) |
| 104 | + \* ...but also sync local WAL for read your writes. |
| 105 | + /\ SyncLog(m.dest) |
| 106 | + /\ SyncDurable(logs'[Primary]) |
| 107 | + /\ commitIndex' = commitIndex + 1 |
| 108 | + /\ commitServer' = m.dest |
| 109 | + /\ Discard(m) |
| 110 | + /\ UNCHANGED(<<txID>>) |
| 111 | + |
| 112 | +RcvExecuteMsg(i, j) == |
| 113 | + \E m \in MessagesTo(i, j) : |
| 114 | + /\ m.type = ExecuteMsg |
| 115 | + /\ HandleExecuteMsg(m) |
| 116 | + |
| 117 | +HandleGetFramesMsg(m) == |
| 118 | + /\ IsPrimary(m.dest) |
| 119 | + /\ SyncLog(m.source) |
| 120 | + /\ Discard(m) |
| 121 | + /\ UNCHANGED(<<txID, commitServer, commitIndex, durableLog>>) |
| 122 | + |
| 123 | +RcvGetFramesMsg(i, j) == |
| 124 | + \E m \in MessagesTo(i, j) : |
| 125 | + /\ m.type = GetFramesMsg |
| 126 | + /\ HandleGetFramesMsg(m) |
| 127 | + |
| 128 | +Receive(i, j) == |
| 129 | + \/ RcvGetFramesMsg(i, j) |
| 130 | + \/ RcvExecuteMsg(i, j) |
| 131 | + |
| 132 | +SendGetFrames(s) == |
| 133 | + LET |
| 134 | + msg == [ |
| 135 | + type |-> GetFramesMsg, |
| 136 | + dest |-> Primary, |
| 137 | + source |-> s |
| 138 | + ] |
| 139 | + IN |
| 140 | + /\ Send(msg) |
| 141 | + /\ UNCHANGED(<<txID, commitIndex, commitServer, logs, durableLog>>) |
| 142 | + |
| 143 | +SendExecute(i, j) == |
| 144 | + LET |
| 145 | + msg == [ |
| 146 | + type |-> ExecuteMsg, |
| 147 | + txId |-> txID, |
| 148 | + dest |-> i, |
| 149 | + source |-> j |
| 150 | + ] |
| 151 | + IN |
| 152 | + /\ txID' = txID + 1 |
| 153 | + /\ Send(msg) |
| 154 | + /\ UNCHANGED(<<commitIndex, commitServer, logs, durableLog>>) |
| 155 | + |
| 156 | +Next == |
| 157 | + \/ Recover |
| 158 | + \/ \E i, j \in Servers: SendExecute(i, j) |
| 159 | + \/ \E s \in Servers: SendGetFrames(s) |
| 160 | + \/ \E i, j \in Servers: Receive(i, j) |
| 161 | + |
| 162 | +Init == |
| 163 | + /\ txID = 0 |
| 164 | + /\ commitIndex = 0 |
| 165 | + /\ commitServer = Primary |
| 166 | + /\ InitMessageVar |
| 167 | + /\ logs = [s \in Servers |-> <<>>] |
| 168 | + /\ durableLog = <<>> |
| 169 | + |
| 170 | +---------------------------------------------------------------------------------- |
| 171 | +\* Invariants |
| 172 | + |
| 173 | +ReadYourWritesInv == |
| 174 | + commitIndex = 0 \/ Contains(logs[commitServer], commitIndex) |
| 175 | + |
| 176 | +LogsAreContinuousInv == |
| 177 | + \A s \in Servers: Len(logs[s]) = 0 \/ \A i \in 1..Max(ToSet(logs[s])) : Contains(logs[s], i) |
| 178 | + |
| 179 | +NoServerIsAheadOfPrimaryInv == |
| 180 | + \A s \in Servers: Len(logs[s]) <= Len(logs[Primary]) |
| 181 | + |
| 182 | +NoDurableFramesLostInv == |
| 183 | + \A i \in 1..commitIndex : i \in ToSet(durableLog) |
| 184 | + |
| 185 | +---------------------------------------------------------------------------------- |
| 186 | +\* Temporal properties |
| 187 | + |
| 188 | +WriteLivenessProp == <>(commitIndex > 0) |
| 189 | + |
| 190 | +ReplicationProp == |
| 191 | + [] (commitIndex > 0 => \A s \in Servers : <> (Len(logs[s]) > 0)) |
| 192 | + |
| 193 | +==== |
0 commit comments