-------------------------------- MODULE replication -------------------------------- (* TLA+ specification of the BaraDB replication manager (core/replication.nim) supporting Async, Sync, and Semi-sync modes. Key properties verified: - MonotonicLsn : applied LSN never moves backwards (temporal). - AcksRemovePending: a replica that acked an LSN is not pending for it. - PendingAreKnown : all pending acks refer to valid replicas. - SyncDurability : in sync mode, appliedLsn only advances when 0 pending. - SemiSyncQuorum : in semi-sync mode, ack count >= min(MaxSyncCount, Connected). - AppliedLteCurrent : appliedLsn never exceeds currentLsn. - AckedIsConnected : only connected replicas can ack. *) EXTENDS Integers, Sequences, FiniteSets, TLC CONSTANTS Replicas, \* set of replica IDs MaxLsn, \* bound LSN values for model checking MaxSyncCount \* bound semi-sync count ASSUME IsFiniteSet(Replicas) VARIABLES mode, \* mode ∈ {"Async", "Sync", "SemiSync"} replicaState, \* replicaState[r] ∈ {"Disconnected", "Connected"} currentLsn, \* currentLsn ∈ Nat pendingAcks, \* pendingAcks[l] ⊆ Replicas (LSNs waiting for acks) appliedLsn, \* appliedLsn ∈ Nat ackedBy \* ackedBy[l] ⊆ Replicas (who acked which LSN) vars == <> ----------------------------------------------------------------------------- \* Helper operators Max(a, b) == IF a > b THEN a ELSE b Min(a, b) == IF a < b THEN a ELSE b ConnectedReplicas == {r \in Replicas : replicaState[r] = "Connected"} ----------------------------------------------------------------------------- \* Initial state Init == /\ mode \in {"Async", "Sync", "SemiSync"} /\ replicaState = [r \in Replicas |-> "Disconnected"] /\ currentLsn = 0 /\ pendingAcks = [l \in 0..MaxLsn |-> {}] /\ appliedLsn = 0 /\ ackedBy = [l \in 0..MaxLsn |-> {}] ----------------------------------------------------------------------------- \* State transitions \* A replica comes online. Connect(r) == /\ replicaState[r] = "Disconnected" /\ replicaState' = [replicaState EXCEPT ![r] = "Connected"] /\ UNCHANGED <> \* A replica goes offline. Disconnect(r) == /\ replicaState[r] = "Connected" /\ replicaState' = [replicaState EXCEPT ![r] = "Disconnected"] /\ UNCHANGED <> \* A new LSN is produced by the primary. WriteLsn == /\ currentLsn < MaxLsn /\ currentLsn' = currentLsn + 1 /\ LET newLsn == currentLsn + 1 conn == ConnectedReplicas IN IF mode = "Sync" /\ conn /= {} THEN pendingAcks' = [pendingAcks EXCEPT ![newLsn] = conn] ELSE IF mode = "SemiSync" /\ conn /= {} THEN pendingAcks' = [pendingAcks EXCEPT ![newLsn] = CHOOSE s \in SUBSET conn : Cardinality(s) = Min(MaxSyncCount, Cardinality(conn))] ELSE pendingAcks' = pendingAcks /\ UNCHANGED <> \* Replica r acknowledges LSN l. AckLsn(r, l) == /\ replicaState[r] = "Connected" /\ l \in 1..currentLsn /\ r \in pendingAcks[l] /\ ackedBy' = [ackedBy EXCEPT ![l] = @ \cup {r}] /\ pendingAcks' = [pendingAcks EXCEPT ![l] = @ \ {r}] /\ IF pendingAcks'[l] = {} THEN appliedLsn' = Max(appliedLsn, l) ELSE appliedLsn' = appliedLsn /\ UNCHANGED <> \* Switch replication mode. SwitchMode(newMode) == /\ newMode \in {"Async", "Sync", "SemiSync"} /\ mode' = newMode /\ UNCHANGED <> ----------------------------------------------------------------------------- \* Next-state relation Next == \/ \E r \in Replicas : Connect(r) \/ \E r \in Replicas : Disconnect(r) \/ WriteLsn \/ \E r \in Replicas : \E l \in 1..MaxLsn : AckLsn(r, l) \/ \E newMode \in {"Async", "Sync", "SemiSync"} : SwitchMode(newMode) ----------------------------------------------------------------------------- \* Safety properties \* The applied LSN is monotonically non-decreasing (temporal). MonotonicLsn == [][appliedLsn' >= appliedLsn]_vars \* A replica that has acked an LSN is no longer pending for it. AcksRemovePending == \A l \in 1..currentLsn : \A r \in Replicas : r \in ackedBy[l] => r \notin pendingAcks[l] \* All pending acks reference valid replica IDs. PendingAreKnown == \A l \in 1..currentLsn : pendingAcks[l] \subseteq Replicas \* In sync mode, any applied LSN has zero pending acks. SyncDurability == \/ mode /= "Sync" \/ appliedLsn = 0 \/ \A l \in 1..appliedLsn : pendingAcks[l] = {} \* In semi-sync mode, each pending LSN has at most MaxSyncCount replicas. SemiSyncQuorum == IF currentLsn > 0 THEN \A l \in 1..currentLsn : Cardinality(pendingAcks[l]) <= MaxSyncCount + 1 ELSE TRUE \* Applied LSN never exceeds current LSN. AppliedLteCurrent == appliedLsn <= currentLsn \* Only connected replicas appear in ackedBy. AckedIsConnected == IF currentLsn > 0 THEN \A l \in 1..currentLsn : \A r \in ackedBy[l] : replicaState[r] = "Connected" ELSE TRUE \* Type invariant TypeOk == /\ mode \in {"Async", "Sync", "SemiSync"} /\ replicaState \in [Replicas -> {"Disconnected", "Connected"}] /\ currentLsn \in 0..MaxLsn /\ pendingAcks \in [0..MaxLsn -> SUBSET Replicas] /\ appliedLsn \in 0..MaxLsn /\ ackedBy \in [0..MaxLsn -> SUBSET Replicas] \* Symmetry reduction for model checking. Symmetry == Permutations({r1, r2, r3}) =============================================================================