Implement appendentries fully

This commit is contained in:
Smaug123
2022-11-01 21:23:14 +00:00
parent 3ece92e753
commit 5bd6f23a11
6 changed files with 245 additions and 45 deletions

View File

@@ -19,7 +19,10 @@ type Cluster<'a> =
this.Servers.[i / 1<ServerId>].TriggerHeartbeatTimeout ()
this.Servers.[i / 1<ServerId>].Sync ()
member this.State (i : int<ServerId>) : ServerStatus = this.Servers.[i / 1<ServerId>].State
member this.Status (i : int<ServerId>) : ServerStatus = this.Servers.[i / 1<ServerId>].State
member this.GetCurrentInternalState (i : int<ServerId>) : ServerInternalState<'a> Async =
this.Servers.[i / 1<ServerId>].GetCurrentInternalState ()
member this.ClusterSize : int = this.Servers.Length

View File

@@ -20,18 +20,34 @@ type VolatileState =
type LeaderState =
{
/// For each server, index of the next log entry to send to that server
NextIndex : int<LogIndex> array
/// For each server, index of the log entry to send to them next. Note that this might not
/// actually be the *first* index we need to send - the recipient may reject this message.
/// When they reject this message, we'll decrement ToSend and try again with an earlier
/// message, until eventually we go far back enough in time that our log intersects with
/// that of the recipient, and they'll accept it.
ToSend : int<LogIndex> array
/// For each server, index of the highest log entry known to be replicated on that server
MatchIndex : int<LogIndex> array
}
static member New (clusterSize : int) (currentIndex : int<LogIndex>) : LeaderState =
{
NextIndex = Array.create clusterSize (currentIndex + 1<LogIndex>)
// +1, because these are indexed from 1.
ToSend = Array.create clusterSize (currentIndex + 1<LogIndex>)
MatchIndex = Array.zeroCreate clusterSize
}
member this.Clone () =
let cloneArray (arr : 'b array) : 'b array =
let result = Array.zeroCreate<'b> arr.Length
System.Array.Copy (arr, result, arr.Length)
result
{
ToSend = cloneArray this.ToSend
MatchIndex = cloneArray this.MatchIndex
}
/// You asked me to vote for you to become leader. Here is my response.
type RequestVoteReply =
{
@@ -99,6 +115,8 @@ type AppendEntriesMessage<'a> =
/// with what happened during terms that took place while it was down.
NewEntry : ('a * int<Term>) option
LeaderCommitIndex : int<LogIndex>
/// TODO - we don't need this, the responder should just construct
/// the appropriate Message and send it themselves
ReplyChannel : AppendEntriesReply -> unit
}
@@ -115,6 +133,17 @@ type AppendEntriesMessage<'a> =
this.LeaderTerm
this.LeaderCommitIndex
/// A readout of the server's internal state, suitable for e.g. debugging tools.
type ServerInternalState<'a> =
{
LogIndex : int<LogIndex>
CurrentTerm : int<Term>
CurrentVote : int<ServerId> option
Log : ('a * int<Term>) option list
/// A clone of the leader state, if this is a leader.
LeaderState : LeaderState option
}
type ClientReply =
/// You asked a node that isn't the leader. Here's a hint about whom you should ask instead.
/// The hint may not be accurate even as of the time when we reply, and certainly it may not be
@@ -208,6 +237,7 @@ type private ServerAction<'a> =
| EmitHeartbeat
| Receive of Message<'a>
| Sync of AsyncReplyChannel<unit>
| StateReadout of AsyncReplyChannel<ServerInternalState<'a>>
[<RequireQualifiedAccess>]
module internal ServerUtils =
@@ -468,30 +498,39 @@ type Server<'a>
assert (logIsConsistent message)
acceptRequest ()
let sendAppendEntries (leaderState : LeaderState) (j : int<ServerId>) =
let toSend = leaderState.ToSend.[j / 1<ServerId>]
let prevLogTerm = persistentState.GetLogEntry (toSend - 1<LogIndex>)
{
LeaderTerm = persistentState.CurrentTerm
LeaderId = me
PrevLogEntry =
match prevLogTerm with
| None -> None
| Some (_, term) ->
{
Term = term
Index = toSend - 1<LogIndex>
}
|> Some
NewEntry = persistentState.GetLogEntry toSend
LeaderCommitIndex = volatileState.CommitIndex
ReplyChannel = fun reply -> reply |> Reply.AppendEntriesReply |> Message.Reply |> messageChannel me
}
|> Instruction.AppendEntries
|> Message.Instruction
|> messageChannel j
let divideByTwoRoundingUp (n : int) =
if n % 2 = 0 then n / 2 else (n / 2) + 1
let emitHeartbeat () =
match currentType with
| ServerSpecialisation.Candidate _
| ServerSpecialisation.Follower _ -> ()
| ServerSpecialisation.Leader _ ->
let lastLogEntry = persistentState.GetLastLogEntry () |> Option.map snd
let emitHeartbeat (leaderState : LeaderState) =
for i in 0 .. clusterSize - 1 do
let i = i * 1<ServerId>
for i in 0 .. clusterSize - 1 do
if i * 1<ServerId> <> me then
{
LeaderTerm = persistentState.CurrentTerm
LeaderId = me
PrevLogEntry = lastLogEntry
NewEntry = None
LeaderCommitIndex = volatileState.CommitIndex
ReplyChannel =
fun reply -> messageChannel me (reply |> Reply.AppendEntriesReply |> Message.Reply)
}
|> Instruction.AppendEntries
|> Message.Instruction
|> messageChannel (i * 1<ServerId>)
if i <> me then
sendAppendEntries leaderState i
let processReply (r : Reply) : unit =
match r with
@@ -504,13 +543,15 @@ type Server<'a>
if appendEntriesReply.FollowerTerm = persistentState.CurrentTerm then
match appendEntriesReply.Success with
| Some matchIndex ->
// They applied our request. Update our record of what we know they have applied...
leaderState.MatchIndex.[appendEntriesReply.Follower / 1<ServerId>] <- matchIndex
leaderState.NextIndex.[appendEntriesReply.Follower / 1<ServerId>] <- matchIndex + 1<LogIndex>
// ... and update our record of what we'll be sending them next.
leaderState.ToSend.[appendEntriesReply.Follower / 1<ServerId>] <- matchIndex + 1<LogIndex>
| None ->
leaderState.NextIndex.[appendEntriesReply.Follower / 1<ServerId>] <-
max
(leaderState.NextIndex.[appendEntriesReply.Follower / 1<ServerId>] - 1<LogIndex>)
1<LogIndex>
// They failed to apply our request. Next time, we'll be trying one message further
// back in our history.
leaderState.ToSend.[appendEntriesReply.Follower / 1<ServerId>] <-
max (leaderState.ToSend.[appendEntriesReply.Follower / 1<ServerId>] - 1<LogIndex>) 1<LogIndex>
// Note that the decision to process this *here* means the algorithm doesn't work in clusters of
// only one node, because then there will never be any AppendEntries replies.
let maxLogAQuorumHasCommitted =
@@ -551,11 +592,10 @@ type Server<'a>
state.Votes > clusterSize / 2
then
// Become the leader!
currentType <-
LeaderState.New clusterSize persistentState.CurrentLogIndex
|> ServerSpecialisation.Leader
let state = LeaderState.New clusterSize persistentState.CurrentLogIndex
currentType <- ServerSpecialisation.Leader state
emitHeartbeat ()
emitHeartbeat state
let mailbox =
let rec loop (mailbox : MailboxProcessor<_>) =
@@ -565,7 +605,12 @@ type Server<'a>
//System.Console.WriteLine toPrint
match m with
| ServerAction.EmitHeartbeat -> emitHeartbeat ()
| ServerAction.EmitHeartbeat ->
match currentType with
| ServerSpecialisation.Leader state -> emitHeartbeat state
| ServerSpecialisation.Candidate _
| ServerSpecialisation.Follower _ -> ()
| ServerAction.BeginElection ->
match currentType with
| ServerSpecialisation.Leader _ -> ()
@@ -592,13 +637,32 @@ type Server<'a>
| ServerAction.Receive (Message.Reply r) -> processReply r
| ServerAction.Receive (Message.ClientRequest (toAdd, replyChannel)) ->
match currentType with
| ServerSpecialisation.Leader _ ->
| ServerSpecialisation.Leader leaderState ->
persistentState.AppendToLog toAdd persistentState.CurrentTerm
replyChannel ClientReply.Acknowledged
emitHeartbeat leaderState
| ServerSpecialisation.Follower followerState ->
replyChannel (ClientReply.Redirect followerState.CurrentLeader)
| ServerSpecialisation.Candidate _ -> replyChannel ClientReply.Dropped
| ServerAction.Sync replyChannel -> replyChannel.Reply ()
| ServerAction.StateReadout replyChannel ->
{
LogIndex = persistentState.CurrentLogIndex
CurrentTerm = persistentState.CurrentTerm
CurrentVote = persistentState.VotedFor
Log =
match persistentState.GetLastLogEntry () with
| None -> []
| Some (_, last) ->
List.init
(last.Index / 1<LogIndex>)
(fun index -> persistentState.GetLogEntry (1<LogIndex> + index * 1<LogIndex>))
LeaderState =
match currentType with
| ServerSpecialisation.Leader state -> state.Clone () |> Some
| _ -> None
}
|> replyChannel.Reply
return! loop mailbox
}
@@ -617,6 +681,11 @@ type Server<'a>
member this.Message (m : Message<'a>) = mailbox.Post (ServerAction.Receive m)
member this.GetCurrentInternalState () : Async<ServerInternalState<'a>> =
mailbox.PostAndAsyncReply ServerAction.StateReadout
member this.PersistentState = persistentState
member this.Sync () =
// This rather eccentric phrasing is so that Fable can run this mailbox.
// (Fable does not support `mailbox.PostAndReply`, nor does it support