-
Notifications
You must be signed in to change notification settings - Fork 260
Add the C++-aligned Lean disaster recovery model #8277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
c087cf4
Add Lean disaster recovery transition model
achamayou 3fa4736
Prove local recovery safety and liveness
achamayou 6f5f0ff
Add global recovery semantics and invariants
achamayou 5330b7d
Prove quorum and committed-prefix safety
achamayou e973110
Prove fair global recovery progress
achamayou 728ed1d
Add canonical Lean checks and CI
achamayou ae6f36c
Format Lean disaster recovery documentation
achamayou 6151e0c
Normalize Lean source line endings
achamayou 9a81d98
Separate Lean review contracts from proof implementations
achamayou 9b30697
Merge main into Lean disaster recovery PR
achamayou f6bc9a8
Consolidate Lean verification workflow
achamayou 7d32f3e
Share Lean build ignore rule
achamayou fa9f0e8
Upgrade Lean disaster recovery to 4.33.1
achamayou 568b8d1
Merge remote-tracking branch 'origin/main' into achamayou-update-pr-f…
achamayou 5ba2c0a
Merge commit '2e45423e738f4db3364901228dc65f573df4572e' into achamayo…
achamayou File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| name: "Lean" | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - "lean/**" | ||
| - ".github/workflows/lean.yml" | ||
|
|
||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: read-all | ||
|
|
||
| jobs: | ||
| disaster-recovery: | ||
| name: Disaster recovery model and proofs | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
|
|
||
| steps: | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
|
|
||
| - name: Install Lean | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| sudo apt-get update | ||
| sudo apt-get install -y elan | ||
| elan toolchain install "$(cat lean/disaster-recovery/lean-toolchain)" | ||
|
|
||
| - name: Restore Mathlib cache | ||
| working-directory: lean/disaster-recovery | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| lake exe cache get | ||
|
|
||
| - name: Build and check canonical model | ||
| working-directory: lean/disaster-recovery | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| lake exe mk_all --check --lib DisasterRecovery | ||
| lake build --wfail | ||
| lake lint | ||
| lake exe canonical-checks |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| .lake/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,145 @@ | ||
| import DisasterRecovery.Protocol.Temporal | ||
|
|
||
| open DisasterRecovery.Protocol | ||
|
|
||
| private def expect (condition : Bool) (message : String) : IO Unit := | ||
| unless condition do throw (IO.userError message) | ||
|
|
||
| private def eventsFor (config : Config) : List Event := | ||
| let messages := config.expectedLocations.flatMap fun source => | ||
| [ | ||
| .receiveGossip source { view := 0, seqno := source.length } .accepted, | ||
| .receiveGossip source { view := 0, seqno := source.length } .rejected, | ||
| .receiveVote source .accepted, | ||
| .receiveVote source .rejected, | ||
| .receiveIAmOpen source .accepted, | ||
| .receiveIAmOpen source .rejected | ||
| ] | ||
| messages ++ [.timeout, .retry] | ||
|
|
||
| private def invariant (state : NodeState) : Bool := | ||
| let chosenReady := | ||
| if state.phase == .voting then state.chosen.isSome else true | ||
| let openingKind := | ||
| if state.phase == .opening || state.phase == .open then | ||
| state.openKind.isSome | ||
| else | ||
| true | ||
| let restartOnlyJoining := | ||
| if state.restartRequested then state.phase == .joining else true | ||
| chosenReady && openingKind && restartOnlyJoining | ||
|
|
||
| private def enumerate (config : Config) (location : Location) : IO (Prod Nat Nat) := do | ||
| let initial := initialNode location | ||
| let mut states := #[initial] | ||
| let mut seen : Std.HashMap String Nat := {} | ||
| seen := seen.insert (stateKey initial) 0 | ||
| let mut cursor := 0 | ||
| let mut edges := 0 | ||
| while cursor < states.size do | ||
| let state := states[cursor]! | ||
| expect (invariant state) s!"canonical invariant failed: {stateKey state}" | ||
| for event in eventsFor config do | ||
| let next := (step config state event).state | ||
| edges := edges + 1 | ||
| let key := stateKey next | ||
| if !seen.contains key then | ||
| seen := seen.insert key states.size | ||
| states := states.push next | ||
| cursor := cursor + 1 | ||
| pure (states.size, edges) | ||
|
|
||
| def main : IO UInt32 := do | ||
| let config : Config := { | ||
| instanceId := "canonical-tests" | ||
| expectedLocations := ["A", "B"] | ||
| } | ||
| expect config.isValid "canonical test configuration is invalid" | ||
| expect | ||
| (!({ instanceId := "invalid", expectedLocations := ["A", "A"] } : | ||
| Config).isValid) | ||
| "duplicate expected locations were accepted" | ||
| expect (voteQuorum config == 2) "two-node strict majority must be two" | ||
|
|
||
| let initial := initialNode "A" | ||
| expect initial.gossips.isEmpty "canonical C++ state must start without gossip" | ||
|
|
||
| let first := step config initial | ||
| (.receiveGossip "A" { view := 1, seqno := 10 } .accepted) | ||
| expect (first.state.phase == .gossiping) "one of two gossips advanced early" | ||
| let duplicate := step config first.state | ||
| (.receiveGossip "A" { view := 99, seqno := 99 } .accepted) | ||
| expect (duplicate.state == first.state) | ||
| "duplicate gossip source changed its recorded TxID" | ||
| let second := step config first.state | ||
| (.receiveGossip "B" { view := 2, seqno := 1 } .accepted) | ||
| expect (second.state.phase == .voting) "all expected gossips did not advance" | ||
| expect (second.state.chosen == some "B") "full TxID maximum was not chosen" | ||
|
|
||
| let tiedA := step config initial | ||
| (.receiveGossip "A" { view := 2, seqno := 1 } .accepted) | ||
| let tiedB := step config tiedA.state | ||
| (.receiveGossip "B" { view := 2, seqno := 1 } .accepted) | ||
| expect (tiedB.state.chosen == some "B") | ||
| "location name did not break an equal TxID tie lexicographically" | ||
|
|
||
| let frozen := step config second.state | ||
| (.receiveGossip "C" { view := 9, seqno := 9 } .accepted) | ||
| expect (!frozen.accepted && frozen.state == second.state) | ||
| "gossip did not freeze after choosing a node" | ||
|
|
||
| let oneVote := step config second.state (.receiveVote "A" .accepted) | ||
| expect (oneVote.state.phase == .voting) "even-node quorum used legacy threshold" | ||
| let twoVotes := step config oneVote.state (.receiveVote "B" .accepted) | ||
| expect (twoVotes.state.phase == .opening) "strict voting quorum did not open" | ||
| expect (twoVotes.state.openKind == some .quorum) "quorum path mislabeled" | ||
|
|
||
| let emptyVoting := { | ||
| initial with | ||
| phase := .voting | ||
| timeoutState := .voting | ||
| chosen := some "A" | ||
| } | ||
| let noVotes := step config emptyVoting .timeout | ||
| expect (noVotes.state == emptyVoting) | ||
| "aligned voting timeout with zero votes advanced" | ||
|
|
||
| let oneVoteWaiting := { emptyVoting with votes := ["A"] } | ||
| let failover := step config oneVoteWaiting .timeout | ||
| expect (failover.state.phase == .opening) "failover vote did not open" | ||
| expect (failover.state.openKind == some .failover) "failover path mislabeled" | ||
|
|
||
| let opening := { | ||
| twoVotes.state with | ||
| timeoutState := .opening | ||
| } | ||
| let complete := step config opening .timeout | ||
| expect (complete.state.phase == .open) "Opening timeout did not reach Open" | ||
|
|
||
| let joining := step config initial | ||
| (.receiveIAmOpen "B" .accepted) | ||
| expect (joining.state.phase == .joining && joining.state.restartRequested) | ||
| "IAmOpen did not request joining restart" | ||
|
|
||
| let retry := step config second.state .retry | ||
| expect | ||
| (retry.effects == | ||
| [.sendVote "B", .sendGossip "A", .sendGossip "B"]) | ||
| "Voting retry did not send vote before continuing gossip" | ||
|
|
||
| let unexpectedConfig : Config := { | ||
| instanceId := "unexpected" | ||
| expectedLocations := ["A"] | ||
| } | ||
| let unexpected := step unexpectedConfig (initialNode "A") | ||
| (.receiveGossip "OUTSIDE" { view := 1, seqno := 1 } .accepted) | ||
| expect (unexpected.state.phase == .voting) | ||
| "model no longer exposes C++ acceptance of unexpected validated locations" | ||
|
|
||
| let (oneStates, oneEdges) <- enumerate | ||
| { instanceId := "n1", expectedLocations := ["A"] } "A" | ||
| let (twoStates, twoEdges) <- enumerate config "A" | ||
| IO.println s!"canonical n=1: {oneStates} states, {oneEdges} event edges" | ||
| IO.println s!"canonical n=2: {twoStates} states, {twoEdges} event edges" | ||
| IO.println "all canonical semantic checks passed" | ||
| pure 0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import DisasterRecovery.Proofs.Committed | ||
| import DisasterRecovery.Proofs.GlobalTemporal | ||
| import DisasterRecovery.Proofs.Invariants | ||
| import DisasterRecovery.Proofs.Quorum | ||
| import DisasterRecovery.Proofs.Temporal | ||
| import DisasterRecovery.Properties | ||
| import DisasterRecovery.Protocol.Committed | ||
| import DisasterRecovery.Protocol.Global | ||
| import DisasterRecovery.Protocol.GlobalTemporal | ||
| import DisasterRecovery.Protocol.Invariants | ||
| import DisasterRecovery.Protocol.Model | ||
| import DisasterRecovery.Protocol.Quorum | ||
| import DisasterRecovery.Protocol.Temporal |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.