Files
Baradb/formal-verification/recovery.tla
T
dimgigov 422df08ab9 feat: clean build + crossmodal.tla + TLA+ symmetry (v1.0.0-prep)
Build warnings cleanup (0 warnings):
- Suppress threadpool deprecation warning (baradadb.nim)
- Remove unused 'os' import (logging.nim)
- Fix ImplicitDefaultValue for newNode params (ast.nim)
- Add explicit cstring cast for posix.open (wal.nim)
- Fix HoleEnumConv for MsgKind parsing (server.nim)

TLA+ Formal Verification:
- Add symmetry reduction (Permutations) to all 9 existing specs
- Add SYMMETRY directive to all .cfg files
- New crossmodal.tla: cross-modal consistency spec
  * MetadataVectorConsistency, HybridResultValid
  * CommittedAtomicity, AbortedAtomicity, TxnStateValid
- New models/crossmodal.cfg

Tests:
- Add Cross-Modal TLA+ Faithfulness tests (4 tests)

Docs:
- Update PLAN.md and BUG_AUDIT.md with completed tasks
2026-05-13 09:24:04 +03:00

276 lines
9.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-------------------------------- MODULE recovery --------------------------------
(*
TLA+ specification of Crash Recovery via WAL replay (REDO/UNDO) as
implemented in BaraDB (storage/recovery.nim + storage/wal.nim).
Key properties verified:
- RedoCommitted : after recovery, all committed transaction
entries are present in the LSM-Tree.
- UndoUncommitted : after recovery, uncommitted transaction
entries are NOT present in the LSM-Tree.
- RecoveryCompleteness: the recovered LSM-Tree contains exactly the
committed data and nothing else.
- NoPartialCommits : a transaction without a commit record never
contributes data to the recovered state.
- MonotonicLsn : WAL entry LSNs are strictly increasing.
- WalIntegrity : every WAL entry has a valid txnId and key.
*)
EXTENDS Integers, Sequences, FiniteSets, TLC
CONSTANTS Keys, \* set of keys
Values, \* set of possible values
MaxTxnId, \* bound transaction IDs for model checking
MaxWalLen, \* bound WAL length for model checking
MaxSteps, \* bound total actions for model checking
Nil \* distinguished nil value (model value)
ASSUME IsFiniteSet(Keys) /\ IsFiniteSet(Values)
ASSUME MaxTxnId >= 1 /\ MaxWalLen >= 1 /\ MaxSteps >= 1
VARIABLES
wal, \* wal ∈ Seq(<<kind, txnId, key, value>>)
\* kind ∈ {"Put", "Delete", "Commit"}
lsmData, \* lsmData[k] ∈ Values {Nil} — current LSM-Tree state
recovered, \* recovered ∈ BOOLEAN — has recovery run?
recoveredData, \* recoveredData[k] ∈ Values {Nil} — state after recovery
lastTxnId, \* lastTxnId ∈ 0..MaxTxnId — highest committed txn seen
steps \* steps ∈ 0..MaxSteps — action counter bound
vars == <<wal, lsmData, recovered, recoveredData, lastTxnId, steps>>
-----------------------------------------------------------------------------
\* Helper operators
\* Set of committed transaction IDs in the WAL.
CommittedTxns ==
{t \in 1..MaxTxnId :
\E i \in 1..Len(wal) : wal[i] = <<"Commit", t, Nil, Nil>>}
\* Is transaction t committed according to the WAL?
IsCommitted(t) == t \in CommittedTxns
\* All entries belonging to a committed transaction.
CommittedEntries(t) ==
{i \in 1..Len(wal) :
wal[i][2] = t /\ wal[i][1] \in {"Put", "Delete"}}
\* The last committed transaction ID (largest committed txn).
MaxCommittedTxn ==
IF CommittedTxns = {} THEN 0
ELSE CHOOSE t \in CommittedTxns :
\A t2 \in CommittedTxns : t >= t2
\* Simulate recovery: build recoveredData from WAL.
\* REDO: apply all entries of committed transactions.
\* UNDO: skip all entries of uncommitted transactions.
RecoverState ==
[k \in Keys |->
LET committedPuts ==
{i \in 1..Len(wal) :
/\ wal[i][1] = "Put"
/\ wal[i][3] = k
/\ IsCommitted(wal[i][2])}
IN IF committedPuts = {}
THEN Nil
ELSE LET lastIdx == CHOOSE i \in committedPuts :
\A j \in committedPuts : j <= i
IN wal[lastIdx][4]]
-----------------------------------------------------------------------------
\* Initial state
Init ==
/\ wal = << >>
/\ lsmData = [k \in Keys |-> Nil]
/\ recovered = FALSE
/\ recoveredData = [k \in Keys |-> Nil]
/\ lastTxnId = 0
/\ steps = 0
-----------------------------------------------------------------------------
\* State transitions
\* Append a Put entry to the WAL for transaction t.
WalPut(t, k, v) ==
/\ ~recovered
/\ steps < MaxSteps
/\ Len(wal) < MaxWalLen
/\ t \in 1..MaxTxnId
/\ k \in Keys
/\ v \in Values
/\ wal' = Append(wal, <<"Put", t, k, v>>)
/\ steps' = steps + 1
/\ UNCHANGED <<lsmData, recovered, recoveredData, lastTxnId>>
\* Append a Delete entry to the WAL for transaction t.
WalDelete(t, k) ==
/\ ~recovered
/\ steps < MaxSteps
/\ Len(wal) < MaxWalLen
/\ t \in 1..MaxTxnId
/\ k \in Keys
/\ wal' = Append(wal, <<"Delete", t, k, Nil>>)
/\ steps' = steps + 1
/\ UNCHANGED <<lsmData, recovered, recoveredData, lastTxnId>>
\* Append a Commit entry for transaction t.
WalCommit(t) ==
/\ ~recovered
/\ steps < MaxSteps
/\ Len(wal) < MaxWalLen
/\ t \in 1..MaxTxnId
/\ wal' = Append(wal, <<"Commit", t, Nil, Nil>>)
/\ lastTxnId' = IF t > lastTxnId THEN t ELSE lastTxnId
/\ steps' = steps + 1
/\ UNCHANGED <<lsmData, recovered, recoveredData>>
\* Normal operation: apply a committed Put directly to LSM-Tree.
ApplyPut(t, k, v) ==
/\ ~recovered
/\ steps < MaxSteps
/\ t \in 1..MaxTxnId
/\ IsCommitted(t)
/\ k \in Keys
/\ v \in Values
/\ lsmData' = [lsmData EXCEPT ![k] = v]
/\ steps' = steps + 1
/\ UNCHANGED <<wal, recovered, recoveredData, lastTxnId>>
\* Normal operation: apply a committed Delete directly to LSM-Tree.
ApplyDelete(t, k) ==
/\ ~recovered
/\ steps < MaxSteps
/\ t \in 1..MaxTxnId
/\ IsCommitted(t)
/\ k \in Keys
/\ lsmData' = [lsmData EXCEPT ![k] = Nil]
/\ steps' = steps + 1
/\ UNCHANGED <<wal, recovered, recoveredData, lastTxnId>>
\* Crash: the system crashes. LSM-Tree state may be lost; WAL is durable.
Crash ==
/\ ~recovered
/\ steps < MaxSteps
/\ lsmData' = [k \in Keys |-> Nil]
/\ steps' = steps + 1
/\ UNCHANGED <<wal, recovered, recoveredData, lastTxnId>>
\* Recover: replay WAL to reconstruct LSM-Tree state.
Recover ==
/\ ~recovered
/\ steps < MaxSteps
/\ recovered' = TRUE
/\ recoveredData' = RecoverState
/\ lsmData' = recoveredData'
/\ steps' = steps + 1
/\ UNCHANGED <<wal, lastTxnId>>
-----------------------------------------------------------------------------
\* Next-state relation
Next ==
\/ \E t \in 1..MaxTxnId : \E k \in Keys : \E v \in Values : WalPut(t, k, v)
\/ \E t \in 1..MaxTxnId : \E k \in Keys : WalDelete(t, k)
\/ \E t \in 1..MaxTxnId : WalCommit(t)
\/ \E t \in 1..MaxTxnId : \E k \in Keys : \E v \in Values : ApplyPut(t, k, v)
\/ \E t \in 1..MaxTxnId : \E k \in Keys : ApplyDelete(t, k)
\/ Crash
\/ Recover
-----------------------------------------------------------------------------
\* Safety properties
\* After recovery, every committed Put entry is reflected in recoveredData.
RedoCommitted ==
recovered =>
\A t \in 1..MaxTxnId :
\A k \in Keys :
(IsCommitted(t) /\ \E i \in 1..Len(wal) :
wal[i][1] = "Put" /\ wal[i][2] = t /\ wal[i][3] = k) =>
recoveredData[k] /= Nil
\* After recovery, no uncommitted entry determines the final value.
\* If recoveredData[k] /= Nil, the last Put for k must be from a committed txn.
UndoUncommitted ==
recovered =>
\A k \in Keys :
recoveredData[k] /= Nil =>
\E i \in 1..Len(wal) :
/\ wal[i][1] = "Put"
/\ wal[i][3] = k
/\ IsCommitted(wal[i][2])
/\ \A j \in (i+1)..Len(wal) :
~(wal[j][1] = "Put" /\ wal[j][3] = k)
\* The recovered data contains exactly the committed data.
\* For every key, if the last committed Put for that key has value v,
\* then recoveredData[k] = v; otherwise Nil.
RecoveryCompleteness ==
recovered =>
\A k \in Keys :
LET hasCommittedPut ==
\E i \in 1..Len(wal) :
wal[i][1] = "Put" /\ wal[i][3] = k /\ IsCommitted(wal[i][2])
lastCommittedPut ==
IF ~hasCommittedPut
THEN 0
ELSE CHOOSE i \in 1..Len(wal) :
/\ wal[i][1] = "Put"
/\ wal[i][3] = k
/\ IsCommitted(wal[i][2])
/\ \A j \in (i+1)..Len(wal) :
~(wal[j][1] = "Put" /\ wal[j][3] = k /\ IsCommitted(wal[j][2]))
IN IF lastCommittedPut = 0
THEN recoveredData[k] = Nil
ELSE recoveredData[k] = wal[lastCommittedPut][4]
\* A transaction without a commit record never contributes data.
NoPartialCommits ==
recovered =>
\A t \in 1..MaxTxnId :
~IsCommitted(t) =>
\A k \in Keys :
~(\E i \in 1..Len(wal) :
wal[i][1] = "Put" /\ wal[i][2] = t /\ wal[i][3] = k /\
recoveredData[k] /= Nil)
\* WAL LSNs (entry indices) are strictly increasing (enforced by Append).
MonotonicLsn ==
TRUE \* This is inherent in the sequence model; no separate check needed.
\* Every WAL entry has valid kind, txnId, and key.
WalIntegrity ==
\A i \in 1..Len(wal) :
LET entry == wal[i]
kind == entry[1]
txn == entry[2]
k == entry[3]
IN /\ kind \in {"Put", "Delete", "Commit"}
/\ txn \in 1..MaxTxnId
/\ (kind \in {"Put", "Delete"} => k \in Keys)
\* Type invariant
TypeOk ==
/\ wal \in Seq({"Put", "Delete", "Commit"} \X (1..MaxTxnId) \X (Keys \cup {Nil}) \X (Values \cup {Nil}))
/\ lsmData \in [Keys -> Values \cup {Nil}]
/\ recovered \in BOOLEAN
/\ recoveredData \in [Keys -> Values \cup {Nil}]
/\ lastTxnId \in 0..MaxTxnId
/\ steps \in 0..MaxSteps
\* Liveness properties
\* If there are committed entries in the WAL, recovery eventually produces
\* a non-empty recoveredData (or Nil for all keys if all are deletes).
RecoveryProgress ==
(Len(wal) > 0 /\ recovered) ~> (recoveredData = RecoverState)
\* Specification with weak fairness.
Spec == Init /\ [][Next]_vars /\ WF_vars(Next)
\* Symmetry reduction for model checking.
Symmetry == Permutations({k1, k2}) \cup Permutations({v1, v2})
=============================================================================