- Deleted compiler/src/generators/wasm.ts (472 lines) - Deleted compiler/src/runtime/wasm-host.ts (165 lines) - Deleted compiler/__tests__/e2e-wasm.test.ts (203 lines) - Removed WASM import and generation call from compiler/src/index.ts - Removed WASMGenerated from TS generator state enum literals - Removed WASM codegen tests from parser.test.ts - All 37 tests passing The WASM generator was architecturally wrong: every Map/Set operation crossed the WASM→JS boundary, creating 45-55% overhead for a module that doesn't compute anything. WASM will be a byproduct of the Rust backend via wasm-pack instead (M2.1). Closes #19
272 lines
No EOL
9.4 KiB
Text
272 lines
No EOL
9.4 KiB
Text
speck Raft {
|
|
// Raft consensus protocol ported to SpeckDL v0.3
|
|
// Demonstrates leader election, log replication, commitment, and safety.
|
|
// Based on Ongaro & Ousterhout's "In Search of an Understandable Consensus Algorithm" (2014).
|
|
// Natural bridge from Paxos (academic) → TigerBeetle (production replicated state machine).
|
|
|
|
type LogIndex = Nat
|
|
type Term = Nat
|
|
type ServerId = String
|
|
|
|
type LogEntry = {
|
|
term: Term,
|
|
index: LogIndex,
|
|
command: String
|
|
}
|
|
|
|
type ServerState = {
|
|
role: String, // "follower", "candidate", "leader"
|
|
currentTerm: Term,
|
|
votedFor: String | null,
|
|
log: List(LogEntry),
|
|
commitIndex: LogIndex,
|
|
lastApplied: LogIndex
|
|
}
|
|
|
|
type AppendEntriesReq = {
|
|
term: Term,
|
|
leaderId: ServerId,
|
|
prevLogIndex: LogIndex,
|
|
prevLogTerm: Term,
|
|
entries: List(LogEntry),
|
|
leaderCommit: LogIndex
|
|
}
|
|
|
|
type RequestVoteReq = {
|
|
term: Term,
|
|
candidateId: ServerId,
|
|
lastLogIndex: LogIndex,
|
|
lastLogTerm: Term
|
|
}
|
|
|
|
state {
|
|
servers: Set(ServerId)
|
|
serverState: Map(ServerId, ServerState)
|
|
|
|
// Leader-specific: for each server, track nextIndex and matchIndex
|
|
nextIndex: Map(Map(ServerId, ServerId), LogIndex)
|
|
matchIndex: Map(Map(ServerId, ServerId), LogIndex)
|
|
|
|
// Election timer (abstracted as random timeout)
|
|
electionTimeout: Map(ServerId, Nat)
|
|
}
|
|
|
|
init {
|
|
servers == {"s1", "s2", "s3", "s4", "s5"}
|
|
|
|
// All servers start as followers in term 0
|
|
forall s in servers:
|
|
serverState[s] == {
|
|
role: "follower",
|
|
currentTerm: 0,
|
|
votedFor: null,
|
|
log: List.empty,
|
|
commitIndex: 0,
|
|
lastApplied: 0
|
|
}
|
|
|
|
nextIndex == emptyMap
|
|
matchIndex == emptyMap
|
|
electionTimeout == emptyMap
|
|
}
|
|
|
|
// Safety: Election Safety — at most one leader per term
|
|
invariant SingleLeaderPerTerm {
|
|
forall s1 in servers:
|
|
forall s2 in servers:
|
|
serverState[s1].role == "leader" and
|
|
serverState[s2].role == "leader" and
|
|
s1 != s2 implies
|
|
serverState[s1].currentTerm != serverState[s2].currentTerm
|
|
}
|
|
|
|
// Safety: Leader Completeness — committed entries survive elections
|
|
invariant LeaderCompleteness {
|
|
forall s in servers:
|
|
serverState[s].role == "leader" implies
|
|
forall entry in serverState[s].log:
|
|
entry.index <= serverState[s].commitIndex implies
|
|
entry.term <= serverState[s].currentTerm
|
|
}
|
|
|
|
// Safety: Log Matching — if two logs have same term at same index, entries match
|
|
invariant LogMatching {
|
|
forall s1 in servers:
|
|
forall s2 in servers:
|
|
forall i in [1 .. length(serverState[s1].log)]:
|
|
i <= length(serverState[s2].log) and
|
|
serverState[s1].log[i].term == serverState[s2].log[i].term implies
|
|
serverState[s1].log[i].command == serverState[s2].log[i].command
|
|
}
|
|
|
|
// Start election: follower times out, becomes candidate
|
|
action StartElection(server: ServerId) {
|
|
require server in servers
|
|
require serverState[server].role == "follower"
|
|
|
|
let newTerm := serverState[server].currentTerm + 1
|
|
|
|
// Increment term, vote for self
|
|
serverState[server].currentTerm := newTerm
|
|
serverState[server].role := "candidate"
|
|
serverState[server].votedFor := server
|
|
|
|
emit ElectionStarted { server: server, term: newTerm }
|
|
}
|
|
|
|
// Grant vote: follower votes for candidate if candidate's log is at least as up-to-date
|
|
action GrantVote(voter: ServerId, req: RequestVoteReq) {
|
|
require voter in servers
|
|
require serverState[voter].role == "follower" or
|
|
serverState[voter].role == "candidate"
|
|
|
|
// Only vote if candidate's term >= our term
|
|
require req.term >= serverState[voter].currentTerm
|
|
|
|
// If candidate's term > ours, step down
|
|
if req.term > serverState[voter].currentTerm {
|
|
serverState[voter].currentTerm := req.term
|
|
serverState[voter].role := "follower"
|
|
serverState[voter].votedFor := null
|
|
}
|
|
|
|
// Check not already voted in this term
|
|
require serverState[voter].votedFor == null or
|
|
serverState[voter].votedFor == req.candidateId
|
|
|
|
// Candidate log must be at least as complete as ours
|
|
let voterLog := serverState[voter].log
|
|
let voterLastIndex := length(voterLog)
|
|
let voterLastTerm := if voterLastIndex > 0: voterLog[voterLastIndex].term else 0
|
|
|
|
require req.lastLogTerm > voterLastTerm or
|
|
(req.lastLogTerm == voterLastTerm and req.lastLogIndex >= voterLastIndex)
|
|
|
|
serverState[voter].votedFor := req.candidateId
|
|
emit VoteGranted { voter: voter, candidate: req.candidateId, term: req.term }
|
|
}
|
|
|
|
// Become leader: candidate receives majority of votes
|
|
action BecomeLeader(server: ServerId) {
|
|
require server in servers
|
|
require serverState[server].role == "candidate"
|
|
|
|
// Count votes received (simplified: assumes quorum reached via external counting)
|
|
serverState[server].role := "leader"
|
|
|
|
// Initialize nextIndex/matchIndex for all other servers
|
|
forall s in servers \ {server}:
|
|
let lastLogIndex := length(serverState[server].log)
|
|
nextIndex[{ leader: server, follower: s }] := lastLogIndex + 1
|
|
matchIndex[{ leader: server, follower: s }] := 0
|
|
|
|
emit LeaderElected { leader: server, term: serverState[server].currentTerm }
|
|
|
|
// Send initial empty AppendEntries (heartbeat) to assert leadership
|
|
emit HeartbeatSent { leader: server, term: serverState[server].currentTerm }
|
|
}
|
|
|
|
// Append entries: leader replicates log entry to followers
|
|
action AppendEntries(leader: ServerId, follower: ServerId, req: AppendEntriesReq) {
|
|
require leader in servers
|
|
require follower in servers
|
|
require leader != follower
|
|
require serverState[leader].role == "leader"
|
|
|
|
// Verify term consistency
|
|
let followerTerm := serverState[follower].currentTerm
|
|
require req.term >= followerTerm
|
|
|
|
// If leader's term is stale, step down
|
|
if req.term > serverState[leader].currentTerm {
|
|
serverState[leader].currentTerm := req.term
|
|
serverState[leader].role := "follower"
|
|
serverState[leader].votedFor := null
|
|
return
|
|
}
|
|
|
|
// Update follower's term if behind
|
|
if req.term > followerTerm {
|
|
serverState[follower].currentTerm := req.term
|
|
serverState[follower].role := "follower"
|
|
serverState[follower].votedFor := null
|
|
}
|
|
|
|
// Check log consistency at prevLogIndex
|
|
let fLog := serverState[follower].log
|
|
require req.prevLogIndex == 0 or
|
|
(req.prevLogIndex <= length(fLog) and
|
|
fLog[req.prevLogIndex].term == req.prevLogTerm)
|
|
|
|
// Delete conflicting entries, append new ones
|
|
let truncated := if req.prevLogIndex < length(fLog):
|
|
take(fLog, req.prevLogIndex)
|
|
else fLog
|
|
|
|
serverState[follower].log := concat(truncated, req.entries)
|
|
|
|
// Update commit index
|
|
if req.leaderCommit > serverState[follower].commitIndex {
|
|
let lastNewEntry := req.prevLogIndex + length(req.entries)
|
|
serverState[follower].commitIndex :=
|
|
if req.leaderCommit < lastNewEntry: req.leaderCommit else lastNewEntry
|
|
}
|
|
|
|
emit EntriesReplicated {
|
|
leader: leader,
|
|
follower: follower,
|
|
entries: length(req.entries)
|
|
}
|
|
}
|
|
|
|
// Advance commit index: leader commits when majority replicated
|
|
action AdvanceCommit(leader: ServerId) {
|
|
require leader in servers
|
|
require serverState[leader].role == "leader"
|
|
|
|
let currentCommit := serverState[leader].commitIndex
|
|
let logLen := length(serverState[leader].log)
|
|
|
|
// Find highest index that has been replicated to a majority
|
|
for i from currentCommit + 1 to logLen:
|
|
let replicated := 1 // leader counts itself
|
|
forall s in servers \ {leader}:
|
|
if matchIndex[{ leader: leader, follower: s }] >= i:
|
|
replicated := replicated + 1
|
|
|
|
if replicated * 2 > size(servers) and
|
|
serverState[leader].log[i].term == serverState[leader].currentTerm {
|
|
serverState[leader].commitIndex := i
|
|
}
|
|
|
|
emit CommitAdvanced {
|
|
leader: leader,
|
|
commitIndex: serverState[leader].commitIndex
|
|
}
|
|
}
|
|
|
|
// Apply committed entries to state machine
|
|
action ApplyEntries(server: ServerId) {
|
|
require server in servers
|
|
|
|
let s := serverState[server]
|
|
let commitIdx := s.commitIndex
|
|
|
|
for i from s.lastApplied + 1 to commitIdx:
|
|
emit CommandApplied {
|
|
server: server,
|
|
index: i,
|
|
command: s.log[i].command
|
|
}
|
|
|
|
serverState[server].lastApplied := commitIdx
|
|
}
|
|
|
|
next:
|
|
StartElection
|
|
| GrantVote
|
|
| BecomeLeader
|
|
| AppendEntries
|
|
| AdvanceCommit
|
|
| ApplyEntries
|
|
} |