From 427dd859d6a6cd5fa9b3c9de85edc1f0b5e4d263 Mon Sep 17 00:00:00 2001 From: Ugilt Date: Tue, 16 Apr 2024 12:04:06 +0200 Subject: [PATCH 1/8] Add functions to list library Added following functions to list library: contains filter first slice insert_at_index --- lib/lists.trp | 37 +++++++++++++++++++++++++++++++++++-- lib/out/lists.exports | 8 +++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/lib/lists.trp b/lib/lists.trp index 482f875..77d42bb 100644 --- a/lib/lists.trp +++ b/lib/lists.trp @@ -9,8 +9,6 @@ let fun map f list = | (x::xs) => (f (j,x)) :: (mapj (j+1) xs) in mapj 0 list end - - fun foldl f y [] = y | foldl f y (x::xs) = foldl f (f (x,y)) xs @@ -58,6 +56,35 @@ let fun map f list = in partition_aux ([],[]) ls end + fun contains element list = + case list of + [] => false + | h :: t => + if h = element then true + else contains element t + + fun filter f l = case l of + [] => [] + | h :: t => if f h then h :: (filter f t) else filter f t + + fun first l = case l of + [] => [] + | h :: _ => h + + fun slice beg s_end l = case (beg, s_end, l) of + (_, _, []) => [] + | (0, 0, _) => [] + | (0, n, h :: t) => h :: (slice beg (n - 1) t) + | (m, n, h :: t) => slice (m - 1) (n - 1) t + | (_, _, _) => [] + + fun insert_at_index l appended_l index = + let val beg_slice = slice 0 index l + val end_slice = slice index (length l) l + in append beg_slice (append appended_l end_slice) + end + + in [ ("map", map) , ("mapi", mapi) @@ -69,5 +96,11 @@ in , ("length", length) , ("append", append) , ("partition", partition) + , ("nth", nth) + , ("contains", contains) + , ("filter", filter) + , ("first", first) + , ("slice", slice) + , ("insert_at_index", insert_at_index) ] end diff --git a/lib/out/lists.exports b/lib/out/lists.exports index d0a5830..cf5a5c3 100644 --- a/lib/out/lists.exports +++ b/lib/out/lists.exports @@ -7,4 +7,10 @@ lookup elem length append -partition \ No newline at end of file +partition +nth +contains +filter +first +slice +insert_at_index \ No newline at end of file From ee3f15eadaef6b4a5521d991360a33572402e9ec Mon Sep 17 00:00:00 2001 From: Victor Ask Justesen Date: Tue, 21 May 2024 16:42:12 +0200 Subject: [PATCH 2/8] Added Raft-Troupe to libraries --- Makefile | 3 +- examples/network/bug.zip | Bin 0 -> 2096 bytes examples/network/bug/Makefile | 15 + examples/network/bug/aliases.json | 1 + examples/network/bug/ids/test-dialer.json | 1 + examples/network/bug/ids/test-listener.json | 1 + examples/network/bug/test.trp | 10 + examples/network/bug/zero.trp | 1 + examples/network/pingpong/p2ppingpong.trp | 6 +- lib/out/raft.exports | 6 - lib/out/raft_debug.exports | 6 - lib/raft_troupe.trp | 1063 +++++++++++++++++++ 12 files changed, 1098 insertions(+), 15 deletions(-) create mode 100644 examples/network/bug.zip create mode 100644 examples/network/bug/Makefile create mode 100644 examples/network/bug/aliases.json create mode 100644 examples/network/bug/ids/test-dialer.json create mode 100644 examples/network/bug/ids/test-listener.json create mode 100644 examples/network/bug/test.trp create mode 100644 examples/network/bug/zero.trp delete mode 100644 lib/out/raft.exports delete mode 100644 lib/out/raft_debug.exports create mode 100644 lib/raft_troupe.trp diff --git a/Makefile b/Makefile index 38490f2..5a45cfd 100644 --- a/Makefile +++ b/Makefile @@ -21,10 +21,9 @@ libs: $(COMPILER) ./lib/declassifyutil.trp -l $(COMPILER) ./lib/stdio.trp -l $(COMPILER) ./lib/timeout.trp -l - $(COMPILER) ./lib/raft.trp -l - $(COMPILER) ./lib/raft_debug.trp -l $(COMPILER) ./lib/bst.trp -l $(COMPILER) ./lib/localregistry.trp -l + $(COMPILER) ./lib/raft_troupe.trp -l test: mkdir -p out diff --git a/examples/network/bug.zip b/examples/network/bug.zip new file mode 100644 index 0000000000000000000000000000000000000000..62576039911cdd6110e8e906f359b30bd8ef8e87 GIT binary patch literal 2096 zcmWIWW@h1H0D+B*)%@b8hYfhz&Ogyq+IYAsL%Y!B z)IuR^pJb-BPZ@U|Wxi~(QfRuT!adpT*;|#k=Bzm~|NnpgJ$KisX>tFgbX)Y5Q`CaVpF@S>S}NGz z3pF<$Xxc9t-ks|2zA1k~eu~WEuocc;0vd9G*Gi{V+nGE)sr5x3=rmp^emPkr9ddF!Z;-+kSE44@Fh zo+db;F`k)HjFc`wDaZ4Xdh#it>?0tSLQ(}v8@f4}#U-hEsYQBO#rb)zXAk-{8!#|j zsDCNSFJ_gZCYNxaAuOhq_fD^)^@6rAAM1{f_peU2|8P%S!s-LZ0$HnwQ(apEXT(TH zr{*NiuwEfh811re)udeM0y8lYK4rJGr~D82R#Y0ySiAqgxqdFG+|H%>)f-tnQ%^bH zH{bVU!^%X@b1P3AfBl?$+06GRiY65Xrmxuk^^4%`CcS)@C%;VBy_kO^9#ny z4;%G=dZ~YD`hWGK2S3)PW}0hne;@KC=B?R7*MJvKk7&Gzs=ijpgM=##Yk>g0eu&r%^*B(YK!9!+dE-${k ze>&N|;648ez6YE)3ch-@+-P4Jq+$H8VZk(?DN72U2i|)6_(^?TPPMM^@~;JvJUmw) zFrHYqYr^9n{1tkA<}*}Qo7T9Sw!F)DI%E2@*bAPsj~eWL_2z(o+I`*K-xfV>Db<%L z&UqwTc>Kid(3t`8hcx3N6AtQT&8@65y0WhP$4{}ZKmT&xm>P66Q|Wba%%-a3CO5%} zh5w)4=00m#v$_eFxslBLZL&FaQ+q-~Pf3^f)kg!Hb5f)S`S)(O}E~PF0X{ z1YFW|X=^qB74`zL01$&pn#7#U#NyOqa4K0Hrsb)tck%2=9X-#p-nv1Xf{aXx48NT_ z&;Q-Ctn{OYzlqidueBau{fx~x&y{)em6hsneBa*Da0=2nh^Io$8+Ak&#*G*y1^Fb9PnH&h@U_ zf^>ndWn_|Pz@7_O7*v2R1_6-EK>*}>E(QfKn~_0+f%&6-ulBNAjLd325E{ydn2M`3 zg_+s_G)D=DL8ijWR%Fv#7X4fXGF?UUH<*U;kxj=e>yS+c~$Oaf6+1;3B39`GbkW9ubS&;2& z&QU}Oc^DtvF4SC$Y?lo%EyH{PwhJi_BijV?1uV{Cd}Nz2vj?(Gwk!~vV3`Hk^wa)T z*`Tn>zE%yUVSHrMG4m6$=~+ spawned_func pid) + +in receive [hn x => printString "bug"] +end \ No newline at end of file diff --git a/examples/network/bug/zero.trp b/examples/network/bug/zero.trp new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/examples/network/bug/zero.trp @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/examples/network/pingpong/p2ppingpong.trp b/examples/network/pingpong/p2ppingpong.trp index a6313f2..2091752 100644 --- a/examples/network/pingpong/p2ppingpong.trp +++ b/examples/network/pingpong/p2ppingpong.trp @@ -4,13 +4,17 @@ let fun pingpong () = val {counter, pid=sender} = receive [hn x => x] val _ = send (sender, {counter=counter + 1, pid=self()}) val _ = print counter - in pingpong() + in + (if counter + 1 > 100 then + print (getTime()) else ()); + pingpong() end in let val processA = spawn ("@pingpong-listener", pingpong) val _ = print processA val processB = spawn ("@pingpong-dialer", pingpong) val _ = print processB + val _ = print (getTime()) val _ = send (processA,{counter = 1, pid =processB}) in () end diff --git a/lib/out/raft.exports b/lib/out/raft.exports deleted file mode 100644 index a34226e..0000000 --- a/lib/out/raft.exports +++ /dev/null @@ -1,6 +0,0 @@ -START -pre_machine -spawn_machine -CLIENT_COMMAND -CLIENT_COMMAND_RESPONSE -NOT_LEADER \ No newline at end of file diff --git a/lib/out/raft_debug.exports b/lib/out/raft_debug.exports deleted file mode 100644 index a34226e..0000000 --- a/lib/out/raft_debug.exports +++ /dev/null @@ -1,6 +0,0 @@ -START -pre_machine -spawn_machine -CLIENT_COMMAND -CLIENT_COMMAND_RESPONSE -NOT_LEADER \ No newline at end of file diff --git a/lib/raft_troupe.trp b/lib/raft_troupe.trp new file mode 100644 index 0000000..d46c3ae --- /dev/null +++ b/lib/raft_troupe.trp @@ -0,0 +1,1063 @@ +import lists + +(* + Log = { + snapshot: Snapshot + log: Entry[], + lastApplied: int, + internalChanges: int, + commitIndex: int, + latestSerials: SerialKey[] + } + Snapshot = { + snapshot: Some state + lastIncludedIndex: int, + lastIncludedTerm: int + } + Entry = { + term: int, + command: message, + serial: string + } + SerialKey = { + id: clusterId[] | pid, + key: (logIndex, number) | nonce + } +*) + +(* + LeaderInfo = { + nextIndex = { + peer: p, + next: int + }[], + matchIndex = { + peer: p, + match: int + }[] + } +*) + +(* + StateMachine = { + set_hook : fn (x: string) => x + get_hook : fn (x: string, callback_pid: string) => x + get_snapshot_hook : fn(callback_pid: string) => x + get_changes_hook : fn (callback_pid: string) => x + snapshot_condition_hook : fn (log_summary: LogSummary, callback_pid: string) => x: bool + } + LogSummary = { + log_size: int, + entries_since_snap: int + } +*) + +(* + Node = { + all_nodes: string[], + id: string, + log: Log, + term: int, + voted_for: string, + leader: string, + leader_info: LeaderInfo, + snapshot_condition: fn logSummary => ... : boolean + state_machine: ([SIDE-EFFECTS], STATUS, STEP-FUNC) + total_nodes: int, + verbose: boolean + } +*) + +(* + RaftProcesses = { + type: Client | Cluster, + id: pid | Clusterid[] + } +*) + +datatype Atoms = + WAIT | SUS | DONE + | SEND_HEARTBEAT + | RAFT_UPDATE + | NOT_LEADER + | ACKNOWLEDGE + | REJECT + | ELECTION_TIMEOUT + | REQUEST_VOTE | YES_VOTE | NO_VOTE | VOTE_TIMEOUT + | APPEND_ENTRIES | SNAPSHOT + | ADD_NODES + | DIAL + + | DIALER_ACK | DIALER_SM_BUSY | DIALER_SM_DONE | DIALER_CLIENT_MSG + | DIALER_MESSAGE_TIMEOUT | DIALER_BUSY_TIMEOUT + + | SEND_TO_NTH | SEND_TO_ALL + + | DEBUG_PRINTLOG | DEBUG_PAUSE | DEBUG_CONTINUE | DEBUG_APPLYSNAPSHOT | DEBUG_SNAPSHOT_COND | DEBUG_TIMEOUT + + | FUNCTION_DONE + + | ERROR_TIMEOUT + | CLUSTER | CLIENT + +let + (* Constants *) + val LOCAL_ERROR_TIMEOUT = 4000 + val ELECTION_TIMEOUT_LOWER = 2000 + val ELECTION_TIMEOUT_UPPER = 4000 + val HEARTBEAT_INTERVAL = 500 + + val DIALER_NOLEADER_TIMEOUT = 500 + val DIALER_NOMSG_TIMEOUT = 2000 + val DIALER_SM_BUSY_TIMEOUT = 1000 + + fun not a = a = false + fun send_to_all processes msg sender = map (fn x => send(x, msg)) (filter (fn x => x <> sender) processes) + + fun send_to_nth processes msg n = send((nth (reverse processes) n), msg) + + fun max a b = if a < b then b else a + + fun min a b = if a > b then b else a + + (* Prints if verbose is true. *) + fun verbose_print x verbose = + (* Disabled for library *) + (* if verbose then print x else *) + () + + (* #IMPORT libs/quickselect.trp *) + fun is_even i = i mod 2 = 0 + + (* Using QuickSelect, finds the kth element of a list. *) + fun quickselect list k = + case list of + [] => "ERROR: Empty list" + | h :: t => + let val (ys, zs) = partition (fn x => x > h) t + val l = length ys + in + if k < l then quickselect ys k + else if k > l then quickselect zs (k-l-1) + else h + end + + (* Returns the median of a list. *) + fun median list = + let val len = length list + val middle = if is_even len then len / 2 - 1 else (len - 1) / 2 + in quickselect list (middle) + end + (* END OF libs/quickselect.trp *) + + (* #IMPORT libs/log.trp *) + (* Creates a snapshot. *) + fun set_snapshot snapshot index term = { + snapshot = snapshot, + lastIncludedIndex = index, + lastIncludedTerm = term + } + + (* Creates a default, empty snapshot. *) + val empty_snapshot = { + snapshot = (), + lastIncludedIndex = 0, + lastIncludedTerm = 0 + } + + (* A default, empty log. *) + val empty_log = { + log = [], + snapshot = empty_snapshot, + lastApplied = 0, + commitIndex = 0, + lastMessageSerial = "" + } + + fun pretty_print_log id log = + (* Disabled for library *) + (* printString "\n========******========"; + print (length log.log); + printString ("ID: "^id); + printString "----------------------"; + printString "Entries (term, message):"; + map (fn x => print (x.term, x.command)) log.log; + printString "----------------------"; + printString "CommitIndex:"; + print log.commitIndex; + printString "LastApplied:"; + print log.lastApplied; + printString "----------------------"; + printString "Snapshot:"; + print log.snapshot; + printString "========******========\n";*) + () + + (* Appends a message to the log, and notes the message's serial number. *) + fun append_message log message callback term serial = + let val new_entry = { + term = term, + command = message, + callback = callback, + serial = serial + } + in { + log with + lastMessageSerial = serial, + log = new_entry :: log.log + } + end + + + (* Appends a list of message to the log. *) + fun add_entries_to_log log entries term = + case entries of + [] => log + | h :: t => + add_entries_to_log (append_message log h.command h.callback term h.serial) t + h.term + + (* Updates the lastApplied-index. *) + fun update_applied log = { + log with + lastApplied = log.lastApplied + 1 + } + + (* Commits a message in the log. *) + fun update_commit log new_index = { + log with + commitIndex = (max new_index log.commitIndex) + } + + (* Rolls the log back one entry. *) + fun rollback_log log = + let val loglog = log.log + in case loglog of + (_ :: prev_log) => { + log with + log = prev_log + } + | [] => {log with log = []} + end + + (* Get the entry of the latest log entry. *) + fun get_log_index log = (length log.log) + log.snapshot.lastIncludedIndex + + (*Determines whether or not all log changes have been committed. *) + fun log_is_committed log = (get_log_index log = log.commitIndex) + + (* Rolls the log back n time. *) + fun rollback_log_to log n = + if n < (get_log_index log) then + let val log = rollback_log log + in (rollback_log_to log n) + end + else log + + (* Get the term of the latest entry of the log, or, if empty, the last + included index of the snapshot. *) + fun get_latest_entry_term log = + case log.log of + [] => log.snapshot.lastIncludedTerm + | h :: _ => h.term + + (* Get the term of the latest log entry. *) + fun get_latest_log_term log = get_latest_entry_term log + + (* Get the message of the latest log entry. *) + fun get_latest_log_command log = + case log.log of + [] => 0 (* Should not be reachable. *) + | h :: _ => h.command + + fun get_nth_command log index = nth (reverse log.log) (index - log.snapshot.lastIncludedIndex) + + (* Returns a slice of all entries after log-index n. *) + fun get_commands_after_nth entries n last_included = + let val log_slice = slice (n - last_included) (length entries) (reverse entries) + in log_slice + end + + (* Get a snapshot of all committed entries. *) + fun get_snapshot state log = + if log.commitIndex > 0 andalso + (log.commitIndex - log.snapshot.lastIncludedIndex) <= length log.log then + let val lastCommitted = get_nth_command log log.commitIndex + in set_snapshot state log.commitIndex lastCommitted.term end + else empty_snapshot + + + (* Applies a snapshot to the log. *) + fun apply_snapshot snapshot log = + let val newCommitIndex = + if log.commitIndex < snapshot.lastIncludedIndex then snapshot.lastIncludedIndex + else log.commitIndex + val uncommitted_entries = get_commands_after_nth log.log newCommitIndex log.snapshot.lastIncludedIndex + val newLastApplied = + if log.lastApplied < snapshot.lastIncludedIndex then snapshot.lastIncludedIndex + else log.lastApplied + in { log with + log = uncommitted_entries, + commitIndex = newCommitIndex, + lastApplied = newLastApplied, + snapshot = snapshot } + end + + (* Asks the state-machine whether or not to snapshot. *) + fun evaluate_snapshot_cond state snapshot_cond log = + if (log.lastApplied - log.snapshot.lastIncludedIndex) > snapshot_cond then + apply_snapshot (get_snapshot state log) log + else log + (* END OF libs/log.trp *) + + (* #IMPORT libs/leader-info.trp *) + + (* Generates a default leader info, with the nextIndex of all followers + being the nextIndex of the new leader. This can be changed with followers + rejecting AppendEntries *) + fun new_leader all_nodes log = + let val nextIndex = get_log_index log + val index = map (fn id => {peer = id, next = nextIndex + 1}) all_nodes + val match_index = map (fn id => {peer = id, match = 0}) all_nodes + in { + nextIndex = index, + matchIndex = match_index + } end + + (* Get the nextIndex of a peer *) + fun get_next_index leader_info peer = first (filter (fn (x) => x.peer = peer) leader_info.nextIndex) + + (* Get the matchIndex of a peer *) + fun get_match_index leader_info peer = first (filter (fn (x) => x.peer = peer) leader_info.matchIndex) + + (* Updates a cluster member's next-index. This is done after an + acknowledgement or rejection. *) + fun update_next_index leader_info peer new = let + val prevIndex = get_next_index leader_info peer + val newIndex = {peer = peer, next = new} + val withoutPeer = filter (fn (x) => x.peer <> peer) leader_info.nextIndex + in { + leader_info with + nextIndex = newIndex :: withoutPeer + } end + + (* Updates a cluster member's match-index, denoting how much of their log + matches the leader holding the leader info. *) + fun update_match_index leader_info peer new = let + val prevIndex = get_match_index leader_info peer + val newIndex = {peer = peer, match = new} + val withoutPeer = filter (fn (x) => x.peer <> peer) leader_info.matchIndex + in { + leader_info with + matchIndex = newIndex :: withoutPeer + } end + + (* Get all follower's matchIndex*) + fun get_matches leader_info = map (fn x => x.match) leader_info.matchIndex + + (* Get the highest index of entries that a majority of followers have + appended to, by finding the median *) + fun calc_highest_commit matches = median matches + (* END OF libs/leader-info.trp *) + + (* Executes a function after a given timeout. *) + fun start_timeout func duration = + let fun timeout () = + let val time = duration + val _ = sleep time + in func () + end + val p_id = self() + in spawn timeout + end + + (* Send message after a delay. *) + fun send_delay (to, m) delay = + sleep delay; + send (to, m) + + (* Starts a random timeout with lower=2sec and upper=4sec *) + fun start_random_timeout func = start_timeout func (ELECTION_TIMEOUT_LOWER + ((random ()) * (ELECTION_TIMEOUT_UPPER - ELECTION_TIMEOUT_LOWER))) + + (* #IMPORT ./libs/dialer.trp *) + +(* Selects a random element from a list *) +fun random_element list = + let fun roundUp n m = + if n <= 0 then m else roundUp (n - 1) (m + 1) + val r_n = roundUp (random() * (length list - 1)) 0 + in nth list r_n +end + + +(* Given a list of serialkeys, and a serial key, check if it is valid, and +return a list containing the serial key if so, and a boolean denoting whether or +not it is valid. *) +fun apply_serialkey list key = + case list of + [] => (true, []) + | h :: t => + if h = key then + case (h.key, key.key) of + ((log_index, seq_numb), (new_log_index, new_seq_numb)) => + if new_log_index > log_index orelse + (log_index = new_log_index andalso new_seq_numb > seq_numb) then + (true, ({ h with key = key.key } :: t)) + else (false, h :: t) + | (nonce, new_nonce) => + if nonce <> new_nonce then + (true, ({ h with key = key.key } :: t)) + else (false, h :: t) + | _ => (true, ({ h with key = key.key } :: t)) + else + let val (cond, list) = apply_serialkey t key + in (cond, h :: list) + end + + +(* Used by the dialer to send message to a cluster. If the nodes are busy or if +no leader is present, this function re-sends the message until it is eventually +delivered and acknowledged by the leader of the cluster. If leader is unknown, +it can be defined as unit.*) +fun dialer_send_message p_id msg serial_n leader cluster = + let val nonce = mkuuid() + val msg_timeout = start_timeout (fn() => send(p_id, (DIALER_MESSAGE_TIMEOUT, nonce))) + val busy_timeout = start_timeout (fn() => send(p_id, (DIALER_BUSY_TIMEOUT, nonce))) + fun wait () = + receive [ + hn (NOT_LEADER, leader_id) => + dialer_send_message p_id msg serial_n leader_id cluster, + hn (DIALER_ACK, other_serial) when other_serial = serial_n => + leader, + hn (DIALER_SM_BUSY, other_serial) when other_serial = serial_n => + busy_timeout DIALER_SM_BUSY_TIMEOUT; + wait (), + hn (DIALER_SM_DONE, other_serial) when other_serial = serial_n => + leader, + hn (DIALER_MESSAGE_TIMEOUT, x) => + if x = nonce then dialer_send_message p_id msg serial_n (random_element cluster) cluster + else wait (), + hn (DIALER_BUSY_TIMEOUT, x) => + if x = nonce then dialer_send_message p_id msg serial_n leader cluster + else wait () + ] + in (case leader of + () => msg_timeout DIALER_NOLEADER_TIMEOUT + | x => + msg_timeout DIALER_NOMSG_TIMEOUT; + send(x, msg)); + wait () +end + +(* Facilitates client-side interaction to the Raft cluster. Allows the +programmer to send messages to the cluster in the format (RAFT_UPDATE, msg)*) +fun dialer cluster client_id = + let val p_id = self() + fun update_message x leader = let + val serial_n = mkuuid() + in dialer_send_message p_id ((RAFT_UPDATE, x), p_id, serial_n) serial_n leader cluster + end + val leader = random_element cluster + + fun loop leader sks = + receive [ + hn (RAFT_UPDATE, x) => + loop (update_message x leader) sks, + + hn (DIALER_CLIENT_MSG, msg, sk) => + let val (cond, sks) = apply_serialkey sks sk + in + (if cond then send(client_id, msg) + else ()); + loop leader sks + end, + + hn (SEND_TO_NTH, n, x) => + send_to_nth cluster x n; + loop leader sks, + + hn (SEND_TO_ALL, x) => + send_to_all cluster x (self()); + loop leader sks, + hn _ => loop leader sks ] + in loop leader [] +end + +(* Temporary dialer sends a list of messages to a cluster before terminating. *) +fun leader_dialer cluster msgs = + let val p_id = self() + val leader = random_element cluster + in + map (fn (msg, serial) => dialer_send_message p_id ((RAFT_UPDATE, msg), p_id, serial) serial leader cluster) msgs +end + +(* Send-function used for clusters to send a message to either the dialer or +client. *) +fun raft_send (process, msgs) = case process.type of +CLIENT => map (fn (msg, sk) => send(process.id, (DIALER_CLIENT_MSG, msg, sk))) msgs +| CLUSTER => spawn (fn () => leader_dialer process.id msgs) +(* END OF ./libs/dialer.trp *) + + (* Send the side-effect-messages to dialers or clusters *) + fun send_sides log sides = + (* Add message to key-value-store, sorting by the recipients. *) + let fun add_msg id msg sk dict = case dict of + [] => [(id, [(msg, sk)])] + | (other_id, msgs) :: t => + if id = other_id then + (id, (msg, sk) :: t) + else (other_id, msgs) :: add_msg id msg sk t + (* Generate key-value-store of all message, sorting by recipients. *) + val (sorted_msgs, _) = case sides of + [] => ([], 0) + | x => foldl (fn ((callback, msg), (acc, seq)) => + (add_msg callback msg ({ id = callback, key = (log.lastApplied, seq)}) acc, seq + 1) + ) ([], 1) x + (* Sends all messages. *) + in map (fn x => raft_send x) sorted_msgs + end + + (* Applies all log-entries that have been committed, but not applied *) + fun apply_log log state_machine is_leader = + (* If any non-applied, committed logs apply... *) + if log.lastApplied < log.commitIndex then + (* Get the latest non-applied committed entry *) + let val entry = get_nth_command log (log.lastApplied + 1) + val command = entry.command + (* Update log to apply entry and apply entry on state-machine*) + val log = update_applied log + val (sides, status, step) = state_machine + val (new_sides, new_status, new_step) = step command + (* If leader is applying, execute side-effects. *) + in (if is_leader then + entry.callback (); + send_sides log new_sides + else ()); + apply_log log (new_sides, new_status, new_step) is_leader end + else (log, state_machine) + + (* #IMPORT ./libs/nodes/leader.trp *) +fun leader_node node = + let val p_id = self() + (* Appends appends all entries from a follower's nextIndex to the leader's log index*) + fun append_entries node follower_pid = + let val nextIndex = get_next_index node.leader_info follower_pid + val logIndex = get_log_index node.log + in if logIndex + 1 >= nextIndex.next then + let + val latestLogIndex = nextIndex.next - 1 + in + (* Sends the snapshot if the followers nextIndex is before the Snapshot's lastIncludedIndex *) + if nextIndex.next <= node.log.snapshot.lastIncludedIndex + then send(follower_pid, (SNAPSHOT, node.log.snapshot, p_id, node.term)) + else + let val entries = get_commands_after_nth node.log.log latestLogIndex node.log.snapshot.lastIncludedIndex + val afterSnapshot = latestLogIndex - node.log.snapshot.lastIncludedIndex + val prevEntryTerm = + if afterSnapshot > 0 then (get_nth_command node.log latestLogIndex).term + else node.log.snapshot.lastIncludedTerm + in send(follower_pid, (APPEND_ENTRIES, entries, p_id, node.term, latestLogIndex, prevEntryTerm, node.log.commitIndex)) + end + end + (* A follower should never get more entries than the leader *) + else () + end + + (* Convert leader to follower *) + fun demote term leader voted_for node = + {node with + term = term, + leader = leader, + leader_info = (), + voted_for = voted_for} + + fun append_update node msg callback serial = + let val latestLogIndex = get_log_index node.log + val prevLogTerm = get_latest_log_term node.log + val log = append_message node.log msg callback node.term serial + val leader_info = update_match_index node.leader_info p_id (get_log_index log) + val leader_info = update_next_index leader_info p_id ((get_log_index log) + 1) + val node = {node with log = log, leader_info = leader_info} + in + verbose_print (node.id^": Appending new message to log") node.verbose; + node + end + + (* Applies all committed log entries that have not already been applied *) + fun apply_committed node = + let val prev_commit = node.log.commitIndex + val highest_commit = calc_highest_commit (map (fn x => x.match) node.leader_info.matchIndex) + val node = { node with log = update_commit node.log highest_commit } + val (applied_log, new_sm) = apply_log node.log node.state_machine true + val snapshot_log = + if prev_commit < highest_commit then + evaluate_snapshot_cond new_sm node.snapshot_cond applied_log + else + applied_log + val (_, status, _) = new_sm + val node = { node with log = snapshot_log, state_machine = new_sm} + in + case status of + SUS => if log_is_committed node.log then append_update node () (fn () => ()) (mkuuid()) + else node + | _ => node + end + + val nonce = mkuuid () + + fun loop node = + receive [ + (* Halts the leader *) + hn DEBUG_PAUSE => + let fun pause () = receive [ + hn (DEBUG_CONTINUE) => loop node, + hn x => pause () + ] + in pause () end, + + hn (SEND_HEARTBEAT, x) when nonce = x => + verbose_print (node.id^": Sending heartbeat") node.verbose; + leader_node node, + + hn (SEND_HEARTBEAT, x) => + loop node, + + (* Message has not been appended before *) + hn ((RAFT_UPDATE, x), dialer_id, serial_n) => + let val (cond, sks) = apply_serialkey node.serialkeys serial_n + in if cond then let + val (_, stat, _) = node.state_machine + val node = case stat of + SUS => send(dialer_id, (DIALER_SM_BUSY, serial_n)); node + | DONE => send(dialer_id, (DIALER_SM_DONE, serial_n)); node + | WAIT => + if log_is_committed node.log then + let fun replication_cb () = send (dialer_id, (DIALER_ACK, serial_n)) + in append_update node x replication_cb serial_n end + else send(dialer_id, (DIALER_SM_BUSY, serial_n)); node + in leader_node node end + else send(dialer_id, (DIALER_ACK, serial_n)); + loop node + end, + + (* If append is successful on a follower*) + hn (ACKNOWLEDGE, (peer, logIndex)) => + let val prev_index = get_log_index node.log + val node = { node with leader_info = update_match_index node.leader_info peer logIndex } + val node = { node with leader_info = update_next_index node.leader_info peer (logIndex + 1) } + val node = apply_committed node + val next_index = get_next_index node.leader_info peer + in (if prev_index < get_log_index node.log then + map (fn x => append_entries node x) + (filter (fn x => + let val next_index = get_next_index node.leader_info x + in x <> p_id andalso next_index.next > logIndex end) node.all_nodes) + else if next_index.next <= get_log_index node.log then + append_entries node peer + else ()); + loop node + end, + + (* If append is unsuccessful *) + hn (REJECT, (peer, terminfo, logIndex)) => + if node.term >= terminfo.term then + let val node = { node with leader_info = update_next_index node.leader_info peer (logIndex + 1) } + in loop node + end + else follower (demote terminfo.term terminfo.leader () + node), + + (* If another node has been elected as a candidate, and + their term is in front of ours, convert to a follower *) + hn (REQUEST_VOTE, (c_term, c_id, c_log_index, c_log_term)) when c_term > node.term => + verbose_print (node.id^": Voting yes") node.verbose; + send(c_id, (YES_VOTE, node.id)); + follower (demote c_term () c_id node), + + hn (REQUEST_VOTE, (c_term, c_id, c_log_index, c_log_term)) => + send(c_id, (NO_VOTE, node.id)); + loop node, + + (* If we receive snapshot from a leader in a higher term, + convert to follower *) + hn (SNAPSHOT, snapshot, l_id, other_term) when other_term > node.term => + verbose_print (node.id^": received Snapshot from leader, I must have lost position") node.verbose; + follower (demote other_term l_id () node), + + (* If we receive AppendEntries from a leader in a higher term, + convert to follower *) + hn (APPEND_ENTRIES, x, l_id, other_term, prevIndex, prevTerm, commitIndex) when other_term > node.term => + verbose_print (node.id^": received AppendEntries from leader, I must have lost position") node.verbose; + follower (demote other_term l_id () node), + + (* Prints log *) + hn DEBUG_PRINTLOG => + pretty_print_log node.id node.log; + loop node, + + (* Applies a snapshot *) + hn DEBUG_APPLYSNAPSHOT => + let + val snapshot = get_snapshot node.state_machine node.log + val node = case snapshot.snapshot of + () => node + | _ => {node with log = apply_snapshot snapshot node.log} + in + verbose_print (node.id^": applying snapshot") node.verbose; + loop node end, + hn _ => loop node + ] + in + (* Append entries for each follower *) + map (fn x => append_entries node x) (filter (fn x => x <> p_id) node.all_nodes); + start_timeout (fn () => send (p_id, (SEND_HEARTBEAT, nonce))) HEARTBEAT_INTERVAL; + loop node +end +(* END OF ./libs/nodes/leader.trp *) + (* #IMPORT ./libs/nodes/candidate.trp *) +and candidate node = + let val p_id = self() + + (* A candidate cannot vote for anyone and has no leader *) + val node = {node with voted_for = (), leader = ()} + val nonce = mkuuid() + + (* Sends a vote request to all followers *) + val latestLogIndex = get_log_index node.log + val prevLogTerm = get_latest_log_term node.log + + + (* Becoming a leader requires majority vote *) + val req_votes = ((length node.all_nodes) / 2) + + fun won_election () = + let val (sides, _, _) = node.state_machine + in + verbose_print (node.id^": I won the election") node.verbose; + send_sides node.log sides; + leader_node ({ + node with leader_info = (new_leader node.all_nodes node.log), + leader = (p_id)}) + end + + fun wait_for_votes (follower_votes, vote_amount) = + let + fun loop () = receive [ + (* Received a vote from a follower we have not already + received a vote from *) + hn (YES_VOTE, follower_id) when (not (contains follower_id follower_votes)) => + wait_for_votes ((append follower_votes [follower_id]), vote_amount + 1), + + (*We received a NO_VOTE from a follower in a later term. + This can only happen if there is a leader/candidate in this + term, and as such, we convert to a follower *) + hn (NO_VOTE, other_term) when other_term > node.term => + follower node, + + (* Received vote request from candidate in later term *) + hn (REQUEST_VOTE, (c_term, other_c_id, c_log_index, c_log_term)) when c_term > node.term => + send(other_c_id, (YES_VOTE, node.id)); + follower ({ node with term = c_term, voted_for = other_c_id}), + + (* Received message from leader in a term at least as + up-to-date as ours. Because of this, we must have lost the + election *) + hn (APPEND_ENTRIES, x, l_id, other_term, prevIndex, prevTerm, commitIndex) when other_term >= node.term => + verbose_print (node.id^": received AppendEntries from leader, I must have lost position") node.verbose; + follower ({ node with leader = l_id}), + + (* Election timeout, send out another request vote *) + hn (VOTE_TIMEOUT, x) when x = nonce => candidate {node with term = node.term + 1}, + + (* Halts the candidate *) + hn (DEBUG_PAUSE) => + let fun loop () = receive [ + hn (DEBUG_CONTINUE) => (), + hn x => loop () + ] + in loop () end, + hn _ => loop () + ] + in if vote_amount >= req_votes then won_election () else loop () + end + in + verbose_print (node.id^": I am now a candidate") node.verbose; + send_to_all node.all_nodes (REQUEST_VOTE, (node.term, p_id, latestLogIndex, prevLogTerm)) (p_id); + start_random_timeout (fn () => send(p_id, (VOTE_TIMEOUT, nonce))); + wait_for_votes ([node.id], 1) +end +(* END OF ./libs/nodes/candidate.trp *) + (* #IMPORT ./libs/nodes/follower.trp *) +and follower node = + let val nonce = mkuuid() + val p_id = self() + val _ = start_random_timeout (fn () => send(p_id, (ELECTION_TIMEOUT, nonce))) + (* Sends a YES_VOTE to a candidate *) + fun vote_for c_id c_term node = + send(c_id, (YES_VOTE, node.id)); + { node with term = c_term, voted_for = c_id } + fun loop node start_time = let + fun start_election () = + verbose_print (node.id^": START ELECTION") node.verbose; + candidate ({node with term = node.term + 1}) + val _ = receive [ + (* Starts an election *) + hn (ELECTION_TIMEOUT, x) when x = nonce => + if (getTime() - start_time >= ELECTION_TIMEOUT_LOWER) then start_election () + else start_random_timeout (fn () => send(p_id, (ELECTION_TIMEOUT, nonce))); loop node (getTime()), + + (* Sends a re-vote to a candidate we already voted for *) + hn (REQUEST_VOTE, (c_term, c_id, c_log_index, c_log_term)) when c_id = node.voted_for => + verbose_print (node.id^": Voting yes") node.verbose; + follower (vote_for c_id c_term node), + + (* If we receive a vote request, vote yes if: the log is a + up-to-date and the term of the candidate is later than our + current. Vote no otherwise *) + hn (REQUEST_VOTE, (c_term, c_id, c_log_index, c_log_term)) => + let val latestLogIndex = get_log_index node.log + val latestLogTerm = get_latest_log_term node.log + fun no_vote () = + send(c_id, NO_VOTE); + verbose_print (node.id^":voting no") node.verbose; + follower node + fun yes_vote () = + verbose_print (node.id^": Voting yes") node.verbose; + follower (vote_for c_id c_term ({node with term = c_term})) + in + if latestLogIndex > c_log_index + orelse latestLogTerm <> c_log_term + orelse c_term <= node.term then no_vote () + else yes_vote () + end, + + (* When receiving a snapshot from a leader in a later or + same term, acknowledge if it contains entries past our + current log index. Update leader and term accordingly. *) + hn (SNAPSHOT, x, l_id, leader_term) => + let val node = {node with leader = + if node.leader = () orelse node.term < leader_term then l_id + else node.leader} + + val {snapshot, lastIncludedIndex, lastIncludedTerm} = x + val log_term = get_latest_log_term node.log + val log_index = get_log_index node.log + + val accepting = + if leader_term < node.term then false + else if lastIncludedIndex <= log_index then false + else true + + val newlog = if accepting then apply_snapshot x node.log else node.log + val new_sm = if accepting then snapshot else node.state_machine + val reject = + fn () => send (l_id, (REJECT, (p_id, {term = node.term, leader = node.leader}, (get_log_index newlog)))) + val ack = + fn () => send (l_id, (ACKNOWLEDGE, (p_id, get_log_index newlog))) + + val node = { + node with term = (if node.term < leader_term then leader_term else node.term), + state_machine = new_sm, + log = newlog} + + in (if accepting then verbose_print (node.id^": ACCEPTING SNAPSHOT") node.verbose; ack () + else verbose_print (node.id^": REJECTING SNAPSHOT") node.verbose; reject ()); + loop node (getTime()) + end, + + (* When receiving entries from a leader in a later or + same term, acknowledge if it contains entries past our + current log index. And if the latest log index matches ours. + Update log accordingly.*) + hn (APPEND_ENTRIES, x, l_id, leader_term, latestLogIndex, prevLogTerm, leaderCommit) => + let val node = {node with leader = + if node.leader = () orelse node.term <= leader_term then l_id + else node.leader} + val accepting = + if leader_term < node.term then verbose_print "Old Leader" node.verbose; false + else if latestLogIndex > (get_log_index node.log) then + verbose_print "Leader is way ahead of me" node.verbose; + false + else if (get_latest_log_term node.log) <> prevLogTerm andalso prevLogTerm > 0 then + verbose_print "Leader has inconsistent log with me" node.verbose; + false + else true + val prev_commit = node.log.commitIndex + val newlog = + if accepting then + let val log = rollback_log_to node.log latestLogIndex + val log = add_entries_to_log log x leader_term + in update_commit log (min leaderCommit (get_log_index log)) + end + else node.log + val reject = + fn () => send (l_id, (REJECT, (p_id, {term = node.term, leader = node.leader}, (get_log_index newlog)))) + val ack = + fn () => send (l_id, (ACKNOWLEDGE, (p_id, get_log_index newlog))) + + val node = {node with term = (if node.term < leader_term then leader_term else node.term)} + val (applied_log, new_sm) = apply_log newlog node.state_machine false + val snapshot_log = + if prev_commit < applied_log.commitIndex then + evaluate_snapshot_cond new_sm node.snapshot_cond applied_log + else + applied_log + in + (if accepting then + verbose_print (node.id^": ACCEPTING") node.verbose; + ack () + else + verbose_print (node.id^": REJECTING") node.verbose; + reject ()); + loop {node with log = snapshot_log, state_machine = new_sm} (getTime()) + end, + + (* If client sends update, sends the leader's id *) + hn ((RAFT_UPDATE, x), dialer_id, _) => + send(dialer_id, (NOT_LEADER, node.leader)); + loop node start_time, + + (* Prints the log *) + hn (DEBUG_PRINTLOG) => + pretty_print_log node.id node.log; + loop node start_time, + + (* Halts the follower *) + hn (DEBUG_PAUSE) => + let fun paused () = receive [ + hn (DEBUG_CONTINUE) => (), + hn _ => paused () + ] + in + paused (); + loop node start_time + end, + + (* Start an election, electing this follower to a candidate *) + hn (DEBUG_TIMEOUT) => start_election (), + hn x => loop node start_time + ] + in () + end + in loop node (getTime ()) +end +(* END OF ./libs/nodes/follower.trp *) + + (* A node is dormant until it has received the references of all other nodes. *) + fun dormant_node node = + if length(node.all_nodes) < node.node_amount then + receive [ + (* Adds a node to the cluster, only used for initialization *) + hn (ADD_NODES, x) => + dormant_node ({node with all_nodes = append node.all_nodes x}) + ] + else follower node + + (* Defines a default node, being a follower in term 1 without a leader and + the state-machine in its beginning state *) + fun default_node id all_nodes node_amount state_machine snapshot_cond verbose = + let val node = { + all_nodes = all_nodes, + id = id, + log = empty_log, + term = 1, + voted_for = (), + leader = (), + leader_info = (), + state_machine = case state_machine of + (_, _, _) => state_machine + | _ => ([], WAIT, fn x => x ()), + snapshot_cond = snapshot_cond, + node_amount = node_amount, + serialkeys = [], + verbose = verbose + } + in dormant_node node + end + + (* Spawn a state-machine on a seperate thread, creates a record*) + fun initiate_node state_machine snapshot_cond node_amount id verbose = + spawn (fn () => default_node id [] node_amount state_machine snapshot_cond verbose) + + (* Sends a list of all nodes to all nodes *) + fun add_refs nodes = + map (fn x => send(x, (ADD_NODES, nodes))) nodes + + (* Spawn n nodes*) + fun initiate_nodes n state_machine snapshot_cond verbose = + let val part_init = initiate_node state_machine snapshot_cond n + fun spawn_nodes n acc_id = + case n of + 0 => [] + | x => append + (spawn_nodes (x - 1) (acc_id ^ "I")) + [(part_init acc_id verbose)] + + val nodes = spawn_nodes n "I" + in + add_refs nodes; + nodes + end + + (* Spawn a state-machine on some alias *) + fun initiate_distributed_node state_machine snapshot_cond node_amount id alias verbose = + spawn(alias, fn () => (default_node id [] node_amount state_machine snapshot_cond verbose)) + + fun initiate_distributed_nodes aliases state_machine snapshot_cond verbose = + let val part_init = initiate_distributed_node state_machine snapshot_cond (length(aliases)) + fun spawn_nodes acc acc_id = + case acc of + [] => [] + | h :: t => + append (spawn_nodes t (acc_id ^ "I")) [part_init acc_id h verbose] + val nodes = spawn_nodes aliases "I" + in + add_refs nodes; + nodes + end + + (* Spawns a dialer, dialing into a cluster. *) + fun raft_dial (cluster, client_id) = + spawn(fn () => dialer cluster client_id) + + (* Spawns a distributed Raft network, which can be dialed into to + communicate with their state-machines *) + fun raft_spawn_alias (state_machine, aliases, snapshot_cond, verbose) = + initiate_distributed_nodes aliases state_machine snapshot_cond verbose + | raft_spawn_alias (state_machine, aliases) = + raft_spawn_alias (state_machine, aliases, 50, false) + + (* Spawns a Raft network, which can be contacted to + communicate with their state-machines *) + fun raft_spawn (state_machine, n, snapshot_cond, verbose) = + initiate_nodes n state_machine snapshot_cond verbose + | raft_spawn (state_machine) = raft_spawn (state_machine, 5, 50, false) + + (*fun raft_d (state_machine, client_id, aliases, snapshot_cond, verbose) = + let val nodes = raft_spawn_alias (state_machine, aliases, snapshot_cond, verbose) + in (raft_dial (nodes, client_id), nodes) + end + | raft_d (state_machine, client_id, aliases) = + raft_d (state_machine, client_id, aliases, 50, false) + + fun raft (state_machine, client_id, n, snapshot_cond, verbose) = + let val nodes = raft_spawn (state_machine, n, snapshot_cond, verbose) + in (raft_dial (nodes, client_id), nodes) + end + | raft (state_machine, client_id) = + raft (state_machine, client_id, 5, 50, false) + + val default_aliases = ["@node1", "@node2", "@node3", "@node4", "@node5"]*) + +in + [ ("raft_dial", raft_dial) + , ("raft_spawn_alias", raft_spawn_alias) + , ("raft_spawn", raft_spawn) + , ("WAIT", WAIT) + , ("SUS", SUS) + , ("DONE", DONE) + , ("RAFT_UPDATE", RAFT_UPDATE) + , ("CLIENT", CLIENT) + , ("CLUSTER", CLUSTER) + ] +end From d3ce5ff3de0de83b4469929d4131d022c64489c8 Mon Sep 17 00:00:00 2001 From: Victor Ask Justesen Date: Tue, 21 May 2024 16:42:54 +0200 Subject: [PATCH 3/8] Deleted temporary modes --- examples/network/bug/Makefile | 15 --------------- examples/network/bug/aliases.json | 1 - examples/network/bug/ids/test-dialer.json | 1 - examples/network/bug/ids/test-listener.json | 1 - examples/network/bug/test.trp | 10 ---------- examples/network/bug/zero.trp | 1 - 6 files changed, 29 deletions(-) delete mode 100644 examples/network/bug/Makefile delete mode 100644 examples/network/bug/aliases.json delete mode 100644 examples/network/bug/ids/test-dialer.json delete mode 100644 examples/network/bug/ids/test-listener.json delete mode 100644 examples/network/bug/test.trp delete mode 100644 examples/network/bug/zero.trp diff --git a/examples/network/bug/Makefile b/examples/network/bug/Makefile deleted file mode 100644 index 193c2b2..0000000 --- a/examples/network/bug/Makefile +++ /dev/null @@ -1,15 +0,0 @@ -MKID=node $(TROUPE)/rt/built/p2p/mkid.mjs -MKALIASES=node $(TROUPE)/rt/built/p2p/mkaliases.js -START=$(TROUPE)/network.sh - -zero.listener: - $(START) zero.trp --id=ids/test-listener.json --rspawn=true --aliases=aliases.json --stdiolev={} # --debug --debugp2p - -test.dialer: - $(START) test.trp --id=ids/test-dialer.json --aliases=aliases.json # --debug --debugp2p - -create-network-identifiers: - mkdir -p ids - $(MKID) --outfile=ids/test-listener.json - $(MKID) --outfile=ids/test-dialer.json - $(MKALIASES) --include ids/test-listener.json --include ids/test-dialer.json --outfile aliases.json diff --git a/examples/network/bug/aliases.json b/examples/network/bug/aliases.json deleted file mode 100644 index bb60efe..0000000 --- a/examples/network/bug/aliases.json +++ /dev/null @@ -1 +0,0 @@ -{"test-listener":"12D3KooWLcEDHga2pJexeKf34SkFcSEApWH9cBfejSQ1ijK4vyf7","test-dialer":"12D3KooWPZqq8atHw82spMogKmA6RZrbyzHYTUuspX4nCEgcwM8k"} \ No newline at end of file diff --git a/examples/network/bug/ids/test-dialer.json b/examples/network/bug/ids/test-dialer.json deleted file mode 100644 index ba6d43e..0000000 --- a/examples/network/bug/ids/test-dialer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"12D3KooWPZqq8atHw82spMogKmA6RZrbyzHYTUuspX4nCEgcwM8k","privKey":"CAESQEKXDF2N8k22Ay+bEOg+1cLzst7w0xsadOftWr9rnvspzEsuo4temSVKAyRC0iaoqho4cGMI+2U8n46ZftL1kVE=","pubKey":"CAESIMxLLqOLXpklSgMkQtImqKoaOHBjCPtlPJ+OmX7S9ZFR"} \ No newline at end of file diff --git a/examples/network/bug/ids/test-listener.json b/examples/network/bug/ids/test-listener.json deleted file mode 100644 index 6a6df84..0000000 --- a/examples/network/bug/ids/test-listener.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"12D3KooWLcEDHga2pJexeKf34SkFcSEApWH9cBfejSQ1ijK4vyf7","privKey":"CAESQE9L+uwGkxBHWvTPhhaAof1aX1TLZIkNjgsOKQJA3oTnoFSXJ4h2unqF2kQxYLkNuRDXw0u+nXr4Z518vVPxrjo=","pubKey":"CAESIKBUlyeIdrp6hdpEMWC5DbkQ18NLvp16+GedfL1T8a46"} \ No newline at end of file diff --git a/examples/network/bug/test.trp b/examples/network/bug/test.trp deleted file mode 100644 index fba2cac..0000000 --- a/examples/network/bug/test.trp +++ /dev/null @@ -1,10 +0,0 @@ -datatype Atoms = TEST - -let fun spawned_func dialer = - send(dialer, TEST) - - val pid = self () - val receiver = spawn("@test-listener", fn () => spawned_func pid) - -in receive [hn x => printString "bug"] -end \ No newline at end of file diff --git a/examples/network/bug/zero.trp b/examples/network/bug/zero.trp deleted file mode 100644 index c227083..0000000 --- a/examples/network/bug/zero.trp +++ /dev/null @@ -1 +0,0 @@ -0 \ No newline at end of file From 497aaf2012b70989c51292e5547292b403cb921b Mon Sep 17 00:00:00 2001 From: Victor Ask Justesen Date: Fri, 24 May 2024 08:58:49 +0200 Subject: [PATCH 4/8] Changed raft-troupe library --- lib/raft_troupe.trp | 109 ++++++++++++-------------------------------- 1 file changed, 30 insertions(+), 79 deletions(-) diff --git a/lib/raft_troupe.trp b/lib/raft_troupe.trp index d46c3ae..81a16b1 100644 --- a/lib/raft_troupe.trp +++ b/lib/raft_troupe.trp @@ -76,7 +76,7 @@ import lists *) datatype Atoms = - WAIT | SUS | DONE + WAIT | SUS | DONE | SEND_HEARTBEAT | RAFT_UPDATE | NOT_LEADER @@ -120,12 +120,6 @@ let fun min a b = if a > b then b else a - (* Prints if verbose is true. *) - fun verbose_print x verbose = - (* Disabled for library *) - (* if verbose then print x else *) - () - (* #IMPORT libs/quickselect.trp *) fun is_even i = i mod 2 = 0 @@ -578,9 +572,7 @@ fun leader_node node = val leader_info = update_match_index node.leader_info p_id (get_log_index log) val leader_info = update_next_index leader_info p_id ((get_log_index log) + 1) val node = {node with log = log, leader_info = leader_info} - in - verbose_print (node.id^": Appending new message to log") node.verbose; - node + in node end (* Applies all committed log entries that have not already been applied *) @@ -615,9 +607,7 @@ fun leader_node node = ] in pause () end, - hn (SEND_HEARTBEAT, x) when nonce = x => - verbose_print (node.id^": Sending heartbeat") node.verbose; - leader_node node, + hn (SEND_HEARTBEAT, x) when nonce = x => leader_node node, hn (SEND_HEARTBEAT, x) => loop node, @@ -670,7 +660,6 @@ fun leader_node node = (* If another node has been elected as a candidate, and their term is in front of ours, convert to a follower *) hn (REQUEST_VOTE, (c_term, c_id, c_log_index, c_log_term)) when c_term > node.term => - verbose_print (node.id^": Voting yes") node.verbose; send(c_id, (YES_VOTE, node.id)); follower (demote c_term () c_id node), @@ -680,15 +669,11 @@ fun leader_node node = (* If we receive snapshot from a leader in a higher term, convert to follower *) - hn (SNAPSHOT, snapshot, l_id, other_term) when other_term > node.term => - verbose_print (node.id^": received Snapshot from leader, I must have lost position") node.verbose; - follower (demote other_term l_id () node), + hn (SNAPSHOT, snapshot, l_id, other_term) when other_term > node.term => follower (demote other_term l_id () node), (* If we receive AppendEntries from a leader in a higher term, convert to follower *) - hn (APPEND_ENTRIES, x, l_id, other_term, prevIndex, prevTerm, commitIndex) when other_term > node.term => - verbose_print (node.id^": received AppendEntries from leader, I must have lost position") node.verbose; - follower (demote other_term l_id () node), + hn (APPEND_ENTRIES, x, l_id, other_term, prevIndex, prevTerm, commitIndex) when other_term > node.term => follower (demote other_term l_id () node), (* Prints log *) hn DEBUG_PRINTLOG => @@ -702,9 +687,7 @@ fun leader_node node = val node = case snapshot.snapshot of () => node | _ => {node with log = apply_snapshot snapshot node.log} - in - verbose_print (node.id^": applying snapshot") node.verbose; - loop node end, + in loop node end, hn _ => loop node ] in @@ -733,7 +716,6 @@ and candidate node = fun won_election () = let val (sides, _, _) = node.state_machine in - verbose_print (node.id^": I won the election") node.verbose; send_sides node.log sides; leader_node ({ node with leader_info = (new_leader node.all_nodes node.log), @@ -763,7 +745,6 @@ and candidate node = up-to-date as ours. Because of this, we must have lost the election *) hn (APPEND_ENTRIES, x, l_id, other_term, prevIndex, prevTerm, commitIndex) when other_term >= node.term => - verbose_print (node.id^": received AppendEntries from leader, I must have lost position") node.verbose; follower ({ node with leader = l_id}), (* Election timeout, send out another request vote *) @@ -781,7 +762,6 @@ and candidate node = in if vote_amount >= req_votes then won_election () else loop () end in - verbose_print (node.id^": I am now a candidate") node.verbose; send_to_all node.all_nodes (REQUEST_VOTE, (node.term, p_id, latestLogIndex, prevLogTerm)) (p_id); start_random_timeout (fn () => send(p_id, (VOTE_TIMEOUT, nonce))); wait_for_votes ([node.id], 1) @@ -798,7 +778,6 @@ and follower node = { node with term = c_term, voted_for = c_id } fun loop node start_time = let fun start_election () = - verbose_print (node.id^": START ELECTION") node.verbose; candidate ({node with term = node.term + 1}) val _ = receive [ (* Starts an election *) @@ -808,7 +787,6 @@ and follower node = (* Sends a re-vote to a candidate we already voted for *) hn (REQUEST_VOTE, (c_term, c_id, c_log_index, c_log_term)) when c_id = node.voted_for => - verbose_print (node.id^": Voting yes") node.verbose; follower (vote_for c_id c_term node), (* If we receive a vote request, vote yes if: the log is a @@ -819,10 +797,8 @@ and follower node = val latestLogTerm = get_latest_log_term node.log fun no_vote () = send(c_id, NO_VOTE); - verbose_print (node.id^":voting no") node.verbose; follower node fun yes_vote () = - verbose_print (node.id^": Voting yes") node.verbose; follower (vote_for c_id c_term ({node with term = c_term})) in if latestLogIndex > c_log_index @@ -860,8 +836,8 @@ and follower node = state_machine = new_sm, log = newlog} - in (if accepting then verbose_print (node.id^": ACCEPTING SNAPSHOT") node.verbose; ack () - else verbose_print (node.id^": REJECTING SNAPSHOT") node.verbose; reject ()); + in (if accepting then ack () + else reject ()); loop node (getTime()) end, @@ -874,13 +850,9 @@ and follower node = if node.leader = () orelse node.term <= leader_term then l_id else node.leader} val accepting = - if leader_term < node.term then verbose_print "Old Leader" node.verbose; false - else if latestLogIndex > (get_log_index node.log) then - verbose_print "Leader is way ahead of me" node.verbose; - false - else if (get_latest_log_term node.log) <> prevLogTerm andalso prevLogTerm > 0 then - verbose_print "Leader has inconsistent log with me" node.verbose; - false + if leader_term < node.term then false + else if latestLogIndex > (get_log_index node.log) then false + else if (get_latest_log_term node.log) <> prevLogTerm andalso prevLogTerm > 0 then false else true val prev_commit = node.log.commitIndex val newlog = @@ -903,12 +875,8 @@ and follower node = else applied_log in - (if accepting then - verbose_print (node.id^": ACCEPTING") node.verbose; - ack () - else - verbose_print (node.id^": REJECTING") node.verbose; - reject ()); + (if accepting then ack () + else reject ()); loop {node with log = snapshot_log, state_machine = new_sm} (getTime()) end, @@ -943,7 +911,7 @@ and follower node = end (* END OF ./libs/nodes/follower.trp *) - (* A node is dormant until it has received the references of all other nodes. *) + (* A node is dormant until it has received the references of all other ([], WAIT, fn x => x ()), snapshot_cond = snapshot_cond, node_amount = node_amount, - serialkeys = [], - verbose = verbose + serialkeys = [] } in dormant_node node end (* Spawn a state-machine on a seperate thread, creates a record*) - fun initiate_node state_machine snapshot_cond node_amount id verbose = - spawn (fn () => default_node id [] node_amount state_machine snapshot_cond verbose) + fun initiate_node state_machine snapshot_cond node_amount id = + spawn (fn () => default_node id [] node_amount state_machine snapshot_cond) (* Sends a list of all nodes to all nodes *) fun add_refs nodes = map (fn x => send(x, (ADD_NODES, nodes))) nodes (* Spawn n nodes*) - fun initiate_nodes n state_machine snapshot_cond verbose = + fun initiate_nodes n state_machine snapshot_cond = let val part_init = initiate_node state_machine snapshot_cond n fun spawn_nodes n acc_id = case n of 0 => [] | x => append (spawn_nodes (x - 1) (acc_id ^ "I")) - [(part_init acc_id verbose)] + [(part_init acc_id)] val nodes = spawn_nodes n "I" in @@ -1000,16 +967,16 @@ end end (* Spawn a state-machine on some alias *) - fun initiate_distributed_node state_machine snapshot_cond node_amount id alias verbose = - spawn(alias, fn () => (default_node id [] node_amount state_machine snapshot_cond verbose)) + fun initiate_distributed_node state_machine snapshot_cond node_amount id alias = + spawn(alias, fn () => (default_node id [] node_amount state_machine snapshot_cond)) - fun initiate_distributed_nodes aliases state_machine snapshot_cond verbose = + fun initiate_distributed_nodes aliases state_machine snapshot_cond = let val part_init = initiate_distributed_node state_machine snapshot_cond (length(aliases)) fun spawn_nodes acc acc_id = case acc of [] => [] | h :: t => - append (spawn_nodes t (acc_id ^ "I")) [part_init acc_id h verbose] + append (spawn_nodes t (acc_id ^ "I")) [part_init acc_id h] val nodes = spawn_nodes aliases "I" in add_refs nodes; @@ -1022,32 +989,16 @@ end (* Spawns a distributed Raft network, which can be dialed into to communicate with their state-machines *) - fun raft_spawn_alias (state_machine, aliases, snapshot_cond, verbose) = - initiate_distributed_nodes aliases state_machine snapshot_cond verbose + fun raft_spawn_alias (state_machine, aliases, snapshot_cond) = + initiate_distributed_nodes aliases state_machine snapshot_cond | raft_spawn_alias (state_machine, aliases) = - raft_spawn_alias (state_machine, aliases, 50, false) + raft_spawn_alias (state_machine, aliases, 50) (* Spawns a Raft network, which can be contacted to communicate with their state-machines *) - fun raft_spawn (state_machine, n, snapshot_cond, verbose) = - initiate_nodes n state_machine snapshot_cond verbose - | raft_spawn (state_machine) = raft_spawn (state_machine, 5, 50, false) - - (*fun raft_d (state_machine, client_id, aliases, snapshot_cond, verbose) = - let val nodes = raft_spawn_alias (state_machine, aliases, snapshot_cond, verbose) - in (raft_dial (nodes, client_id), nodes) - end - | raft_d (state_machine, client_id, aliases) = - raft_d (state_machine, client_id, aliases, 50, false) - - fun raft (state_machine, client_id, n, snapshot_cond, verbose) = - let val nodes = raft_spawn (state_machine, n, snapshot_cond, verbose) - in (raft_dial (nodes, client_id), nodes) - end - | raft (state_machine, client_id) = - raft (state_machine, client_id, 5, 50, false) - - val default_aliases = ["@node1", "@node2", "@node3", "@node4", "@node5"]*) + fun raft_spawn (state_machine, n, snapshot_cond) = + initiate_nodes n state_machine snapshot_cond + | raft_spawn (state_machine, n) = raft_spawn (state_machine, n, 50) in [ ("raft_dial", raft_dial) From 7e68d9eaac3aaae9aa05e9d8e312a932ffdf810b Mon Sep 17 00:00:00 2001 From: Victor Ask Justesen Date: Wed, 29 May 2024 15:08:19 +0200 Subject: [PATCH 5/8] removed contains --- examples/network/aliases.json | 1 + examples/network/bug.zip | Bin 2096 -> 0 bytes examples/network/ids/pingpong-dialer.json | 1 + examples/network/ids/pingpong-listener.json | 1 + examples/network/pingpong/aliases.json | 2 +- .../network/pingpong/ids/pingpong-dialer.json | 2 +- .../pingpong/ids/pingpong-listener.json | 2 +- lib/lists.trp | 8 - lib/out/lists.exports | 1 - lib/raft_troupe.trp | 1014 ----------------- 10 files changed, 6 insertions(+), 1026 deletions(-) create mode 100644 examples/network/aliases.json delete mode 100644 examples/network/bug.zip create mode 100644 examples/network/ids/pingpong-dialer.json create mode 100644 examples/network/ids/pingpong-listener.json delete mode 100644 lib/raft_troupe.trp diff --git a/examples/network/aliases.json b/examples/network/aliases.json new file mode 100644 index 0000000..9f20c3f --- /dev/null +++ b/examples/network/aliases.json @@ -0,0 +1 @@ +{"pingpong-listener":"12D3KooWK5UgRtP27Jdujm79Pq3QLJ5Gttg2PucDF8Hppd7txJiA","pingpong-dialer":"12D3KooWQ6DqK22D98Ro4WopLhsGPkUyb2d3EvckDrvSYta4E3Y6"} \ No newline at end of file diff --git a/examples/network/bug.zip b/examples/network/bug.zip deleted file mode 100644 index 62576039911cdd6110e8e906f359b30bd8ef8e87..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2096 zcmWIWW@h1H0D+B*)%@b8hYfhz&Ogyq+IYAsL%Y!B z)IuR^pJb-BPZ@U|Wxi~(QfRuT!adpT*;|#k=Bzm~|NnpgJ$KisX>tFgbX)Y5Q`CaVpF@S>S}NGz z3pF<$Xxc9t-ks|2zA1k~eu~WEuocc;0vd9G*Gi{V+nGE)sr5x3=rmp^emPkr9ddF!Z;-+kSE44@Fh zo+db;F`k)HjFc`wDaZ4Xdh#it>?0tSLQ(}v8@f4}#U-hEsYQBO#rb)zXAk-{8!#|j zsDCNSFJ_gZCYNxaAuOhq_fD^)^@6rAAM1{f_peU2|8P%S!s-LZ0$HnwQ(apEXT(TH zr{*NiuwEfh811re)udeM0y8lYK4rJGr~D82R#Y0ySiAqgxqdFG+|H%>)f-tnQ%^bH zH{bVU!^%X@b1P3AfBl?$+06GRiY65Xrmxuk^^4%`CcS)@C%;VBy_kO^9#ny z4;%G=dZ~YD`hWGK2S3)PW}0hne;@KC=B?R7*MJvKk7&Gzs=ijpgM=##Yk>g0eu&r%^*B(YK!9!+dE-${k ze>&N|;648ez6YE)3ch-@+-P4Jq+$H8VZk(?DN72U2i|)6_(^?TPPMM^@~;JvJUmw) zFrHYqYr^9n{1tkA<}*}Qo7T9Sw!F)DI%E2@*bAPsj~eWL_2z(o+I`*K-xfV>Db<%L z&UqwTc>Kid(3t`8hcx3N6AtQT&8@65y0WhP$4{}ZKmT&xm>P66Q|Wba%%-a3CO5%} zh5w)4=00m#v$_eFxslBLZL&FaQ+q-~Pf3^f)kg!Hb5f)S`S)(O}E~PF0X{ z1YFW|X=^qB74`zL01$&pn#7#U#NyOqa4K0Hrsb)tck%2=9X-#p-nv1Xf{aXx48NT_ z&;Q-Ctn{OYzlqidueBau{fx~x&y{)em6hsneBa*Da0=2nh^Io$8+Ak&#*G*y1^Fb9PnH&h@U_ zf^>ndWn_|Pz@7_O7*v2R1_6-EK>*}>E(QfKn~_0+f%&6-ulBNAjLd325E{ydn2M`3 zg_+s_G)D=DL8ijWR%Fv#7X4fXGF?UUH<*U;kxj=e>yS+c~$Oaf6+1;3B39`GbkW9ubS&;2& z&QU}Oc^DtvF4SC$Y?lo%EyH{PwhJi_BijV?1uV{Cd}Nz2vj?(Gwk!~vV3`Hk^wa)T z*`Tn>zE%yUVSHrMG4m6$=~+ false - | h :: t => - if h = element then true - else contains element t - fun filter f l = case l of [] => [] | h :: t => if f h then h :: (filter f t) else filter f t @@ -97,7 +90,6 @@ in , ("append", append) , ("partition", partition) , ("nth", nth) - , ("contains", contains) , ("filter", filter) , ("first", first) , ("slice", slice) diff --git a/lib/out/lists.exports b/lib/out/lists.exports index cf5a5c3..db2a6ba 100644 --- a/lib/out/lists.exports +++ b/lib/out/lists.exports @@ -9,7 +9,6 @@ length append partition nth -contains filter first slice diff --git a/lib/raft_troupe.trp b/lib/raft_troupe.trp deleted file mode 100644 index 81a16b1..0000000 --- a/lib/raft_troupe.trp +++ /dev/null @@ -1,1014 +0,0 @@ -import lists - -(* - Log = { - snapshot: Snapshot - log: Entry[], - lastApplied: int, - internalChanges: int, - commitIndex: int, - latestSerials: SerialKey[] - } - Snapshot = { - snapshot: Some state - lastIncludedIndex: int, - lastIncludedTerm: int - } - Entry = { - term: int, - command: message, - serial: string - } - SerialKey = { - id: clusterId[] | pid, - key: (logIndex, number) | nonce - } -*) - -(* - LeaderInfo = { - nextIndex = { - peer: p, - next: int - }[], - matchIndex = { - peer: p, - match: int - }[] - } -*) - -(* - StateMachine = { - set_hook : fn (x: string) => x - get_hook : fn (x: string, callback_pid: string) => x - get_snapshot_hook : fn(callback_pid: string) => x - get_changes_hook : fn (callback_pid: string) => x - snapshot_condition_hook : fn (log_summary: LogSummary, callback_pid: string) => x: bool - } - LogSummary = { - log_size: int, - entries_since_snap: int - } -*) - -(* - Node = { - all_nodes: string[], - id: string, - log: Log, - term: int, - voted_for: string, - leader: string, - leader_info: LeaderInfo, - snapshot_condition: fn logSummary => ... : boolean - state_machine: ([SIDE-EFFECTS], STATUS, STEP-FUNC) - total_nodes: int, - verbose: boolean - } -*) - -(* - RaftProcesses = { - type: Client | Cluster, - id: pid | Clusterid[] - } -*) - -datatype Atoms = - WAIT | SUS | DONE - | SEND_HEARTBEAT - | RAFT_UPDATE - | NOT_LEADER - | ACKNOWLEDGE - | REJECT - | ELECTION_TIMEOUT - | REQUEST_VOTE | YES_VOTE | NO_VOTE | VOTE_TIMEOUT - | APPEND_ENTRIES | SNAPSHOT - | ADD_NODES - | DIAL - - | DIALER_ACK | DIALER_SM_BUSY | DIALER_SM_DONE | DIALER_CLIENT_MSG - | DIALER_MESSAGE_TIMEOUT | DIALER_BUSY_TIMEOUT - - | SEND_TO_NTH | SEND_TO_ALL - - | DEBUG_PRINTLOG | DEBUG_PAUSE | DEBUG_CONTINUE | DEBUG_APPLYSNAPSHOT | DEBUG_SNAPSHOT_COND | DEBUG_TIMEOUT - - | FUNCTION_DONE - - | ERROR_TIMEOUT - | CLUSTER | CLIENT - -let - (* Constants *) - val LOCAL_ERROR_TIMEOUT = 4000 - val ELECTION_TIMEOUT_LOWER = 2000 - val ELECTION_TIMEOUT_UPPER = 4000 - val HEARTBEAT_INTERVAL = 500 - - val DIALER_NOLEADER_TIMEOUT = 500 - val DIALER_NOMSG_TIMEOUT = 2000 - val DIALER_SM_BUSY_TIMEOUT = 1000 - - fun not a = a = false - fun send_to_all processes msg sender = map (fn x => send(x, msg)) (filter (fn x => x <> sender) processes) - - fun send_to_nth processes msg n = send((nth (reverse processes) n), msg) - - fun max a b = if a < b then b else a - - fun min a b = if a > b then b else a - - (* #IMPORT libs/quickselect.trp *) - fun is_even i = i mod 2 = 0 - - (* Using QuickSelect, finds the kth element of a list. *) - fun quickselect list k = - case list of - [] => "ERROR: Empty list" - | h :: t => - let val (ys, zs) = partition (fn x => x > h) t - val l = length ys - in - if k < l then quickselect ys k - else if k > l then quickselect zs (k-l-1) - else h - end - - (* Returns the median of a list. *) - fun median list = - let val len = length list - val middle = if is_even len then len / 2 - 1 else (len - 1) / 2 - in quickselect list (middle) - end - (* END OF libs/quickselect.trp *) - - (* #IMPORT libs/log.trp *) - (* Creates a snapshot. *) - fun set_snapshot snapshot index term = { - snapshot = snapshot, - lastIncludedIndex = index, - lastIncludedTerm = term - } - - (* Creates a default, empty snapshot. *) - val empty_snapshot = { - snapshot = (), - lastIncludedIndex = 0, - lastIncludedTerm = 0 - } - - (* A default, empty log. *) - val empty_log = { - log = [], - snapshot = empty_snapshot, - lastApplied = 0, - commitIndex = 0, - lastMessageSerial = "" - } - - fun pretty_print_log id log = - (* Disabled for library *) - (* printString "\n========******========"; - print (length log.log); - printString ("ID: "^id); - printString "----------------------"; - printString "Entries (term, message):"; - map (fn x => print (x.term, x.command)) log.log; - printString "----------------------"; - printString "CommitIndex:"; - print log.commitIndex; - printString "LastApplied:"; - print log.lastApplied; - printString "----------------------"; - printString "Snapshot:"; - print log.snapshot; - printString "========******========\n";*) - () - - (* Appends a message to the log, and notes the message's serial number. *) - fun append_message log message callback term serial = - let val new_entry = { - term = term, - command = message, - callback = callback, - serial = serial - } - in { - log with - lastMessageSerial = serial, - log = new_entry :: log.log - } - end - - - (* Appends a list of message to the log. *) - fun add_entries_to_log log entries term = - case entries of - [] => log - | h :: t => - add_entries_to_log (append_message log h.command h.callback term h.serial) t - h.term - - (* Updates the lastApplied-index. *) - fun update_applied log = { - log with - lastApplied = log.lastApplied + 1 - } - - (* Commits a message in the log. *) - fun update_commit log new_index = { - log with - commitIndex = (max new_index log.commitIndex) - } - - (* Rolls the log back one entry. *) - fun rollback_log log = - let val loglog = log.log - in case loglog of - (_ :: prev_log) => { - log with - log = prev_log - } - | [] => {log with log = []} - end - - (* Get the entry of the latest log entry. *) - fun get_log_index log = (length log.log) + log.snapshot.lastIncludedIndex - - (*Determines whether or not all log changes have been committed. *) - fun log_is_committed log = (get_log_index log = log.commitIndex) - - (* Rolls the log back n time. *) - fun rollback_log_to log n = - if n < (get_log_index log) then - let val log = rollback_log log - in (rollback_log_to log n) - end - else log - - (* Get the term of the latest entry of the log, or, if empty, the last - included index of the snapshot. *) - fun get_latest_entry_term log = - case log.log of - [] => log.snapshot.lastIncludedTerm - | h :: _ => h.term - - (* Get the term of the latest log entry. *) - fun get_latest_log_term log = get_latest_entry_term log - - (* Get the message of the latest log entry. *) - fun get_latest_log_command log = - case log.log of - [] => 0 (* Should not be reachable. *) - | h :: _ => h.command - - fun get_nth_command log index = nth (reverse log.log) (index - log.snapshot.lastIncludedIndex) - - (* Returns a slice of all entries after log-index n. *) - fun get_commands_after_nth entries n last_included = - let val log_slice = slice (n - last_included) (length entries) (reverse entries) - in log_slice - end - - (* Get a snapshot of all committed entries. *) - fun get_snapshot state log = - if log.commitIndex > 0 andalso - (log.commitIndex - log.snapshot.lastIncludedIndex) <= length log.log then - let val lastCommitted = get_nth_command log log.commitIndex - in set_snapshot state log.commitIndex lastCommitted.term end - else empty_snapshot - - - (* Applies a snapshot to the log. *) - fun apply_snapshot snapshot log = - let val newCommitIndex = - if log.commitIndex < snapshot.lastIncludedIndex then snapshot.lastIncludedIndex - else log.commitIndex - val uncommitted_entries = get_commands_after_nth log.log newCommitIndex log.snapshot.lastIncludedIndex - val newLastApplied = - if log.lastApplied < snapshot.lastIncludedIndex then snapshot.lastIncludedIndex - else log.lastApplied - in { log with - log = uncommitted_entries, - commitIndex = newCommitIndex, - lastApplied = newLastApplied, - snapshot = snapshot } - end - - (* Asks the state-machine whether or not to snapshot. *) - fun evaluate_snapshot_cond state snapshot_cond log = - if (log.lastApplied - log.snapshot.lastIncludedIndex) > snapshot_cond then - apply_snapshot (get_snapshot state log) log - else log - (* END OF libs/log.trp *) - - (* #IMPORT libs/leader-info.trp *) - - (* Generates a default leader info, with the nextIndex of all followers - being the nextIndex of the new leader. This can be changed with followers - rejecting AppendEntries *) - fun new_leader all_nodes log = - let val nextIndex = get_log_index log - val index = map (fn id => {peer = id, next = nextIndex + 1}) all_nodes - val match_index = map (fn id => {peer = id, match = 0}) all_nodes - in { - nextIndex = index, - matchIndex = match_index - } end - - (* Get the nextIndex of a peer *) - fun get_next_index leader_info peer = first (filter (fn (x) => x.peer = peer) leader_info.nextIndex) - - (* Get the matchIndex of a peer *) - fun get_match_index leader_info peer = first (filter (fn (x) => x.peer = peer) leader_info.matchIndex) - - (* Updates a cluster member's next-index. This is done after an - acknowledgement or rejection. *) - fun update_next_index leader_info peer new = let - val prevIndex = get_next_index leader_info peer - val newIndex = {peer = peer, next = new} - val withoutPeer = filter (fn (x) => x.peer <> peer) leader_info.nextIndex - in { - leader_info with - nextIndex = newIndex :: withoutPeer - } end - - (* Updates a cluster member's match-index, denoting how much of their log - matches the leader holding the leader info. *) - fun update_match_index leader_info peer new = let - val prevIndex = get_match_index leader_info peer - val newIndex = {peer = peer, match = new} - val withoutPeer = filter (fn (x) => x.peer <> peer) leader_info.matchIndex - in { - leader_info with - matchIndex = newIndex :: withoutPeer - } end - - (* Get all follower's matchIndex*) - fun get_matches leader_info = map (fn x => x.match) leader_info.matchIndex - - (* Get the highest index of entries that a majority of followers have - appended to, by finding the median *) - fun calc_highest_commit matches = median matches - (* END OF libs/leader-info.trp *) - - (* Executes a function after a given timeout. *) - fun start_timeout func duration = - let fun timeout () = - let val time = duration - val _ = sleep time - in func () - end - val p_id = self() - in spawn timeout - end - - (* Send message after a delay. *) - fun send_delay (to, m) delay = - sleep delay; - send (to, m) - - (* Starts a random timeout with lower=2sec and upper=4sec *) - fun start_random_timeout func = start_timeout func (ELECTION_TIMEOUT_LOWER + ((random ()) * (ELECTION_TIMEOUT_UPPER - ELECTION_TIMEOUT_LOWER))) - - (* #IMPORT ./libs/dialer.trp *) - -(* Selects a random element from a list *) -fun random_element list = - let fun roundUp n m = - if n <= 0 then m else roundUp (n - 1) (m + 1) - val r_n = roundUp (random() * (length list - 1)) 0 - in nth list r_n -end - - -(* Given a list of serialkeys, and a serial key, check if it is valid, and -return a list containing the serial key if so, and a boolean denoting whether or -not it is valid. *) -fun apply_serialkey list key = - case list of - [] => (true, []) - | h :: t => - if h = key then - case (h.key, key.key) of - ((log_index, seq_numb), (new_log_index, new_seq_numb)) => - if new_log_index > log_index orelse - (log_index = new_log_index andalso new_seq_numb > seq_numb) then - (true, ({ h with key = key.key } :: t)) - else (false, h :: t) - | (nonce, new_nonce) => - if nonce <> new_nonce then - (true, ({ h with key = key.key } :: t)) - else (false, h :: t) - | _ => (true, ({ h with key = key.key } :: t)) - else - let val (cond, list) = apply_serialkey t key - in (cond, h :: list) - end - - -(* Used by the dialer to send message to a cluster. If the nodes are busy or if -no leader is present, this function re-sends the message until it is eventually -delivered and acknowledged by the leader of the cluster. If leader is unknown, -it can be defined as unit.*) -fun dialer_send_message p_id msg serial_n leader cluster = - let val nonce = mkuuid() - val msg_timeout = start_timeout (fn() => send(p_id, (DIALER_MESSAGE_TIMEOUT, nonce))) - val busy_timeout = start_timeout (fn() => send(p_id, (DIALER_BUSY_TIMEOUT, nonce))) - fun wait () = - receive [ - hn (NOT_LEADER, leader_id) => - dialer_send_message p_id msg serial_n leader_id cluster, - hn (DIALER_ACK, other_serial) when other_serial = serial_n => - leader, - hn (DIALER_SM_BUSY, other_serial) when other_serial = serial_n => - busy_timeout DIALER_SM_BUSY_TIMEOUT; - wait (), - hn (DIALER_SM_DONE, other_serial) when other_serial = serial_n => - leader, - hn (DIALER_MESSAGE_TIMEOUT, x) => - if x = nonce then dialer_send_message p_id msg serial_n (random_element cluster) cluster - else wait (), - hn (DIALER_BUSY_TIMEOUT, x) => - if x = nonce then dialer_send_message p_id msg serial_n leader cluster - else wait () - ] - in (case leader of - () => msg_timeout DIALER_NOLEADER_TIMEOUT - | x => - msg_timeout DIALER_NOMSG_TIMEOUT; - send(x, msg)); - wait () -end - -(* Facilitates client-side interaction to the Raft cluster. Allows the -programmer to send messages to the cluster in the format (RAFT_UPDATE, msg)*) -fun dialer cluster client_id = - let val p_id = self() - fun update_message x leader = let - val serial_n = mkuuid() - in dialer_send_message p_id ((RAFT_UPDATE, x), p_id, serial_n) serial_n leader cluster - end - val leader = random_element cluster - - fun loop leader sks = - receive [ - hn (RAFT_UPDATE, x) => - loop (update_message x leader) sks, - - hn (DIALER_CLIENT_MSG, msg, sk) => - let val (cond, sks) = apply_serialkey sks sk - in - (if cond then send(client_id, msg) - else ()); - loop leader sks - end, - - hn (SEND_TO_NTH, n, x) => - send_to_nth cluster x n; - loop leader sks, - - hn (SEND_TO_ALL, x) => - send_to_all cluster x (self()); - loop leader sks, - hn _ => loop leader sks ] - in loop leader [] -end - -(* Temporary dialer sends a list of messages to a cluster before terminating. *) -fun leader_dialer cluster msgs = - let val p_id = self() - val leader = random_element cluster - in - map (fn (msg, serial) => dialer_send_message p_id ((RAFT_UPDATE, msg), p_id, serial) serial leader cluster) msgs -end - -(* Send-function used for clusters to send a message to either the dialer or -client. *) -fun raft_send (process, msgs) = case process.type of -CLIENT => map (fn (msg, sk) => send(process.id, (DIALER_CLIENT_MSG, msg, sk))) msgs -| CLUSTER => spawn (fn () => leader_dialer process.id msgs) -(* END OF ./libs/dialer.trp *) - - (* Send the side-effect-messages to dialers or clusters *) - fun send_sides log sides = - (* Add message to key-value-store, sorting by the recipients. *) - let fun add_msg id msg sk dict = case dict of - [] => [(id, [(msg, sk)])] - | (other_id, msgs) :: t => - if id = other_id then - (id, (msg, sk) :: t) - else (other_id, msgs) :: add_msg id msg sk t - (* Generate key-value-store of all message, sorting by recipients. *) - val (sorted_msgs, _) = case sides of - [] => ([], 0) - | x => foldl (fn ((callback, msg), (acc, seq)) => - (add_msg callback msg ({ id = callback, key = (log.lastApplied, seq)}) acc, seq + 1) - ) ([], 1) x - (* Sends all messages. *) - in map (fn x => raft_send x) sorted_msgs - end - - (* Applies all log-entries that have been committed, but not applied *) - fun apply_log log state_machine is_leader = - (* If any non-applied, committed logs apply... *) - if log.lastApplied < log.commitIndex then - (* Get the latest non-applied committed entry *) - let val entry = get_nth_command log (log.lastApplied + 1) - val command = entry.command - (* Update log to apply entry and apply entry on state-machine*) - val log = update_applied log - val (sides, status, step) = state_machine - val (new_sides, new_status, new_step) = step command - (* If leader is applying, execute side-effects. *) - in (if is_leader then - entry.callback (); - send_sides log new_sides - else ()); - apply_log log (new_sides, new_status, new_step) is_leader end - else (log, state_machine) - - (* #IMPORT ./libs/nodes/leader.trp *) -fun leader_node node = - let val p_id = self() - (* Appends appends all entries from a follower's nextIndex to the leader's log index*) - fun append_entries node follower_pid = - let val nextIndex = get_next_index node.leader_info follower_pid - val logIndex = get_log_index node.log - in if logIndex + 1 >= nextIndex.next then - let - val latestLogIndex = nextIndex.next - 1 - in - (* Sends the snapshot if the followers nextIndex is before the Snapshot's lastIncludedIndex *) - if nextIndex.next <= node.log.snapshot.lastIncludedIndex - then send(follower_pid, (SNAPSHOT, node.log.snapshot, p_id, node.term)) - else - let val entries = get_commands_after_nth node.log.log latestLogIndex node.log.snapshot.lastIncludedIndex - val afterSnapshot = latestLogIndex - node.log.snapshot.lastIncludedIndex - val prevEntryTerm = - if afterSnapshot > 0 then (get_nth_command node.log latestLogIndex).term - else node.log.snapshot.lastIncludedTerm - in send(follower_pid, (APPEND_ENTRIES, entries, p_id, node.term, latestLogIndex, prevEntryTerm, node.log.commitIndex)) - end - end - (* A follower should never get more entries than the leader *) - else () - end - - (* Convert leader to follower *) - fun demote term leader voted_for node = - {node with - term = term, - leader = leader, - leader_info = (), - voted_for = voted_for} - - fun append_update node msg callback serial = - let val latestLogIndex = get_log_index node.log - val prevLogTerm = get_latest_log_term node.log - val log = append_message node.log msg callback node.term serial - val leader_info = update_match_index node.leader_info p_id (get_log_index log) - val leader_info = update_next_index leader_info p_id ((get_log_index log) + 1) - val node = {node with log = log, leader_info = leader_info} - in node - end - - (* Applies all committed log entries that have not already been applied *) - fun apply_committed node = - let val prev_commit = node.log.commitIndex - val highest_commit = calc_highest_commit (map (fn x => x.match) node.leader_info.matchIndex) - val node = { node with log = update_commit node.log highest_commit } - val (applied_log, new_sm) = apply_log node.log node.state_machine true - val snapshot_log = - if prev_commit < highest_commit then - evaluate_snapshot_cond new_sm node.snapshot_cond applied_log - else - applied_log - val (_, status, _) = new_sm - val node = { node with log = snapshot_log, state_machine = new_sm} - in - case status of - SUS => if log_is_committed node.log then append_update node () (fn () => ()) (mkuuid()) - else node - | _ => node - end - - val nonce = mkuuid () - - fun loop node = - receive [ - (* Halts the leader *) - hn DEBUG_PAUSE => - let fun pause () = receive [ - hn (DEBUG_CONTINUE) => loop node, - hn x => pause () - ] - in pause () end, - - hn (SEND_HEARTBEAT, x) when nonce = x => leader_node node, - - hn (SEND_HEARTBEAT, x) => - loop node, - - (* Message has not been appended before *) - hn ((RAFT_UPDATE, x), dialer_id, serial_n) => - let val (cond, sks) = apply_serialkey node.serialkeys serial_n - in if cond then let - val (_, stat, _) = node.state_machine - val node = case stat of - SUS => send(dialer_id, (DIALER_SM_BUSY, serial_n)); node - | DONE => send(dialer_id, (DIALER_SM_DONE, serial_n)); node - | WAIT => - if log_is_committed node.log then - let fun replication_cb () = send (dialer_id, (DIALER_ACK, serial_n)) - in append_update node x replication_cb serial_n end - else send(dialer_id, (DIALER_SM_BUSY, serial_n)); node - in leader_node node end - else send(dialer_id, (DIALER_ACK, serial_n)); - loop node - end, - - (* If append is successful on a follower*) - hn (ACKNOWLEDGE, (peer, logIndex)) => - let val prev_index = get_log_index node.log - val node = { node with leader_info = update_match_index node.leader_info peer logIndex } - val node = { node with leader_info = update_next_index node.leader_info peer (logIndex + 1) } - val node = apply_committed node - val next_index = get_next_index node.leader_info peer - in (if prev_index < get_log_index node.log then - map (fn x => append_entries node x) - (filter (fn x => - let val next_index = get_next_index node.leader_info x - in x <> p_id andalso next_index.next > logIndex end) node.all_nodes) - else if next_index.next <= get_log_index node.log then - append_entries node peer - else ()); - loop node - end, - - (* If append is unsuccessful *) - hn (REJECT, (peer, terminfo, logIndex)) => - if node.term >= terminfo.term then - let val node = { node with leader_info = update_next_index node.leader_info peer (logIndex + 1) } - in loop node - end - else follower (demote terminfo.term terminfo.leader () - node), - - (* If another node has been elected as a candidate, and - their term is in front of ours, convert to a follower *) - hn (REQUEST_VOTE, (c_term, c_id, c_log_index, c_log_term)) when c_term > node.term => - send(c_id, (YES_VOTE, node.id)); - follower (demote c_term () c_id node), - - hn (REQUEST_VOTE, (c_term, c_id, c_log_index, c_log_term)) => - send(c_id, (NO_VOTE, node.id)); - loop node, - - (* If we receive snapshot from a leader in a higher term, - convert to follower *) - hn (SNAPSHOT, snapshot, l_id, other_term) when other_term > node.term => follower (demote other_term l_id () node), - - (* If we receive AppendEntries from a leader in a higher term, - convert to follower *) - hn (APPEND_ENTRIES, x, l_id, other_term, prevIndex, prevTerm, commitIndex) when other_term > node.term => follower (demote other_term l_id () node), - - (* Prints log *) - hn DEBUG_PRINTLOG => - pretty_print_log node.id node.log; - loop node, - - (* Applies a snapshot *) - hn DEBUG_APPLYSNAPSHOT => - let - val snapshot = get_snapshot node.state_machine node.log - val node = case snapshot.snapshot of - () => node - | _ => {node with log = apply_snapshot snapshot node.log} - in loop node end, - hn _ => loop node - ] - in - (* Append entries for each follower *) - map (fn x => append_entries node x) (filter (fn x => x <> p_id) node.all_nodes); - start_timeout (fn () => send (p_id, (SEND_HEARTBEAT, nonce))) HEARTBEAT_INTERVAL; - loop node -end -(* END OF ./libs/nodes/leader.trp *) - (* #IMPORT ./libs/nodes/candidate.trp *) -and candidate node = - let val p_id = self() - - (* A candidate cannot vote for anyone and has no leader *) - val node = {node with voted_for = (), leader = ()} - val nonce = mkuuid() - - (* Sends a vote request to all followers *) - val latestLogIndex = get_log_index node.log - val prevLogTerm = get_latest_log_term node.log - - - (* Becoming a leader requires majority vote *) - val req_votes = ((length node.all_nodes) / 2) - - fun won_election () = - let val (sides, _, _) = node.state_machine - in - send_sides node.log sides; - leader_node ({ - node with leader_info = (new_leader node.all_nodes node.log), - leader = (p_id)}) - end - - fun wait_for_votes (follower_votes, vote_amount) = - let - fun loop () = receive [ - (* Received a vote from a follower we have not already - received a vote from *) - hn (YES_VOTE, follower_id) when (not (contains follower_id follower_votes)) => - wait_for_votes ((append follower_votes [follower_id]), vote_amount + 1), - - (*We received a NO_VOTE from a follower in a later term. - This can only happen if there is a leader/candidate in this - term, and as such, we convert to a follower *) - hn (NO_VOTE, other_term) when other_term > node.term => - follower node, - - (* Received vote request from candidate in later term *) - hn (REQUEST_VOTE, (c_term, other_c_id, c_log_index, c_log_term)) when c_term > node.term => - send(other_c_id, (YES_VOTE, node.id)); - follower ({ node with term = c_term, voted_for = other_c_id}), - - (* Received message from leader in a term at least as - up-to-date as ours. Because of this, we must have lost the - election *) - hn (APPEND_ENTRIES, x, l_id, other_term, prevIndex, prevTerm, commitIndex) when other_term >= node.term => - follower ({ node with leader = l_id}), - - (* Election timeout, send out another request vote *) - hn (VOTE_TIMEOUT, x) when x = nonce => candidate {node with term = node.term + 1}, - - (* Halts the candidate *) - hn (DEBUG_PAUSE) => - let fun loop () = receive [ - hn (DEBUG_CONTINUE) => (), - hn x => loop () - ] - in loop () end, - hn _ => loop () - ] - in if vote_amount >= req_votes then won_election () else loop () - end - in - send_to_all node.all_nodes (REQUEST_VOTE, (node.term, p_id, latestLogIndex, prevLogTerm)) (p_id); - start_random_timeout (fn () => send(p_id, (VOTE_TIMEOUT, nonce))); - wait_for_votes ([node.id], 1) -end -(* END OF ./libs/nodes/candidate.trp *) - (* #IMPORT ./libs/nodes/follower.trp *) -and follower node = - let val nonce = mkuuid() - val p_id = self() - val _ = start_random_timeout (fn () => send(p_id, (ELECTION_TIMEOUT, nonce))) - (* Sends a YES_VOTE to a candidate *) - fun vote_for c_id c_term node = - send(c_id, (YES_VOTE, node.id)); - { node with term = c_term, voted_for = c_id } - fun loop node start_time = let - fun start_election () = - candidate ({node with term = node.term + 1}) - val _ = receive [ - (* Starts an election *) - hn (ELECTION_TIMEOUT, x) when x = nonce => - if (getTime() - start_time >= ELECTION_TIMEOUT_LOWER) then start_election () - else start_random_timeout (fn () => send(p_id, (ELECTION_TIMEOUT, nonce))); loop node (getTime()), - - (* Sends a re-vote to a candidate we already voted for *) - hn (REQUEST_VOTE, (c_term, c_id, c_log_index, c_log_term)) when c_id = node.voted_for => - follower (vote_for c_id c_term node), - - (* If we receive a vote request, vote yes if: the log is a - up-to-date and the term of the candidate is later than our - current. Vote no otherwise *) - hn (REQUEST_VOTE, (c_term, c_id, c_log_index, c_log_term)) => - let val latestLogIndex = get_log_index node.log - val latestLogTerm = get_latest_log_term node.log - fun no_vote () = - send(c_id, NO_VOTE); - follower node - fun yes_vote () = - follower (vote_for c_id c_term ({node with term = c_term})) - in - if latestLogIndex > c_log_index - orelse latestLogTerm <> c_log_term - orelse c_term <= node.term then no_vote () - else yes_vote () - end, - - (* When receiving a snapshot from a leader in a later or - same term, acknowledge if it contains entries past our - current log index. Update leader and term accordingly. *) - hn (SNAPSHOT, x, l_id, leader_term) => - let val node = {node with leader = - if node.leader = () orelse node.term < leader_term then l_id - else node.leader} - - val {snapshot, lastIncludedIndex, lastIncludedTerm} = x - val log_term = get_latest_log_term node.log - val log_index = get_log_index node.log - - val accepting = - if leader_term < node.term then false - else if lastIncludedIndex <= log_index then false - else true - - val newlog = if accepting then apply_snapshot x node.log else node.log - val new_sm = if accepting then snapshot else node.state_machine - val reject = - fn () => send (l_id, (REJECT, (p_id, {term = node.term, leader = node.leader}, (get_log_index newlog)))) - val ack = - fn () => send (l_id, (ACKNOWLEDGE, (p_id, get_log_index newlog))) - - val node = { - node with term = (if node.term < leader_term then leader_term else node.term), - state_machine = new_sm, - log = newlog} - - in (if accepting then ack () - else reject ()); - loop node (getTime()) - end, - - (* When receiving entries from a leader in a later or - same term, acknowledge if it contains entries past our - current log index. And if the latest log index matches ours. - Update log accordingly.*) - hn (APPEND_ENTRIES, x, l_id, leader_term, latestLogIndex, prevLogTerm, leaderCommit) => - let val node = {node with leader = - if node.leader = () orelse node.term <= leader_term then l_id - else node.leader} - val accepting = - if leader_term < node.term then false - else if latestLogIndex > (get_log_index node.log) then false - else if (get_latest_log_term node.log) <> prevLogTerm andalso prevLogTerm > 0 then false - else true - val prev_commit = node.log.commitIndex - val newlog = - if accepting then - let val log = rollback_log_to node.log latestLogIndex - val log = add_entries_to_log log x leader_term - in update_commit log (min leaderCommit (get_log_index log)) - end - else node.log - val reject = - fn () => send (l_id, (REJECT, (p_id, {term = node.term, leader = node.leader}, (get_log_index newlog)))) - val ack = - fn () => send (l_id, (ACKNOWLEDGE, (p_id, get_log_index newlog))) - - val node = {node with term = (if node.term < leader_term then leader_term else node.term)} - val (applied_log, new_sm) = apply_log newlog node.state_machine false - val snapshot_log = - if prev_commit < applied_log.commitIndex then - evaluate_snapshot_cond new_sm node.snapshot_cond applied_log - else - applied_log - in - (if accepting then ack () - else reject ()); - loop {node with log = snapshot_log, state_machine = new_sm} (getTime()) - end, - - (* If client sends update, sends the leader's id *) - hn ((RAFT_UPDATE, x), dialer_id, _) => - send(dialer_id, (NOT_LEADER, node.leader)); - loop node start_time, - - (* Prints the log *) - hn (DEBUG_PRINTLOG) => - pretty_print_log node.id node.log; - loop node start_time, - - (* Halts the follower *) - hn (DEBUG_PAUSE) => - let fun paused () = receive [ - hn (DEBUG_CONTINUE) => (), - hn _ => paused () - ] - in - paused (); - loop node start_time - end, - - (* Start an election, electing this follower to a candidate *) - hn (DEBUG_TIMEOUT) => start_election (), - hn x => loop node start_time - ] - in () - end - in loop node (getTime ()) -end -(* END OF ./libs/nodes/follower.trp *) - - (* A node is dormant until it has received the references of all other - dormant_node ({node with all_nodes = append node.all_nodes x}) - ] - else follower node - - (* Defines a default node, being a follower in term 1 without a leader and - the state-machine in its beginning state *) - fun default_node id all_nodes node_amount state_machine snapshot_cond = - let val node = { - all_nodes = all_nodes, - id = id, - log = empty_log, - term = 1, - voted_for = (), - leader = (), - leader_info = (), - state_machine = case state_machine of - (_, _, _) => state_machine - | _ => ([], WAIT, fn x => x ()), - snapshot_cond = snapshot_cond, - node_amount = node_amount, - serialkeys = [] - } - in dormant_node node - end - - (* Spawn a state-machine on a seperate thread, creates a record*) - fun initiate_node state_machine snapshot_cond node_amount id = - spawn (fn () => default_node id [] node_amount state_machine snapshot_cond) - - (* Sends a list of all nodes to all nodes *) - fun add_refs nodes = - map (fn x => send(x, (ADD_NODES, nodes))) nodes - - (* Spawn n nodes*) - fun initiate_nodes n state_machine snapshot_cond = - let val part_init = initiate_node state_machine snapshot_cond n - fun spawn_nodes n acc_id = - case n of - 0 => [] - | x => append - (spawn_nodes (x - 1) (acc_id ^ "I")) - [(part_init acc_id)] - - val nodes = spawn_nodes n "I" - in - add_refs nodes; - nodes - end - - (* Spawn a state-machine on some alias *) - fun initiate_distributed_node state_machine snapshot_cond node_amount id alias = - spawn(alias, fn () => (default_node id [] node_amount state_machine snapshot_cond)) - - fun initiate_distributed_nodes aliases state_machine snapshot_cond = - let val part_init = initiate_distributed_node state_machine snapshot_cond (length(aliases)) - fun spawn_nodes acc acc_id = - case acc of - [] => [] - | h :: t => - append (spawn_nodes t (acc_id ^ "I")) [part_init acc_id h] - val nodes = spawn_nodes aliases "I" - in - add_refs nodes; - nodes - end - - (* Spawns a dialer, dialing into a cluster. *) - fun raft_dial (cluster, client_id) = - spawn(fn () => dialer cluster client_id) - - (* Spawns a distributed Raft network, which can be dialed into to - communicate with their state-machines *) - fun raft_spawn_alias (state_machine, aliases, snapshot_cond) = - initiate_distributed_nodes aliases state_machine snapshot_cond - | raft_spawn_alias (state_machine, aliases) = - raft_spawn_alias (state_machine, aliases, 50) - - (* Spawns a Raft network, which can be contacted to - communicate with their state-machines *) - fun raft_spawn (state_machine, n, snapshot_cond) = - initiate_nodes n state_machine snapshot_cond - | raft_spawn (state_machine, n) = raft_spawn (state_machine, n, 50) - -in - [ ("raft_dial", raft_dial) - , ("raft_spawn_alias", raft_spawn_alias) - , ("raft_spawn", raft_spawn) - , ("WAIT", WAIT) - , ("SUS", SUS) - , ("DONE", DONE) - , ("RAFT_UPDATE", RAFT_UPDATE) - , ("CLIENT", CLIENT) - , ("CLUSTER", CLUSTER) - ] -end From d3cdcef24657743983473277997bc3ebf6856787 Mon Sep 17 00:00:00 2001 From: Victor Ask Justesen Date: Fri, 31 May 2024 15:14:45 +0200 Subject: [PATCH 6/8] Revert "Added Raft-Troupe to libraries" This reverts commit ee3f15eadaef6b4a5521d991360a33572402e9ec. --- Makefile | 3 ++- examples/network/aliases.json | 1 - examples/network/ids/pingpong-dialer.json | 1 - examples/network/ids/pingpong-listener.json | 1 - examples/network/pingpong/aliases.json | 2 +- examples/network/pingpong/ids/pingpong-dialer.json | 2 +- examples/network/pingpong/ids/pingpong-listener.json | 2 +- examples/network/pingpong/p2ppingpong.trp | 6 +----- lib/lists.trp | 8 ++++++++ lib/out/lists.exports | 1 + lib/out/raft.exports | 6 ++++++ lib/out/raft_debug.exports | 6 ++++++ 12 files changed, 27 insertions(+), 12 deletions(-) delete mode 100644 examples/network/aliases.json delete mode 100644 examples/network/ids/pingpong-dialer.json delete mode 100644 examples/network/ids/pingpong-listener.json create mode 100644 lib/out/raft.exports create mode 100644 lib/out/raft_debug.exports diff --git a/Makefile b/Makefile index 5a45cfd..38490f2 100644 --- a/Makefile +++ b/Makefile @@ -21,9 +21,10 @@ libs: $(COMPILER) ./lib/declassifyutil.trp -l $(COMPILER) ./lib/stdio.trp -l $(COMPILER) ./lib/timeout.trp -l + $(COMPILER) ./lib/raft.trp -l + $(COMPILER) ./lib/raft_debug.trp -l $(COMPILER) ./lib/bst.trp -l $(COMPILER) ./lib/localregistry.trp -l - $(COMPILER) ./lib/raft_troupe.trp -l test: mkdir -p out diff --git a/examples/network/aliases.json b/examples/network/aliases.json deleted file mode 100644 index 9f20c3f..0000000 --- a/examples/network/aliases.json +++ /dev/null @@ -1 +0,0 @@ -{"pingpong-listener":"12D3KooWK5UgRtP27Jdujm79Pq3QLJ5Gttg2PucDF8Hppd7txJiA","pingpong-dialer":"12D3KooWQ6DqK22D98Ro4WopLhsGPkUyb2d3EvckDrvSYta4E3Y6"} \ No newline at end of file diff --git a/examples/network/ids/pingpong-dialer.json b/examples/network/ids/pingpong-dialer.json deleted file mode 100644 index b0c774e..0000000 --- a/examples/network/ids/pingpong-dialer.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"12D3KooWQ6DqK22D98Ro4WopLhsGPkUyb2d3EvckDrvSYta4E3Y6","privKey":"CAESQMLauuc9ZpSyyUT6M2aEVHOmpWKtu1iJ1RgoF9J27chG1BN8ReNF+MDNiTWrpC3T66mQdb/lo1Rq0PDXJR2KmWs=","pubKey":"CAESINQTfEXjRfjAzYk1q6Qt0+upkHW/5aNUatDw1yUdiplr"} \ No newline at end of file diff --git a/examples/network/ids/pingpong-listener.json b/examples/network/ids/pingpong-listener.json deleted file mode 100644 index 36eab90..0000000 --- a/examples/network/ids/pingpong-listener.json +++ /dev/null @@ -1 +0,0 @@ -{"id":"12D3KooWK5UgRtP27Jdujm79Pq3QLJ5Gttg2PucDF8Hppd7txJiA","privKey":"CAESQJrfEr15UL1tJWbldgZzBh9T9Y9RrkRSAzDT4lKeK9EqiZhB5ZFmAGv3Q8cidbFUpoizcWTqTBIZjYmLn3n4jL8=","pubKey":"CAESIImYQeWRZgBr90PHInWxVKaIs3Fk6kwSGY2Ji595+Iy/"} \ No newline at end of file diff --git a/examples/network/pingpong/aliases.json b/examples/network/pingpong/aliases.json index 8dbf6b0..8607337 100644 --- a/examples/network/pingpong/aliases.json +++ b/examples/network/pingpong/aliases.json @@ -1 +1 @@ -{"pingpong-listener":"12D3KooWRreQ2wg6auezZE2FW5BYaRTW56HH7oPe5EPcwnTDACms","pingpong-dialer":"12D3KooW9yLx9GWMoh58Q3eLwnjSDgZ22pNcbULn1rL1B3g5dCCH"} \ No newline at end of file +{"pingpong-listener":"QmNLstfWyBabHSoCATbjUKyiTkiBbGMRfXKuT2b8Bv62AW","pingpong-dialer":"QmVVBs2C1C9rP72jmHcuJkzvZRPE2WBWUyCPwa7tNavFh7"} \ No newline at end of file diff --git a/examples/network/pingpong/ids/pingpong-dialer.json b/examples/network/pingpong/ids/pingpong-dialer.json index b32dc86..d64c738 100644 --- a/examples/network/pingpong/ids/pingpong-dialer.json +++ b/examples/network/pingpong/ids/pingpong-dialer.json @@ -1 +1 @@ -{"id":"12D3KooW9yLx9GWMoh58Q3eLwnjSDgZ22pNcbULn1rL1B3g5dCCH","privKey":"CAESQF6wIGIGQ5xtPa0aYpASt5nB02lIpyHoKZ/9B7RA3htpAkzZ1fgk6YZabzX9Cx9RMMU6++VHbbJr9eJ7mtgZd9o=","pubKey":"CAESIAJM2dX4JOmGWm81/QsfUTDFOvvlR22ya/Xie5rYGXfa"} \ No newline at end of file +{"id":"QmVVBs2C1C9rP72jmHcuJkzvZRPE2WBWUyCPwa7tNavFh7","privKey":"CAASpwkwggSjAgEAAoIBAQCmU3JZoXROh/HGZTxcQlg2Hun/U6zK0CE+jN7p63cEqfG1rYq6QmQ9oKdTTS7L4Vvtx9JCrcjsXIHQuKBTg1ZS8+hy3ZBo7TbgtYNvD9A8MQW2R6dqurNzUKn1qaxpZl13sQT7ol4cFF4viwMwAo128FeG2k1W4un+D9evg/8FfAbOBxsyTil97R1rWHHtyPobtr+zOV7a9mcbutaKzxWbWjZUQnJcd+BMyesFDYIAZhPYGC458jWkB7/1Sw6nWKIVuh1DDQiQ4AThzp7ks9JLbVKUNzumVn9zOy0CrVlmB3GVj7p77mzJkGuDZqh5CcsK9gs11i/xz5rfuZisqxGDAgMBAAECggEAd+pd3UVMZ3oX1GQUuqeSlaKALneTcr3P2hsSdDAxpQkpnUS7akKHpu729FYHUTLvZmXUsAI/hDnF1kfmP4/HYxM7GeWoQh4UnLoBQsdx6JOnfJ34lDh7PL6Bav6jsXH+HVdhMlMD6ta8eSaOa8TLXV82m6E0dVowPd4KMR7HdJmku/0G7czunZt2huuinYAZ24kLnVxSU47Z7ngQb4pvhaHqepFBEOlp9Gvku9q0nzVeWmLo1J4MPT4CsUybS+c//aFz9pw/6lBBV2SUQuzyZ00fYzLZSRaLKQEo87WUVkdcx81JgoSkFt2i8v7QzmSPDU1sPfC3RUnUluBIfVWz8QKBgQDsWbdRl2WRYDo/S3sD9kwt3WnE+Fdihc24uE1HgLuC9BhRDS9OOBc/XjlUvkCrxd0Glr1FTfP9ewNgxXQ5mnNTo/eMqblxlK/vUeqn4HXysgnBGhsgZmNC1vOb4Q9AnxS54uOBkqNjyyAdU02DHPVFvcfimmWohwBCTfgOywB1FQKBgQC0J2Nq1wnFjyamPcKzIGwEMQWQZ7pRHS9IeAb1c8xitCqFlR3vkKXgbn7EM2gisBLB/Wri5kylQr2Gy84jd7P4hWieGafm1Rc3r7zkafPVtyWomWcfDbcpJsiGsQop66EpBm869dnnexutbAAz3M3fhcjubqj9G1U5o7/1wXjCNwKBgQCTIe6rDlKeQ4c/K9/ywXr++l0Dz42muaEtox4Iqy0QAqC4pDqUuPpP6npKNP3RcSV9Go3M/RAs9k1OCt2llm7A3MwYdvgIqwUzOI2Z4HPMl+TWn0fPza1xSJryqRJzqhSe+42hdgXc8/CUEO2p93cA6XnrqS4r0Y7pt9v6aYlpWQKBgAo3AYgZUVCGYWajsdp+SCGktfAOMZ5PzVKKm7pnKnueQ5r3bY8b4IvtN/rf/1OYMDgXqmvbKxVjx2NRQwr3ypiY1+m/AqowAvUBXfCFoXHIxLXenN5B5NTMgipA95aQ6b5twvjQ394kONmIeip2pqW57D64v5Q6bIasJkJFChfZAoGAYyVkaNVHr8bdH9oxEPbVN40xhVD9J1SaJlk+JERmqtLn9TcXRqT3zvm27W+YQw7E4wEHITjT1P7l7sJ+8n1mzAUjsie8dTLoZ68TvwZC1ktIPU6tUkpefDya+IMbN3DU7X2jrk4y56hnofgK/AzKdBVK/cONoYXtWkotJ0EUZcw=","pubKey":"CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmU3JZoXROh/HGZTxcQlg2Hun/U6zK0CE+jN7p63cEqfG1rYq6QmQ9oKdTTS7L4Vvtx9JCrcjsXIHQuKBTg1ZS8+hy3ZBo7TbgtYNvD9A8MQW2R6dqurNzUKn1qaxpZl13sQT7ol4cFF4viwMwAo128FeG2k1W4un+D9evg/8FfAbOBxsyTil97R1rWHHtyPobtr+zOV7a9mcbutaKzxWbWjZUQnJcd+BMyesFDYIAZhPYGC458jWkB7/1Sw6nWKIVuh1DDQiQ4AThzp7ks9JLbVKUNzumVn9zOy0CrVlmB3GVj7p77mzJkGuDZqh5CcsK9gs11i/xz5rfuZisqxGDAgMBAAE="} \ No newline at end of file diff --git a/examples/network/pingpong/ids/pingpong-listener.json b/examples/network/pingpong/ids/pingpong-listener.json index f1157a8..61931cf 100644 --- a/examples/network/pingpong/ids/pingpong-listener.json +++ b/examples/network/pingpong/ids/pingpong-listener.json @@ -1 +1 @@ -{"id":"12D3KooWRreQ2wg6auezZE2FW5BYaRTW56HH7oPe5EPcwnTDACms","privKey":"CAESQF79dRdiG8tJ5zRBtAU3UMBSOGSPwRzdsgj5pNnOwHb/7lCAtk+OET71pdXNBhOSUQ64O4qdNCwp13M2Ul9VqF4=","pubKey":"CAESIO5QgLZPjhE+9aXVzQYTklEOuDuKnTQsKddzNlJfVahe"} \ No newline at end of file +{"id":"QmNLstfWyBabHSoCATbjUKyiTkiBbGMRfXKuT2b8Bv62AW","privKey":"CAASqAkwggSkAgEAAoIBAQDfhqItLOHTl6cw9hxWkdHipCtlLk5f2NkE4Suebb6h4WpFR8qafNJ8qJLXGxMaYW50OBeMDdnnsefpYbbC2CNgKUx2AnzZ784AdzeDUTTZDwdePwJSuemx1tlSgbw4KADLP8pYNleuflKyUqzdojpc47Sn1gv6zcYPolz+1aSP0Xl76UoRgdm8Zymzxk+xKyxyBYxgCkYu7RNwkoE7hpOyl7eRGPNG31978NR9vu5XdXri27ez51lHpZq7KBgquONFtfok4eEEF26qIJbcb/4xl73I8eXH+FJGqHgyzSyAp0guHXM8Fg+xuenmY0sNtVBAgFbB6unSJWQMFmnxBNoLAgMBAAECggEBAJOSy5ePvjh4M0W79tGgzDUZthzDCbN18zGph6a9RdKShBrhXv3H0x/CG9Awa9hK4yWPstwgePDjH/2RKZxSHmjqWzS+R7eK/zKHgvsLrhxwM6khaGM9ovBqrGgwhxd8Man+n5TFq/XkKKzasI5TAL07CJaWVqprGIxR4ZvNaSwZIERJCLHafn08aC4GVukXeWMoH0ya+WViKev6sGIq39MUHevnA/itpNrj51Btlqs8FHPaMMHzPSxcjEzBIo5upUKqklo7fsx/XE7sp9FeE7h3AaItZMzv/aFJpvMe/m33z7HZHKTBxleU0RnxAKPsW1ZIRr2PNM4wAvfb1XgJcGECgYEA+MN1klZVcUvmniiP8oQ9YHfc0rpC6FKQKJQdizZKQkkrm1m5ulSHx+JuAl0Nk4PcO9g8ZKuI9HLmCeK1fM8MF+rKlmDipI5ajU6P1sryZkzQAFIJLRfjnRub5uuVJWkTedIualE/QADllWEbVOw5eAZIKUAKOd+/kGh9wIWepP8CgYEA5gc63XZJBMgZI1N2hKeN6MmQHABEDacv4mA6Ymhg9y8JmsK890w3wHHdf2NI64rCvELWlPdyfet7SL/xxVI/pYE2ejJXvhrSe6ZARKLEifZ3c3nllHDQ1z2DXyXonhoevfLNdghED5sVvX0ajc246JC4/PrxmEWs18gMvOXVDvUCgYEA2wUUbewvPBosiNGDs200sMu3k51ErVGL9P47aMc66FON3jBIcsJb7ePxIYmWG2v8KoB+48+XPEoxOUDus12D80bYaUASK/ndxg4GXIHAm8tDUxTnWVlwIHIfeFewsAhsilRAY4D3JD3l5PhjXQjCrGczf4YPutbBzb4CAdBjVjcCgYB+AyvuMmRh6DRNM+XTWe7VvcXicQrW5+XFf628Ry4He48pZtEaMHjCRh5vMLa7wjJX682docjozl2lRvFthVc0lYqAep+ylwMDldnTP8+nPIvHiNmJ7huaLiqPrza1ld2NdTu1E2YlnnHUcnpfgHlxfga5H8fGATVkqETCHq4PGQKBgBGUpKmxcVbUNWE8zeOf19nqr5y6dovn3Wm6T1NqBP7yOpEK3wuSOwjwhqy4vdcSHcudiFQAurUVP3cz5WXbdhZp3B8g68VFfq+61fcxMDlXeiIhWxJdqmD2LZPGv4jg+oxB40cJC1R6x5WGuqQQNo/ezrtqP9Q/5XyX80alUXaK","pubKey":"CAASpgIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDfhqItLOHTl6cw9hxWkdHipCtlLk5f2NkE4Suebb6h4WpFR8qafNJ8qJLXGxMaYW50OBeMDdnnsefpYbbC2CNgKUx2AnzZ784AdzeDUTTZDwdePwJSuemx1tlSgbw4KADLP8pYNleuflKyUqzdojpc47Sn1gv6zcYPolz+1aSP0Xl76UoRgdm8Zymzxk+xKyxyBYxgCkYu7RNwkoE7hpOyl7eRGPNG31978NR9vu5XdXri27ez51lHpZq7KBgquONFtfok4eEEF26qIJbcb/4xl73I8eXH+FJGqHgyzSyAp0guHXM8Fg+xuenmY0sNtVBAgFbB6unSJWQMFmnxBNoLAgMBAAE="} \ No newline at end of file diff --git a/examples/network/pingpong/p2ppingpong.trp b/examples/network/pingpong/p2ppingpong.trp index 2091752..a6313f2 100644 --- a/examples/network/pingpong/p2ppingpong.trp +++ b/examples/network/pingpong/p2ppingpong.trp @@ -4,17 +4,13 @@ let fun pingpong () = val {counter, pid=sender} = receive [hn x => x] val _ = send (sender, {counter=counter + 1, pid=self()}) val _ = print counter - in - (if counter + 1 > 100 then - print (getTime()) else ()); - pingpong() + in pingpong() end in let val processA = spawn ("@pingpong-listener", pingpong) val _ = print processA val processB = spawn ("@pingpong-dialer", pingpong) val _ = print processB - val _ = print (getTime()) val _ = send (processA,{counter = 1, pid =processB}) in () end diff --git a/lib/lists.trp b/lib/lists.trp index b402da8..77d42bb 100644 --- a/lib/lists.trp +++ b/lib/lists.trp @@ -56,6 +56,13 @@ let fun map f list = in partition_aux ([],[]) ls end + fun contains element list = + case list of + [] => false + | h :: t => + if h = element then true + else contains element t + fun filter f l = case l of [] => [] | h :: t => if f h then h :: (filter f t) else filter f t @@ -90,6 +97,7 @@ in , ("append", append) , ("partition", partition) , ("nth", nth) + , ("contains", contains) , ("filter", filter) , ("first", first) , ("slice", slice) diff --git a/lib/out/lists.exports b/lib/out/lists.exports index db2a6ba..cf5a5c3 100644 --- a/lib/out/lists.exports +++ b/lib/out/lists.exports @@ -9,6 +9,7 @@ length append partition nth +contains filter first slice diff --git a/lib/out/raft.exports b/lib/out/raft.exports new file mode 100644 index 0000000..a34226e --- /dev/null +++ b/lib/out/raft.exports @@ -0,0 +1,6 @@ +START +pre_machine +spawn_machine +CLIENT_COMMAND +CLIENT_COMMAND_RESPONSE +NOT_LEADER \ No newline at end of file diff --git a/lib/out/raft_debug.exports b/lib/out/raft_debug.exports new file mode 100644 index 0000000..a34226e --- /dev/null +++ b/lib/out/raft_debug.exports @@ -0,0 +1,6 @@ +START +pre_machine +spawn_machine +CLIENT_COMMAND +CLIENT_COMMAND_RESPONSE +NOT_LEADER \ No newline at end of file From eded7254f05c9a97ab50989cff6f72b225c043c6 Mon Sep 17 00:00:00 2001 From: Victor Ask Justesen Date: Fri, 31 May 2024 15:15:51 +0200 Subject: [PATCH 7/8] Removed contains again --- lib/lists.trp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/lib/lists.trp b/lib/lists.trp index 77d42bb..b402da8 100644 --- a/lib/lists.trp +++ b/lib/lists.trp @@ -56,13 +56,6 @@ let fun map f list = in partition_aux ([],[]) ls end - fun contains element list = - case list of - [] => false - | h :: t => - if h = element then true - else contains element t - fun filter f l = case l of [] => [] | h :: t => if f h then h :: (filter f t) else filter f t @@ -97,7 +90,6 @@ in , ("append", append) , ("partition", partition) , ("nth", nth) - , ("contains", contains) , ("filter", filter) , ("first", first) , ("slice", slice) From 0da157437d2eefb152edf66d0570c2cf5d584701 Mon Sep 17 00:00:00 2001 From: Victor Ask Justesen Date: Mon, 3 Jun 2024 11:49:30 +0200 Subject: [PATCH 8/8] Added final Raft implementation to examples --- examples/raft-troupe/Makefile | 59 ++ examples/raft-troupe/aliases.json | 1 + examples/raft-troupe/build.py | 34 + examples/raft-troupe/build/raft_troupe.trp | 967 ++++++++++++++++++ examples/raft-troupe/ids/node1.json | 1 + examples/raft-troupe/ids/node10.json | 1 + examples/raft-troupe/ids/node11.json | 1 + examples/raft-troupe/ids/node2.json | 1 + examples/raft-troupe/ids/node3.json | 1 + examples/raft-troupe/ids/node4.json | 1 + examples/raft-troupe/ids/node5.json | 1 + examples/raft-troupe/ids/node6.json | 1 + examples/raft-troupe/ids/node7.json | 1 + examples/raft-troupe/ids/node8.json | 1 + examples/raft-troupe/ids/node9.json | 1 + examples/raft-troupe/ids/raft-dialer.json | 1 + ...m-into-the-troupe-programming-language.pdf | Bin 0 -> 212006 bytes examples/raft-troupe/libs/dialer.trp | 118 +++ examples/raft-troupe/libs/leader-info.trp | 51 + examples/raft-troupe/libs/log.trp | 159 +++ examples/raft-troupe/libs/nodes/candidate.trp | 70 ++ examples/raft-troupe/libs/nodes/follower.trp | 142 +++ examples/raft-troupe/libs/nodes/leader.trp | 164 +++ examples/raft-troupe/libs/quickselect.trp | 25 + .../raft-troupe/libs/state_machines/fib.trp | 27 + .../libs/state_machines/keyval-cps.trp | 30 + .../raft-troupe/libs/state_machines/ping.trp | 16 + examples/raft-troupe/node.trp | 324 ++++++ examples/raft-troupe/tests/bad_actor.trp | 52 + .../raft-troupe/tests/evaluation/fib_eval.trp | 53 + examples/raft-troupe/tests/ping-tests.trp | 35 + examples/raft-troupe/zero.trp | 1 + 32 files changed, 2340 insertions(+) create mode 100644 examples/raft-troupe/Makefile create mode 100644 examples/raft-troupe/aliases.json create mode 100755 examples/raft-troupe/build.py create mode 100644 examples/raft-troupe/build/raft_troupe.trp create mode 100644 examples/raft-troupe/ids/node1.json create mode 100644 examples/raft-troupe/ids/node10.json create mode 100644 examples/raft-troupe/ids/node11.json create mode 100644 examples/raft-troupe/ids/node2.json create mode 100644 examples/raft-troupe/ids/node3.json create mode 100644 examples/raft-troupe/ids/node4.json create mode 100644 examples/raft-troupe/ids/node5.json create mode 100644 examples/raft-troupe/ids/node6.json create mode 100644 examples/raft-troupe/ids/node7.json create mode 100644 examples/raft-troupe/ids/node8.json create mode 100644 examples/raft-troupe/ids/node9.json create mode 100644 examples/raft-troupe/ids/raft-dialer.json create mode 100644 examples/raft-troupe/implementing-a-raft-based-consensus-algorithm-into-the-troupe-programming-language.pdf create mode 100644 examples/raft-troupe/libs/dialer.trp create mode 100644 examples/raft-troupe/libs/leader-info.trp create mode 100644 examples/raft-troupe/libs/log.trp create mode 100644 examples/raft-troupe/libs/nodes/candidate.trp create mode 100644 examples/raft-troupe/libs/nodes/follower.trp create mode 100644 examples/raft-troupe/libs/nodes/leader.trp create mode 100644 examples/raft-troupe/libs/quickselect.trp create mode 100644 examples/raft-troupe/libs/state_machines/fib.trp create mode 100644 examples/raft-troupe/libs/state_machines/keyval-cps.trp create mode 100644 examples/raft-troupe/libs/state_machines/ping.trp create mode 100755 examples/raft-troupe/node.trp create mode 100644 examples/raft-troupe/tests/bad_actor.trp create mode 100644 examples/raft-troupe/tests/evaluation/fib_eval.trp create mode 100644 examples/raft-troupe/tests/ping-tests.trp create mode 100644 examples/raft-troupe/zero.trp diff --git a/examples/raft-troupe/Makefile b/examples/raft-troupe/Makefile new file mode 100644 index 0000000..968bd9f --- /dev/null +++ b/examples/raft-troupe/Makefile @@ -0,0 +1,59 @@ +MKID=node $(TROUPE)/rt/built/p2p/mkid.mjs +MKALIASES=node $(TROUPE)/rt/built/p2p/mkaliases.js +START=$(TROUPE)/network.sh +LOCAL=$(TROUPE)/local.sh + +LIBS := $(shell find libs -type f) +TESTS := $(shell find tests -type f) + +run: build/raft_troupe.trp + $(LOCAL) ./build/raft_troupe.trp + +build/raft_troupe.trp: node.trp $(LIBS) $(TESTS) + python build.py node.trp + +zero.listener1: + $(START) zero.trp --id=ids/node1.json --rspawn=true --aliases=aliases.json --stdiolev={} # +zero.listener2: + $(START) zero.trp --id=ids/node2.json --rspawn=true --aliases=aliases.json --stdiolev={} # +zero.listener3: + $(START) zero.trp --id=ids/node3.json --rspawn=true --aliases=aliases.json --stdiolev={} # +zero.listener4: + $(START) zero.trp --id=ids/node4.json --rspawn=true --aliases=aliases.json --stdiolev={} # +zero.listener5: + $(START) zero.trp --id=ids/node5.json --rspawn=true --aliases=aliases.json --stdiolev={} # +zero.listener6: + $(START) zero.trp --id=ids/node6.json --rspawn=true --aliases=aliases.json --stdiolev={} # +zero.listener7: + $(START) zero.trp --id=ids/node7.json --rspawn=true --aliases=aliases.json --stdiolev={} # +zero.listener8: + $(START) zero.trp --id=ids/node8.json --rspawn=true --aliases=aliases.json --stdiolev={} # +zero.listener9: + $(START) zero.trp --id=ids/node9.json --rspawn=true --aliases=aliases.json --stdiolev={} # +zero.listener10: + $(START) zero.trp --id=ids/node10.json --rspawn=true --aliases=aliases.json --stdiolev={} # +zero.listener11: + $(START) zero.trp --id=ids/node11.json --rspawn=true --aliases=aliases.json --stdiolev={} # + +raft.dialer: build/raft_troupe.trp + $(START) ./build/raft_troupe.trp --id=ids/raft-dialer.json --aliases=aliases.json + +test.dialer: + $(START) test.trp --id=ids/raft-dialer.json --aliases=aliases.json # --debug --debugp2p + + +create-network-identifiers: + mkdir -p ids + $(MKID) --outfile=ids/raft-dialer.json + $(MKID) --outfile=ids/node1.json + $(MKID) --outfile=ids/node2.json + $(MKID) --outfile=ids/node3.json + $(MKID) --outfile=ids/node4.json + $(MKID) --outfile=ids/node5.json + $(MKID) --outfile=ids/node6.json + $(MKID) --outfile=ids/node7.json + $(MKID) --outfile=ids/node8.json + $(MKID) --outfile=ids/node9.json + $(MKID) --outfile=ids/node10.json + $(MKID) --outfile=ids/node11.json + $(MKALIASES) --include ids/raft-dialer.json --include ids/node1.json --include ids/node2.json --include ids/node3.json --include ids/node4.json --include ids/node5.json --include ids/node6.json --include ids/node7.json --include ids/node8.json --include ids/node9.json --include ids/node10.json --include ids/node11.json --outfile aliases.json diff --git a/examples/raft-troupe/aliases.json b/examples/raft-troupe/aliases.json new file mode 100644 index 0000000..0502a36 --- /dev/null +++ b/examples/raft-troupe/aliases.json @@ -0,0 +1 @@ +{"raft-dialer":"12D3KooWDRik7kYXVNES7GVo9GcujxPttNqwLHoHmdp19wLSVrgc","node1":"12D3KooWEqiFsAJDFucPu8p77jLkDx1EgpL1UjFCbttiULEvEQRj","node2":"12D3KooWEvWUngvpYBp4asiHJqM7BpWGzH3ykv9P8JiUDKMyPQV6","node3":"12D3KooWDWGgz9tm3zg7C6HaqsCaztKtp4QGKD6S7a7TL8u3dkRA","node4":"12D3KooWDpfXNjRh22FFJgUcc4g8FqMxrVTfeNRDST2g56aYRtRE","node5":"12D3KooWRQkHssDfdeCNvitXY1rh1HaQ6rHJ67Lx3fkVQQ2puZVG","node6":"12D3KooWBFEAgdHniZbaciJPv62evrZbz78sh69cHw8DNYMWeTTb","node7":"12D3KooWHsMaKdPcSwN2pydP7ZX9arQTgAFukCgiFLJ9jgvukkxz","node8":"12D3KooWLNFHoDDHgiTd4QkJ7FNMBC79iY8G1t1Vyr6CNR7ZeCqM","node9":"12D3KooWCS1nVjpAJPFZrz9DkHHEMTpuXzDaFJh8zt4bzBtod6Jb","node10":"12D3KooWLCW72v8mRZAxa1YNRtVkJrVuCSDmy1yt1wnPB1NH894e","node11":"12D3KooWA7Promv3T6AR7BNmaudfjkdtRc3mxkJoqgDCbyhjmoyZ"} \ No newline at end of file diff --git a/examples/raft-troupe/build.py b/examples/raft-troupe/build.py new file mode 100755 index 0000000..14e711a --- /dev/null +++ b/examples/raft-troupe/build.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +import re +import sys + +if len(sys.argv) < 2: + print("Needs file to build") + exit(1) + +src = open(sys.argv[1], 'r') +dest = open("build/raft_troupe.trp", 'w') + +print("Building:" + sys.argv[1]) + +for line in src.readlines(): + dest.write(line) + match = re.search('(?<=\#IMPORT ).*\.trp', line) + if match is None: + continue + + with open(match.group(0), 'r') as f: + print("Importing: " + match.group(0)) + exportgroup = re.search( + '(\(\* ?EXPORT START ?\*\)\n)((.|\n)*)(\(\* ?EXPORT END ?\*\))', f.read()) + if exportgroup is None: + for li in f.readlines(): + dest.write(li) + else: + for li in exportgroup.group(2): + dest.write(li) + dest.write("(* END OF " + match.group(0) + " *)\n") + +src.close() +dest.close() diff --git a/examples/raft-troupe/build/raft_troupe.trp b/examples/raft-troupe/build/raft_troupe.trp new file mode 100644 index 0000000..93bb11d --- /dev/null +++ b/examples/raft-troupe/build/raft_troupe.trp @@ -0,0 +1,967 @@ +import lists + +let + (* Constants *) + val WAIT = "WAIT" + val SUS = "SUS" + val DONE = "DONE" + val SEND_HEARTBEAT = "SEND_HEARTBEAT" + val RAFT_UPDATE = "RAFT_UPDATE" + val NOT_LEADER = "NOT_LEADER" + val ACKNOWLEDGE = "ACKNOWLEDGE" + val REJECT = "REJECT" + val ELECTION_TIMEOUT = "ELECTION_TIMEOUT" + val REQUEST_VOTE = "REQUEST_VOTE" + val YES_VOTE = "YES_VOTE" + val NO_VOTE = "NO_VOTE" + val VOTE_TIMEOUT = "VOTE_TIMEOUT" + val APPEND_ENTRIES = "APPEND_ENTRIES" + val SNAPSHOT = "SNAPSHOT" + val ADD_NODES = "ADD_NODES" + val DIAL = "DIAL" + val DIALER_ACK = "DIALER_ACK" + val DIALER_SM_BUSY = "DIALER_SM_BUSY" + val DIALER_SM_DONE = "DIALER_SM_DONE" + val DIALER_CLIENT_MSG = "DIALER_CLIENT_MSG" + val DIALER_MESSAGE_TIMEOUT = "DIALER_MESSAGE_TIMEOUT" + val DIALER_BUSY_TIMEOUT = "DIALER_BUSY_TIMEOUT" + val SEND_TO_NTH = "SEND_TO_NTH" + val SEND_TO_ALL = "SEND_TO_ALL" + val DEBUG_PRINTLOG = "DEBUG_PRINTLOG" + val DEBUG_PAUSE = "DEBUG_PAUSE" + val DEBUG_CONTINUE = "DEBUG_CONTINUE" + val DEBUG_APPLYSNAPSHOT = "DEBUG_APPLYSNAPSHOT" + val DEBUG_SNAPSHOT_COND = "DEBUG_SNAPSHOT_COND" + val DEBUG_TIMEOUT = "DEBUG_TIMEOUT" + val FUNCTION_DONE = "FUNCTION_DONE" + val ERROR_TIMEOUT = "ERROR_TIMEOUT" + val CLUSTER = "CLUSTER" + val CLIENT = "CLIENT" + + fun not a = a = false + val contains = elem + fun send_to_all processes msg sender = map (fn x => send(x, msg)) (filter (fn x => x <> sender) processes) + + fun send_to_nth processes msg n = send((nth (reverse processes) n), msg) + + fun max a b = if a < b then b else a + + fun min a b = if a > b then b else a + + (* #IMPORT libs/quickselect.trp *) + fun is_even i = i mod 2 = 0 + + (* Using QuickSelect, finds the kth element of a list. *) + fun quickselect list k = + case list of + [] => "ERROR: Empty list" + | h :: t => + let val (ys, zs) = partition (fn x => x > h) t + val l = length ys + in + if k < l then quickselect ys k + else if k > l then quickselect zs (k-l-1) + else h + end + + (* Returns the median of a list. *) + fun median list = + let val len = length list + val middle = if is_even len then len / 2 - 1 else (len - 1) / 2 + in quickselect list (middle) + end + (* END OF libs/quickselect.trp *) + + (* #IMPORT libs/log.trp *) + (* Creates a snapshot. *) + fun set_snapshot snapshot index term = { + snapshot = snapshot, + lastIncludedIndex = index, + lastIncludedTerm = term + } + + (* Creates a default, empty snapshot. *) + val empty_snapshot = { + snapshot = (), + lastIncludedIndex = 0, + lastIncludedTerm = 0 + } + + (* A default, empty log. *) + val empty_log = { + log = [], + snapshot = empty_snapshot, + lastApplied = 0, + commitIndex = 0, + lastMessageSerial = "" + } + + fun pretty_print_log id log = + (* Disabled for library *) + (* printString "\n========******========"; + print (length log.log); + printString ("ID: "^id); + printString "----------------------"; + printString "Entries (term, message):"; + map (fn x => print (x.term, x.command)) log.log; + printString "----------------------"; + printString "CommitIndex:"; + print log.commitIndex; + printString "LastApplied:"; + print log.lastApplied; + printString "----------------------"; + printString "Snapshot:"; + print log.snapshot; + printString "========******========\n";*) + () + + (* Appends a message to the log, and notes the message's serial number. *) + fun append_message log message callback term serial = + let val new_entry = { + term = term, + command = message, + callback = callback, + serial = serial + } + in { + log with + lastMessageSerial = serial, + log = new_entry :: log.log + } + end + + + (* Appends a list of message to the log. *) + fun add_entries_to_log log entries term = + case entries of + [] => log + | h :: t => + add_entries_to_log (append_message log h.command h.callback term h.serial) t + h.term + + (* Updates the lastApplied-index. *) + fun update_applied log = { + log with + lastApplied = log.lastApplied + 1 + } + + (* Commits a message in the log. *) + fun update_commit log new_index = { + log with + commitIndex = (max new_index log.commitIndex) + } + + (* Rolls the log back one entry. *) + fun rollback_log log = + let val loglog = log.log + in case loglog of + (_ :: prev_log) => { + log with + log = prev_log + } + | [] => {log with log = []} + end + + (* Get the entry of the latest log entry. *) + fun get_log_index log = (length log.log) + log.snapshot.lastIncludedIndex + + (*Determines whether or not all log changes have been committed. *) + fun log_is_committed log = (get_log_index log = log.commitIndex) + + (* Rolls the log back n time. *) + fun rollback_log_to log n = + if n < (get_log_index log) then + let val log = rollback_log log + in (rollback_log_to log n) + end + else log + + (* Get the term of the latest entry of the log, or, if empty, the last + included index of the snapshot. *) + fun get_latest_entry_term log = + case log.log of + [] => log.snapshot.lastIncludedTerm + | h :: _ => h.term + + (* Get the term of the latest log entry. *) + fun get_latest_log_term log = get_latest_entry_term log + + (* Get the message of the latest log entry. *) + fun get_latest_log_command log = + case log.log of + [] => 0 (* Should not be reachable. *) + | h :: _ => h.command + + fun get_nth_command log index = nth (reverse log.log) (index - log.snapshot.lastIncludedIndex) + + (* Returns a slice of all entries after log-index n. *) + fun get_commands_after_nth entries n last_included = + let val log_slice = slice (n - last_included) (length entries) (reverse entries) + in log_slice + end + + (* Get a snapshot of all committed entries. *) + fun get_snapshot state log = + if log.commitIndex > 0 andalso + (log.commitIndex - log.snapshot.lastIncludedIndex) <= length log.log then + let val lastCommitted = get_nth_command log log.commitIndex + in set_snapshot state log.commitIndex lastCommitted.term end + else empty_snapshot + + + (* Applies a snapshot to the log. *) + fun apply_snapshot snapshot log = + let val newCommitIndex = + if log.commitIndex < snapshot.lastIncludedIndex then snapshot.lastIncludedIndex + else log.commitIndex + val uncommitted_entries = get_commands_after_nth log.log newCommitIndex log.snapshot.lastIncludedIndex + val newLastApplied = + if log.lastApplied < snapshot.lastIncludedIndex then snapshot.lastIncludedIndex + else log.lastApplied + in { log with + log = uncommitted_entries, + commitIndex = newCommitIndex, + lastApplied = newLastApplied, + snapshot = snapshot } + end + + (* Asks the state-machine whether or not to snapshot. *) + fun evaluate_snapshot_cond state snapshot_cond log = + if (log.lastApplied - log.snapshot.lastIncludedIndex) > snapshot_cond then + apply_snapshot (get_snapshot state log) log + else log + (* END OF libs/log.trp *) + + (* #IMPORT libs/leader-info.trp *) + + (* Generates a default leader info, with the nextIndex of all followers + being the nextIndex of the new leader. This can be changed with followers + rejecting AppendEntries *) + fun new_leader all_nodes log = + let val nextIndex = get_log_index log + val index = map (fn id => {peer = id, next = nextIndex + 1}) all_nodes + val match_index = map (fn id => {peer = id, match = 0}) all_nodes + in { + nextIndex = index, + matchIndex = match_index + } end + + (* Get the nextIndex of a peer *) + fun get_next_index leader_info peer = first (filter (fn (x) => x.peer = peer) leader_info.nextIndex) + + (* Get the matchIndex of a peer *) + fun get_match_index leader_info peer = first (filter (fn (x) => x.peer = peer) leader_info.matchIndex) + + (* Updates a cluster member's next-index. This is done after an + acknowledgement or rejection. *) + fun update_next_index leader_info peer new = let + val prevIndex = get_next_index leader_info peer + val newIndex = {peer = peer, next = new} + val withoutPeer = filter (fn (x) => x.peer <> peer) leader_info.nextIndex + in { + leader_info with + nextIndex = newIndex :: withoutPeer + } end + + (* Updates a cluster member's match-index, denoting how much of their log + matches the leader holding the leader info. *) + fun update_match_index leader_info peer new = let + val prevIndex = get_match_index leader_info peer + val newIndex = {peer = peer, match = new} + val withoutPeer = filter (fn (x) => x.peer <> peer) leader_info.matchIndex + in { + leader_info with + matchIndex = newIndex :: withoutPeer + } end + + (* Get all follower's matchIndex*) + fun get_matches leader_info = map (fn x => x.match) leader_info.matchIndex + + (* Get the highest index of entries that a majority of followers have + appended to, by finding the median *) + fun calc_highest_commit matches = median matches + (* END OF libs/leader-info.trp *) + + (* Executes a function after a given timeout. *) + fun start_timeout func duration = + let fun timeout () = + let val time = duration + val _ = sleep time + in func () + end + val p_id = self() + in spawn timeout + end + + (* Send message after a delay. *) + fun send_delay (to, m) delay = + sleep delay; + send (to, m) + + (* Starts a random timeout with lower=2sec and upper=4sec *) + fun start_random_timeout func settings = start_timeout func (settings.ELECTION_TIMEOUT_LOWER + ((random ()) * (settings.ELECTION_TIMEOUT_UPPER - settings.ELECTION_TIMEOUT_LOWER))) + + (* #IMPORT ./libs/dialer.trp *) + +(* Selects a random element from a list *) +fun random_element list = + let fun roundUp n m = + if n <= 0 then m else roundUp (n - 1) (m + 1) + val r_n = roundUp (random() * (length list - 1)) 0 + in nth list r_n +end + + +(* Given a list of serialkeys, and a serial key, check if it is valid, and +return a list containing the serial key if so, and a boolean denoting whether or +not it is valid. *) +fun apply_serialkey list key = + case list of + [] => (true, []) + | h :: t => + if h = key then + case (h.key, key.key) of + ((log_index, seq_numb), (new_log_index, new_seq_numb)) => + if new_log_index > log_index orelse + (log_index = new_log_index andalso new_seq_numb > seq_numb) then + (true, ({ h with key = key.key } :: t)) + else (false, h :: t) + | (nonce, new_nonce) => + if nonce <> new_nonce then + (true, ({ h with key = key.key } :: t)) + else (false, h :: t) + | _ => (true, ({ h with key = key.key } :: t)) + else + let val (cond, list) = apply_serialkey t key + in (cond, h :: list) + end + + +(* Used by the dialer to send message to a cluster. If the nodes are busy or if +no leader is present, this function re-sends the message until it is eventually +delivered and acknowledged by the leader of the cluster. If leader is unknown, +it can be defined as unit.*) +fun dialer_send_message p_id msg serial_n leader cluster dialer_settings = + let val nonce = mkuuid() + val msg_timeout = start_timeout (fn() => send(p_id, (DIALER_MESSAGE_TIMEOUT, nonce))) + val busy_timeout = start_timeout (fn() => send(p_id, (DIALER_BUSY_TIMEOUT, nonce))) + fun wait () = + receive [ + hn ("NOT_LEADER", leader_id) => + dialer_send_message p_id msg serial_n leader_id cluster dialer_settings, + hn ("DIALER_ACK", other_serial) when other_serial = serial_n => + leader, + hn ("DIALER_SM_BUSY", other_serial) when other_serial = serial_n => + busy_timeout dialer_settings.DIALER_SM_BUSY_TIMEOUT; + wait (), + hn ("DIALER_SM_DONE", other_serial) when other_serial = serial_n => + leader, + hn ("DIALER_MESSAGE_TIMEOUT", x) => + if x = nonce then dialer_send_message p_id msg serial_n (random_element cluster) cluster dialer_settings + else wait (), + hn ("DIALER_BUSY_TIMEOUT", x) => + if x = nonce then dialer_send_message p_id msg serial_n leader cluster dialer_settings + else wait () + ] + in (case leader of + () => msg_timeout dialer_settings.DIALER_NOLEADER_TIMEOUT + | x => + msg_timeout dialer_settings.DIALER_NOMSG_TIMEOUT; + send(x, msg)); + wait () +end + +(* Facilitates client-side interaction to the Raft cluster. Allows the +programmer to send messages to the cluster in the format (RAFT_UPDATE, msg)*) +fun dialer cluster client_id dialer_settings = + let val p_id = self() + fun update_message x leader = let + val serial_n = mkuuid() + in dialer_send_message p_id ((RAFT_UPDATE, x), p_id, serial_n) serial_n leader cluster dialer_settings + end + val leader = random_element cluster + + fun loop leader sks = + receive [ + hn ("RAFT_UPDATE", x) => + loop (update_message x leader) sks, + + hn ("DIALER_CLIENT_MSG", msg, sk) => + let val (cond, sks) = apply_serialkey sks sk + in + (if cond then send(client_id, msg) + else ()); + loop leader sks + end, + + hn ("SEND_TO_NTH", n, x) => + send_to_nth cluster x n; + loop leader sks, + + hn ("SEND_TO_ALL", x) => + send_to_all cluster x (self()); + loop leader sks, + hn _ => loop leader sks ] + in loop leader [] +end + +(* Temporary dialer sends a list of messages to a cluster before terminating. *) +fun leader_dialer cluster msgs dialer_settings = + let val p_id = self() + val leader = random_element cluster + in + map (fn (msg, serial) => dialer_send_message p_id ((RAFT_UPDATE, msg), p_id, serial) serial leader cluster dialer_settings) msgs +end + +(* Send-function used for clusters to send a message to either the dialer or +client. *) +fun raft_send (process, msgs) dialer_settings = case process.type of +"CLIENT" => map (fn (msg, sk) => send(process.id, (DIALER_CLIENT_MSG, msg, sk))) msgs +| "CLUSTER" => spawn (fn () => leader_dialer process.id msgs dialer_settings) +(* END OF ./libs/dialer.trp *) + + (* Send the side-effect-messages to dialers or clusters *) + fun send_sides log sides dialer_settings = + (* Add message to key-value-store, sorting by the recipients. *) + let fun add_msg id msg sk dict = case dict of + [] => [(id, [(msg, sk)])] + | (other_id, msgs) :: t => + if id = other_id then + (id, (msg, sk) :: t) + else (other_id, msgs) :: add_msg id msg sk t + (* Generate key-value-store of all message, sorting by recipients. *) + val (sorted_msgs, _) = case sides of + [] => ([], 0) + | x => foldl (fn ((callback, msg), (acc, seq)) => + (add_msg callback msg ({ id = callback, key = (log.lastApplied, seq)}) acc, seq + 1) + ) ([], 1) x + (* Sends all messages. *) + in map (fn x => raft_send x dialer_settings) sorted_msgs + end + + (* Applies all log-entries that have been committed, but not applied *) + fun apply_log log state_machine is_leader dialer_settings = + (* If any non-applied, committed logs apply... *) + if log.lastApplied < log.commitIndex then + (* Get the latest non-applied committed entry *) + let val entry = get_nth_command log (log.lastApplied + 1) + val command = entry.command + (* Update log to apply entry and apply entry on state-machine*) + val log = update_applied log + val (sides, status, step) = state_machine + val (new_sides, new_status, new_step) = step command + (* If leader is applying, execute side-effects. *) + in (if is_leader then + entry.callback (); + send_sides log new_sides dialer_settings + else ()); + apply_log log (new_sides, new_status, new_step) is_leader dialer_settings end + else (log, state_machine) + + (* #IMPORT ./libs/nodes/leader.trp *) +fun leader_node node = + let val p_id = self() + (* Appends appends all entries from a follower's nextIndex to the leader's log index*) + fun append_entries node follower_pid = + let val nextIndex = get_next_index node.leader_info follower_pid + val logIndex = get_log_index node.log + in if logIndex + 1 >= nextIndex.next then + let + val latestLogIndex = nextIndex.next - 1 + in + (* Sends the snapshot if the followers nextIndex is before the Snapshot's lastIncludedIndex *) + if nextIndex.next <= node.log.snapshot.lastIncludedIndex + then send(follower_pid, (SNAPSHOT, node.log.snapshot, p_id, node.term)) + else + let val entries = get_commands_after_nth node.log.log latestLogIndex node.log.snapshot.lastIncludedIndex + val afterSnapshot = latestLogIndex - node.log.snapshot.lastIncludedIndex + val prevEntryTerm = + if afterSnapshot > 0 then (get_nth_command node.log latestLogIndex).term + else node.log.snapshot.lastIncludedTerm + in send(follower_pid, (APPEND_ENTRIES, entries, p_id, node.term, latestLogIndex, prevEntryTerm, node.log.commitIndex)) + end + end + (* A follower should never get more entries than the leader *) + else () + end + + (* Convert leader to follower *) + fun demote term leader voted_for node = + {node with + term = term, + leader = leader, + leader_info = (), + voted_for = voted_for} + + fun append_update node msg callback serial = + let val latestLogIndex = get_log_index node.log + val prevLogTerm = get_latest_log_term node.log + val log = append_message node.log msg callback node.term serial + val leader_info = update_match_index node.leader_info p_id (get_log_index log) + val leader_info = update_next_index leader_info p_id ((get_log_index log) + 1) + val node = {node with log = log, leader_info = leader_info} + in node + end + + (* Applies all committed log entries that have not already been applied *) + fun apply_committed node = + let val prev_commit = node.log.commitIndex + val highest_commit = calc_highest_commit (map (fn x => x.match) node.leader_info.matchIndex) + val node = { node with log = update_commit node.log highest_commit } + val (applied_log, new_sm) = apply_log node.log node.state_machine true node.settings.leader_dialer_settings + val snapshot_log = + if prev_commit < highest_commit then + evaluate_snapshot_cond new_sm node.snapshot_cond applied_log + else + applied_log + val (_, status, _) = new_sm + val node = { node with log = snapshot_log, state_machine = new_sm} + in + case status of + "SUS" => if log_is_committed node.log then append_update node () (fn () => ()) (mkuuid()) + else node + | _ => node + end + + val nonce = mkuuid () + + fun loop node = + receive [ + (* Halts the leader *) + hn "DEBUG_PAUSE" => + let fun pause () = receive [ + hn ("DEBUG_CONTINUE") => loop node, + hn x => pause () + ] + in pause () end, + + hn ("SEND_HEARTBEAT", x) when nonce = x => + leader_node node, + + (* Message has not been appended before *) + hn (("RAFT_UPDATE", x), dialer_id, serial_n) => + let val (cond, sks) = apply_serialkey node.serialkeys serial_n + in if cond then let + val (_, stat, _) = node.state_machine + val node = case stat of + "SUS" => send(dialer_id, (DIALER_SM_BUSY, serial_n)); node + | "DONE" => send(dialer_id, (DIALER_SM_DONE, serial_n)); node + | "WAIT" => + if log_is_committed node.log then + let fun replication_cb () = send (dialer_id, (DIALER_ACK, serial_n)) + in append_update node x replication_cb serial_n end + else send(dialer_id, (DIALER_SM_BUSY, serial_n)); node + in leader_node node end + else send(dialer_id, (DIALER_ACK, serial_n)); + loop node + end, + + (* If append is successful on a follower*) + hn ("ACKNOWLEDGE", (peer, logIndex)) => + let val prev_index = get_log_index node.log + val node = { node with leader_info = update_match_index node.leader_info peer logIndex } + val node = { node with leader_info = update_next_index node.leader_info peer (logIndex + 1) } + val node = apply_committed node + val next_index = get_next_index node.leader_info peer + in (if not node.settings.TIE_COMMITS_TO_HEARTBEAT andalso + prev_index < get_log_index node.log then + map (fn x => append_entries node x) + (filter (fn x => + let val next_index = get_next_index node.leader_info x + in x <> p_id andalso next_index.next > logIndex end) node.all_nodes) + else ()); + loop node + end, + + (* If append is unsuccessful *) + hn ("REJECT", (peer, terminfo, logIndex)) => + if node.term >= terminfo.term then + let val node = { node with leader_info = update_next_index node.leader_info peer (logIndex + 1) } + in loop node + end + else follower (demote terminfo.term terminfo.leader () + node), + + (* If another node has been elected as a candidate, and + their term is in front of ours, convert to a follower *) + hn ("REQUEST_VOTE", (c_term, c_id, c_log_index, c_log_term)) when c_term > node.term => + send(c_id, (YES_VOTE, node.id)); + follower (demote c_term () c_id node), + + hn ("REQUEST_VOTE", (c_term, c_id, c_log_index, c_log_term)) => + send(c_id, (NO_VOTE, node.id)); + loop ({node with term = max c_term node.term}), + + (* If we receive snapshot from a leader in a higher term, + convert to follower *) + hn ("SNAPSHOT", snapshot, l_id, other_term) when other_term > node.term => follower (demote other_term l_id () node), + + (* If we receive AppendEntries from a leader in a higher term, + convert to follower *) + hn ("APPEND_ENTRIES", x, l_id, other_term, prevIndex, prevTerm, commitIndex) when other_term > node.term => follower (demote other_term l_id () node), + + (* Prints log *) + hn "DEBUG_PRINTLOG" => + pretty_print_log node.id node.log; + loop node, + + (* Applies a snapshot *) + hn "DEBUG_APPLYSNAPSHOT" => + let + val snapshot = get_snapshot node.state_machine node.log + val node = case snapshot.snapshot of + () => node + | _ => {node with log = apply_snapshot snapshot node.log} + in loop node end, + hn _ => loop node + ] + in + (* Append entries for each follower *) + map (fn x => append_entries node x) (filter (fn x => x <> p_id) node.all_nodes); + start_timeout (fn () => send (p_id, (SEND_HEARTBEAT, nonce))) node.settings.HEARTBEAT_INTERVAL; + loop node +end +(* END OF ./libs/nodes/leader.trp *) + (* #IMPORT ./libs/nodes/candidate.trp *) +and candidate node = + let val p_id = self() + + (* A candidate cannot vote for anyone and has no leader *) + val node = {node with voted_for = (), leader = ()} + val nonce = mkuuid() + + (* Sends a vote request to all followers *) + val latestLogIndex = get_log_index node.log + val prevLogTerm = get_latest_log_term node.log + + + (* Becoming a leader requires majority vote *) + val req_votes = ((length node.all_nodes) / 2) + + fun won_election () = + let val (sides, _, _) = node.state_machine + in + send_sides node.log sides; + leader_node ({ + node with leader_info = (new_leader node.all_nodes node.log), + leader = (p_id)}) + end + + fun wait_for_votes (follower_votes, vote_amount) = + let + fun loop () = receive [ + (* Received a vote from a follower we have not already + received a vote from *) + hn ("YES_VOTE", follower_id) when (not (contains follower_id follower_votes)) => + wait_for_votes ((append follower_votes [follower_id]), vote_amount + 1), + + (*We received a NO_VOTE from a follower in a later term. + This can only happen if there is a leader/candidate in this + term, and as such, we convert to a follower *) + hn ("NO_VOTE", other_term) when other_term > node.term => + follower node, + + (* Received vote request from candidate in later term *) + hn ("REQUEST_VOTE", (c_term, other_c_id, c_log_index, c_log_term)) when c_term > node.term => + send(other_c_id, (YES_VOTE, node.id)); + follower ({ node with term = c_term, voted_for = other_c_id}), + + (* Received message from leader in a term at least as + up-to-date as ours. Because of this, we must have lost the + election *) + hn ("APPEND_ENTRIES", x, l_id, other_term, prevIndex, prevTerm, commitIndex) when other_term >= node.term => + follower ({ node with leader = l_id}), + + (* Election timeout, send out another request vote *) + hn ("VOTE_TIMEOUT", x) when x = nonce => candidate {node with term = node.term + 1}, + + (* Halts the candidate *) + hn ("DEBUG_PAUSE") => + let fun loop () = receive [ + hn ("DEBUG_CONTINUE") => (), + hn x => loop () + ] + in loop () end, + hn _ => loop () + ] + in if vote_amount >= req_votes then won_election () else loop () + end + in + send_to_all node.all_nodes (REQUEST_VOTE, (node.term, p_id, latestLogIndex, prevLogTerm)) (p_id); + start_random_timeout (fn () => send(p_id, (VOTE_TIMEOUT, nonce))) node.settings; + wait_for_votes ([node.id], 1) +end +(* END OF ./libs/nodes/candidate.trp *) + (* #IMPORT ./libs/nodes/follower.trp *) +and follower node = + let val nonce = mkuuid() + val p_id = self() + val _ = start_random_timeout (fn () => send(p_id, (ELECTION_TIMEOUT, nonce))) node.settings + (* Sends a YES_VOTE to a candidate *) + fun vote_for c_id c_term node = + send(c_id, (YES_VOTE, node.id)); + { node with term = c_term, voted_for = c_id } + fun loop node = let + fun start_election () = + candidate ({node with term = node.term + 1}) + val _ = receive [ + (* Starts an election *) + hn ("ELECTION_TIMEOUT", x) when x = nonce => start_election (), + + (* Sends a re-vote to a candidate we already voted for *) + hn ("REQUEST_VOTE", (c_term, c_id, c_log_index, c_log_term)) when c_id = node.voted_for => + follower (vote_for c_id c_term node), + + (* If we receive a vote request, vote yes if: the log is a + up-to-date and the term of the candidate is later than our + current. Vote no otherwise *) + hn ("REQUEST_VOTE", (c_term, c_id, c_log_index, c_log_term)) => + let val latestLogIndex = get_log_index node.log + val latestLogTerm = get_latest_log_term node.log + val old_term = node.term + val node = {node with term = max c_term old_term} + fun no_vote () = + send(c_id, NO_VOTE); + follower node + fun yes_vote () = + follower (vote_for c_id c_term ({node with term = c_term})) + in + if latestLogIndex > c_log_index + orelse latestLogTerm <> c_log_term + orelse node.term = old_term then no_vote () + else yes_vote () + end, + + (* When receiving a snapshot from a leader in a later or + same term, acknowledge if it contains entries past our + current log index. Update leader and term accordingly. *) + hn ("SNAPSHOT", x, l_id, leader_term) => + let val node = {node with leader = + if node.leader = () orelse node.term < leader_term then l_id + else node.leader} + + val {snapshot, lastIncludedIndex, lastIncludedTerm} = x + val log_term = get_latest_log_term node.log + val log_index = get_log_index node.log + + val accepting = + if leader_term < node.term then false + else if lastIncludedIndex <= log_index then false + else true + + val newlog = if accepting then apply_snapshot x node.log else node.log + val new_sm = if accepting then snapshot else node.state_machine + val reject = + fn () => send (l_id, (REJECT, (p_id, {term = node.term, leader = node.leader}, (get_log_index newlog)))) + val ack = + fn () => send (l_id, (ACKNOWLEDGE, (p_id, get_log_index newlog))) + + val node = { + node with term = (if node.term < leader_term then leader_term else node.term), + state_machine = new_sm, + log = newlog} + in (if accepting then ack () + else reject ()); + follower node + end, + + (* When receiving entries from a leader in a later or + same term, acknowledge if it contains entries past our + current log index. And if the latest log index matches ours. + Update log accordingly.*) + hn ("APPEND_ENTRIES", x, l_id, leader_term, latestLogIndex, prevLogTerm, leaderCommit) => + let val node = {node with leader = + if node.leader = () orelse node.term <= leader_term then l_id + else node.leader} + val accepting = + if leader_term < node.term then false + else if latestLogIndex > (get_log_index node.log) then false + else if (get_latest_log_term node.log) <> prevLogTerm andalso prevLogTerm > 0 then false + else true + val prev_commit = node.log.commitIndex + val newlog = + if accepting then + let val log = rollback_log_to node.log latestLogIndex + val log = add_entries_to_log log x leader_term + in update_commit log (min leaderCommit (get_log_index log)) + end + else node.log + val reject = + fn () => send (l_id, (REJECT, (p_id, {term = node.term, leader = node.leader}, (get_log_index newlog)))) + val ack = + fn () => send (l_id, (ACKNOWLEDGE, (p_id, get_log_index newlog))) + + val node = {node with term = (if node.term < leader_term then leader_term else node.term)} + val (applied_log, new_sm) = apply_log newlog node.state_machine false node.settings.leader_dialer_settings + val snapshot_log = + if prev_commit < applied_log.commitIndex then + evaluate_snapshot_cond new_sm node.snapshot_cond applied_log + else + applied_log + in + (if accepting then ack () + else reject ()); + follower {node with log = snapshot_log, state_machine = new_sm} + end, + + (* If client sends update, sends the leader's id *) + hn (("RAFT_UPDATE", x), dialer_id, _) => + send(dialer_id, (NOT_LEADER, node.leader)); + loop node, + + (* Prints the log *) + hn ("DEBUG_PRINTLOG") => + pretty_print_log node.id node.log; + loop node, + + (* Halts the follower *) + hn ("DEBUG_PAUSE") => + let fun paused () = receive [ + hn ("DEBUG_CONTINUE") => (), + hn _ => paused () + ] + in + paused (); + loop node + end, + + (* Start an election, electing this follower to a candidate *) + hn ("DEBUG_TIMEOUT") => start_election (), + hn _ => loop node + ] + in () + end + in loop node +end +(* END OF ./libs/nodes/follower.trp *) + + (* A node is dormant until it has received the references of all other + dormant_node ({node with all_nodes = append node.all_nodes x}) + ] + else follower node + + (* Defines a default node, being a follower in term 1 without a leader and + the state-machine in its beginning state *) + fun default_node id all_nodes node_amount state_machine settings = + let val node = { + all_nodes = all_nodes, + id = id, + log = empty_log, + term = 1, + voted_for = (), + leader = (), + leader_info = (), + state_machine = case state_machine of + (_, _, _) => state_machine + | _ => ([], WAIT, fn x => x ()), + snapshot_cond = settings.MAXIMUM_LOG_SIZE, + node_amount = node_amount, + serialkeys = [], + settings = settings + } + in dormant_node node + end + + (* Spawn a state-machine on a seperate thread, creates a record*) + fun initiate_node state_machine node_amount id settings = + spawn (fn () => default_node id [] node_amount state_machine settings) + + (* Sends a list of all nodes to all nodes *) + fun add_refs nodes = + map (fn x => send(x, (ADD_NODES, nodes))) nodes + + (* Spawn n nodes*) + fun initiate_nodes n state_machine settings = + let val part_init = initiate_node state_machine n + fun spawn_nodes n acc_id = + case n of + 0 => [] + | x => append + (spawn_nodes (x - 1) (acc_id ^ "I")) + [(part_init acc_id settings)] + + val nodes = spawn_nodes n "I" + in + add_refs nodes; + nodes + end + + (* Spawn a state-machine on some alias *) + fun initiate_distributed_node state_machine node_amount id alias settings = + spawn(alias, fn () => (default_node id [] node_amount state_machine settings)) + + fun initiate_distributed_nodes aliases state_machine settings = + let val part_init = initiate_distributed_node state_machine (length(aliases)) + fun spawn_nodes acc acc_id = + case acc of + [] => [] + | h :: t => + append (spawn_nodes t (acc_id ^ "I")) [part_init acc_id h settings] + val nodes = spawn_nodes aliases "I" + in + add_refs nodes; + nodes + end + + val default_dialer_settings = { + DIALER_NOLEADER_TIMEOUT = 500, + DIALER_NOMSG_TIMEOUT = 2000, + DIALER_SM_BUSY_TIMEOUT = 1000 + } + + val default_local_settings = { + ELECTION_TIMEOUT_LOWER = 2000, + ELECTION_TIMEOUT_UPPER = 4000, + HEARTBEAT_INTERVAL = 500, + TIE_COMMITS_TO_HEARTBEAT = true, + MAXIMUM_LOG_SIZE = 50, + leader_dialer_settings = default_dialer_settings + } + + val default_distributed_settings = { + ELECTION_TIMEOUT_LOWER = default_local_settings.ELECTION_TIMEOUT_LOWER, + ELECTION_TIMEOUT_UPPER = default_local_settings.ELECTION_TIMEOUT_UPPER, + HEARTBEAT_INTERVAL = default_local_settings.HEARTBEAT_INTERVAL, + MAXIMUM_LOG_SIZE = default_local_settings.MAXIMUM_LOG_SIZE, + TIE_COMMITS_TO_HEARTBEAT = false, + leader_dialer_settings = default_local_settings.leader_dialer_settings + } + + + (* Spawns a dialer, dialing into a cluster. *) + fun raft_dial (cluster, client_id, dialer_settings) = + spawn(fn () => dialer cluster client_id dialer_settings) + | raft_dial (cluster, client_id) = raft_dial (cluster, client_id, default_dialer_settings) + + (* Spawns a distributed Raft network, which can be dialed into to + communicate with their state-machines *) + fun raft_spawn_alias (state_machine, aliases, settings) = + initiate_distributed_nodes aliases state_machine settings + | raft_spawn_alias (state_machine, aliases) = + raft_spawn_alias (state_machine, aliases, default_distributed_settings) + + (* Spawns a Raft network, which can be contacted to + communicate with their state-machines *) + fun raft_spawn (state_machine, n, settings) = + initiate_nodes n state_machine settings + | raft_spawn (state_machine, n) = raft_spawn (state_machine, n, default_local_settings) + +in [("raft_spawn_alias", raft_spawn_alias), + ("raft_spawn", raft_spawn), + ("raft_dial", raft_dial), + ("default_dialer_settings", default_dialer_settings), + ("default_distributed_settings", default_distributed_settings), + ("default_local_settings", default_local_settings), + ("WAIT", WAIT), + ("SUS", SUS), + ("DONE", DONE), + ("CLIENT", CLIENT), + ("CLUSTER", CLUSTER), + ("RAFT_UPDATE", RAFT_UPDATE) + ] +end diff --git a/examples/raft-troupe/ids/node1.json b/examples/raft-troupe/ids/node1.json new file mode 100644 index 0000000..0599fa4 --- /dev/null +++ b/examples/raft-troupe/ids/node1.json @@ -0,0 +1 @@ +{"id":"12D3KooWEqiFsAJDFucPu8p77jLkDx1EgpL1UjFCbttiULEvEQRj","privKey":"CAESQFvDsoUA3fjX6MaLEz6a/Dmy6P6hXEJpzJukBNgqaW8ISqK6trERxIBwhwodxr4PzLpmBmRLeR6kgmAdAbnxyo4=","pubKey":"CAESIEqiuraxEcSAcIcKHca+D8y6ZgZkS3kepIJgHQG58cqO"} \ No newline at end of file diff --git a/examples/raft-troupe/ids/node10.json b/examples/raft-troupe/ids/node10.json new file mode 100644 index 0000000..fa5209f --- /dev/null +++ b/examples/raft-troupe/ids/node10.json @@ -0,0 +1 @@ +{"id":"12D3KooWLCW72v8mRZAxa1YNRtVkJrVuCSDmy1yt1wnPB1NH894e","privKey":"CAESQPIoSqWA/mwKsrC6EGsNSLlTVC7bGc7fYPDjVFkmQQKbmkCez4q93iKvA5Kg9x6jCTc901qkQtvLYOElnVQJMSs=","pubKey":"CAESIJpAns+Kvd4irwOSoPceowk3PdNapELby2DhJZ1UCTEr"} \ No newline at end of file diff --git a/examples/raft-troupe/ids/node11.json b/examples/raft-troupe/ids/node11.json new file mode 100644 index 0000000..e183553 --- /dev/null +++ b/examples/raft-troupe/ids/node11.json @@ -0,0 +1 @@ +{"id":"12D3KooWA7Promv3T6AR7BNmaudfjkdtRc3mxkJoqgDCbyhjmoyZ","privKey":"CAESQN3AaNBEuh7sAwFEtc2Wxatdc+9d94pibntKJ/wA3NZABFzJFRkQTU6eTt2SaoHH1L1ONyZncT1oX0X5u6BQD8g=","pubKey":"CAESIARcyRUZEE1Onk7dkmqBx9S9TjcmZ3E9aF9F+bugUA/I"} \ No newline at end of file diff --git a/examples/raft-troupe/ids/node2.json b/examples/raft-troupe/ids/node2.json new file mode 100644 index 0000000..9b1c266 --- /dev/null +++ b/examples/raft-troupe/ids/node2.json @@ -0,0 +1 @@ +{"id":"12D3KooWEvWUngvpYBp4asiHJqM7BpWGzH3ykv9P8JiUDKMyPQV6","privKey":"CAESQM5iZ/6mXpFNeITyfcea10IomRJbHBfJNyNKW/hDSSA3S91RHzp2EOrp8JSypbfpqTLZdx4FyHf9jIc6MeX1D4k=","pubKey":"CAESIEvdUR86dhDq6fCUsqW36aky2XceBch3/YyHOjHl9Q+J"} \ No newline at end of file diff --git a/examples/raft-troupe/ids/node3.json b/examples/raft-troupe/ids/node3.json new file mode 100644 index 0000000..8b5e92e --- /dev/null +++ b/examples/raft-troupe/ids/node3.json @@ -0,0 +1 @@ +{"id":"12D3KooWDWGgz9tm3zg7C6HaqsCaztKtp4QGKD6S7a7TL8u3dkRA","privKey":"CAESQLFt9P4nR+Ru3C/0HjhtWr3VLk/9Gx0R7/CuEdLzTe5tNswZNgKsweIMzBHGiH7BTmUZCm7xEIZ1tqXkxgVBVAU=","pubKey":"CAESIDbMGTYCrMHiDMwRxoh+wU5lGQpu8RCGdbal5MYFQVQF"} \ No newline at end of file diff --git a/examples/raft-troupe/ids/node4.json b/examples/raft-troupe/ids/node4.json new file mode 100644 index 0000000..886ae48 --- /dev/null +++ b/examples/raft-troupe/ids/node4.json @@ -0,0 +1 @@ +{"id":"12D3KooWDpfXNjRh22FFJgUcc4g8FqMxrVTfeNRDST2g56aYRtRE","privKey":"CAESQL69L4ktoU6idz0Nf4eEKt7i/UlSgKOPhQjYiW0tGGdRO4JfMqfJP/mzoYNmkSd0AthidLAXOSt1z+UAPoMMcDk=","pubKey":"CAESIDuCXzKnyT/5s6GDZpEndALYYnSwFzkrdc/lAD6DDHA5"} \ No newline at end of file diff --git a/examples/raft-troupe/ids/node5.json b/examples/raft-troupe/ids/node5.json new file mode 100644 index 0000000..21b72a1 --- /dev/null +++ b/examples/raft-troupe/ids/node5.json @@ -0,0 +1 @@ +{"id":"12D3KooWRQkHssDfdeCNvitXY1rh1HaQ6rHJ67Lx3fkVQQ2puZVG","privKey":"CAESQKiXIXbPr8CIirbAXTgLJVNQl5XxWdZ09iC8Nu3blm/9564Q4evtZYroTdD9f8xgx/qPg/rcmwItV1S+AW4FSdc=","pubKey":"CAESIOeuEOHr7WWK6E3Q/X/MYMf6j4P63JsCLVdUvgFuBUnX"} \ No newline at end of file diff --git a/examples/raft-troupe/ids/node6.json b/examples/raft-troupe/ids/node6.json new file mode 100644 index 0000000..ef78ab0 --- /dev/null +++ b/examples/raft-troupe/ids/node6.json @@ -0,0 +1 @@ +{"id":"12D3KooWBFEAgdHniZbaciJPv62evrZbz78sh69cHw8DNYMWeTTb","privKey":"CAESQEf3AIUgYghpp1YYYPPRxijW7foxzzJ/Kph0QvGV+2EwFTopDHQ9fz8/HLsNTVnsNqw8ZVXP+J2seO0jmT0tVwY=","pubKey":"CAESIBU6KQx0PX8/Pxy7DU1Z7DasPGVVz/idrHjtI5k9LVcG"} \ No newline at end of file diff --git a/examples/raft-troupe/ids/node7.json b/examples/raft-troupe/ids/node7.json new file mode 100644 index 0000000..9f72f3c --- /dev/null +++ b/examples/raft-troupe/ids/node7.json @@ -0,0 +1 @@ +{"id":"12D3KooWHsMaKdPcSwN2pydP7ZX9arQTgAFukCgiFLJ9jgvukkxz","privKey":"CAESQDRsyMlcO7pwbk9daTPKAm5glFSCez009/AxtVcOmCexd6GRGDeUkgsSgw5tRxzPppJSo6gOCNDraAzVHglPApM=","pubKey":"CAESIHehkRg3lJILEoMObUccz6aSUqOoDgjQ62gM1R4JTwKT"} \ No newline at end of file diff --git a/examples/raft-troupe/ids/node8.json b/examples/raft-troupe/ids/node8.json new file mode 100644 index 0000000..8ac4e2f --- /dev/null +++ b/examples/raft-troupe/ids/node8.json @@ -0,0 +1 @@ +{"id":"12D3KooWLNFHoDDHgiTd4QkJ7FNMBC79iY8G1t1Vyr6CNR7ZeCqM","privKey":"CAESQAoHZ3eIP8eH9I4+eKZYwMqK/lbIKJRuQnUUF4PxhP8fnL+tf5brVlQb/6PxiDhDXKJMwINLBAy+RWYfBnuRKog=","pubKey":"CAESIJy/rX+W61ZUG/+j8Yg4Q1yiTMCDSwQMvkVmHwZ7kSqI"} \ No newline at end of file diff --git a/examples/raft-troupe/ids/node9.json b/examples/raft-troupe/ids/node9.json new file mode 100644 index 0000000..98abe7a --- /dev/null +++ b/examples/raft-troupe/ids/node9.json @@ -0,0 +1 @@ +{"id":"12D3KooWCS1nVjpAJPFZrz9DkHHEMTpuXzDaFJh8zt4bzBtod6Jb","privKey":"CAESQBY0X5z/8zKKi73hnhTwPUer7zG+oEP8KeVYJLq6glxRJtk8NOVi/eR0agWhr+hno86fg2qmceMhivWaQCEOxBA=","pubKey":"CAESICbZPDTlYv3kdGoFoa/oZ6POn4NqpnHjIYr1mkAhDsQQ"} \ No newline at end of file diff --git a/examples/raft-troupe/ids/raft-dialer.json b/examples/raft-troupe/ids/raft-dialer.json new file mode 100644 index 0000000..e869275 --- /dev/null +++ b/examples/raft-troupe/ids/raft-dialer.json @@ -0,0 +1 @@ +{"id":"12D3KooWDRik7kYXVNES7GVo9GcujxPttNqwLHoHmdp19wLSVrgc","privKey":"CAESQKznjbctiLLs7yYMt12k6rutIGbhspcHWuLgdDZK/rMyNaGnXUbeVIGaSyvM1gqLfef57ueYBjzMho7qvMJKxi0=","pubKey":"CAESIDWhp11G3lSBmksrzNYKi33n+e7nmAY8zIaO6rzCSsYt"} \ No newline at end of file diff --git a/examples/raft-troupe/implementing-a-raft-based-consensus-algorithm-into-the-troupe-programming-language.pdf b/examples/raft-troupe/implementing-a-raft-based-consensus-algorithm-into-the-troupe-programming-language.pdf new file mode 100644 index 0000000000000000000000000000000000000000..e8e586dac8d5455c1696936778d38e7e3a8f5528 GIT binary patch literal 212006 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%0D?nB{X7GazGH-OGF|my%|P`NNLbIQ)vPutS<14 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 g{~!3xZtf (true, []) + | h :: t => + if h = key then + case (h.key, key.key) of + ((log_index, seq_numb), (new_log_index, new_seq_numb)) => + if new_log_index > log_index orelse + (log_index = new_log_index andalso new_seq_numb > seq_numb) then + (true, ({ h with key = key.key } :: t)) + else (false, h :: t) + | (nonce, new_nonce) => + if nonce <> new_nonce then + (true, ({ h with key = key.key } :: t)) + else (false, h :: t) + | _ => (true, ({ h with key = key.key } :: t)) + else + let val (cond, list) = apply_serialkey t key + in (cond, h :: list) + end + + +(* Used by the dialer to send message to a cluster. If the nodes are busy or if +no leader is present, this function re-sends the message until it is eventually +delivered and acknowledged by the leader of the cluster. If leader is unknown, +it can be defined as unit.*) +fun dialer_send_message p_id msg serial_n leader cluster dialer_settings = + let val nonce = mkuuid() + val msg_timeout = start_timeout (fn() => send(p_id, (DIALER_MESSAGE_TIMEOUT, nonce))) + val busy_timeout = start_timeout (fn() => send(p_id, (DIALER_BUSY_TIMEOUT, nonce))) + fun wait () = + receive [ + hn ("NOT_LEADER", leader_id) => + dialer_send_message p_id msg serial_n leader_id cluster dialer_settings, + hn ("DIALER_ACK", other_serial) when other_serial = serial_n => + leader, + hn ("DIALER_SM_BUSY", other_serial) when other_serial = serial_n => + busy_timeout dialer_settings.DIALER_SM_BUSY_TIMEOUT; + wait (), + hn ("DIALER_SM_DONE", other_serial) when other_serial = serial_n => + leader, + hn ("DIALER_MESSAGE_TIMEOUT", x) => + if x = nonce then dialer_send_message p_id msg serial_n (random_element cluster) cluster dialer_settings + else wait (), + hn ("DIALER_BUSY_TIMEOUT", x) => + if x = nonce then dialer_send_message p_id msg serial_n leader cluster dialer_settings + else wait () + ] + in (case leader of + () => msg_timeout dialer_settings.DIALER_NOLEADER_TIMEOUT + | x => + msg_timeout dialer_settings.DIALER_NOMSG_TIMEOUT; + send(x, msg)); + wait () +end + +(* Facilitates client-side interaction to the Raft cluster. Allows the +programmer to send messages to the cluster in the format (RAFT_UPDATE, msg)*) +fun dialer cluster client_id dialer_settings = + let val p_id = self() + fun update_message x leader = let + val serial_n = mkuuid() + in dialer_send_message p_id ((RAFT_UPDATE, x), p_id, serial_n) serial_n leader cluster dialer_settings + end + val leader = random_element cluster + + fun loop leader sks = + receive [ + hn ("RAFT_UPDATE", x) => + loop (update_message x leader) sks, + + hn ("DIALER_CLIENT_MSG", msg, sk) => + let val (cond, sks) = apply_serialkey sks sk + in + (if cond then send(client_id, msg) + else ()); + loop leader sks + end, + + hn ("SEND_TO_NTH", n, x) => + send_to_nth cluster x n; + loop leader sks, + + hn ("SEND_TO_ALL", x) => + send_to_all cluster x (self()); + loop leader sks, + hn _ => loop leader sks ] + in loop leader [] +end + +(* Temporary dialer sends a list of messages to a cluster before terminating. *) +fun leader_dialer cluster msgs dialer_settings = + let val p_id = self() + val leader = random_element cluster + in + map (fn (msg, serial) => dialer_send_message p_id ((RAFT_UPDATE, msg), p_id, serial) serial leader cluster dialer_settings) msgs +end + +(* Send-function used for clusters to send a message to either the dialer or +client. *) +fun raft_send (process, msgs) dialer_settings = case process.type of +"CLIENT" => map (fn (msg, sk) => send(process.id, (DIALER_CLIENT_MSG, msg, sk))) msgs +| "CLUSTER" => spawn (fn () => leader_dialer process.id msgs dialer_settings) +(* EXPORT END *) \ No newline at end of file diff --git a/examples/raft-troupe/libs/leader-info.trp b/examples/raft-troupe/libs/leader-info.trp new file mode 100644 index 0000000..6d0fb57 --- /dev/null +++ b/examples/raft-troupe/libs/leader-info.trp @@ -0,0 +1,51 @@ +let + (* EXPORT START *) + + (* Generates a default leader info, with the nextIndex of all followers + being the nextIndex of the new leader. This can be changed with followers + rejecting AppendEntries *) + fun new_leader all_nodes log = + let val nextIndex = get_log_index log + val index = map (fn id => {peer = id, next = nextIndex + 1}) all_nodes + val match_index = map (fn id => {peer = id, match = 0}) all_nodes + in { + nextIndex = index, + matchIndex = match_index + } end + + (* Get the nextIndex of a peer *) + fun get_next_index leader_info peer = first (filter (fn (x) => x.peer = peer) leader_info.nextIndex) + + (* Get the matchIndex of a peer *) + fun get_match_index leader_info peer = first (filter (fn (x) => x.peer = peer) leader_info.matchIndex) + + (* Updates a cluster member's next-index. This is done after an + acknowledgement or rejection. *) + fun update_next_index leader_info peer new = let + val prevIndex = get_next_index leader_info peer + val newIndex = {peer = peer, next = new} + val withoutPeer = filter (fn (x) => x.peer <> peer) leader_info.nextIndex + in { + leader_info with + nextIndex = newIndex :: withoutPeer + } end + + (* Updates a cluster member's match-index, denoting how much of their log + matches the leader holding the leader info. *) + fun update_match_index leader_info peer new = let + val prevIndex = get_match_index leader_info peer + val newIndex = {peer = peer, match = new} + val withoutPeer = filter (fn (x) => x.peer <> peer) leader_info.matchIndex + in { + leader_info with + matchIndex = newIndex :: withoutPeer + } end + + (* Get all follower's matchIndex*) + fun get_matches leader_info = map (fn x => x.match) leader_info.matchIndex + + (* Get the highest index of entries that a majority of followers have + appended to, by finding the median *) + fun calc_highest_commit matches = median matches + (* EXPORT END *) +in () end diff --git a/examples/raft-troupe/libs/log.trp b/examples/raft-troupe/libs/log.trp new file mode 100644 index 0000000..b99e5c5 --- /dev/null +++ b/examples/raft-troupe/libs/log.trp @@ -0,0 +1,159 @@ + (* EXPORT START *) + (* Creates a snapshot. *) + fun set_snapshot snapshot index term = { + snapshot = snapshot, + lastIncludedIndex = index, + lastIncludedTerm = term + } + + (* Creates a default, empty snapshot. *) + val empty_snapshot = { + snapshot = (), + lastIncludedIndex = 0, + lastIncludedTerm = 0 + } + + (* A default, empty log. *) + val empty_log = { + log = [], + snapshot = empty_snapshot, + lastApplied = 0, + commitIndex = 0, + lastMessageSerial = "" + } + + fun pretty_print_log id log = + (* Disabled for library *) + (* printString "\n========******========"; + print (length log.log); + printString ("ID: "^id); + printString "----------------------"; + printString "Entries (term, message):"; + map (fn x => print (x.term, x.command)) log.log; + printString "----------------------"; + printString "CommitIndex:"; + print log.commitIndex; + printString "LastApplied:"; + print log.lastApplied; + printString "----------------------"; + printString "Snapshot:"; + print log.snapshot; + printString "========******========\n";*) + () + + (* Appends a message to the log, and notes the message's serial number. *) + fun append_message log message callback term serial = + let val new_entry = { + term = term, + command = message, + callback = callback, + serial = serial + } + in { + log with + lastMessageSerial = serial, + log = new_entry :: log.log + } + end + + + (* Appends a list of message to the log. *) + fun add_entries_to_log log entries term = + case entries of + [] => log + | h :: t => + add_entries_to_log (append_message log h.command h.callback term h.serial) t + h.term + + (* Updates the lastApplied-index. *) + fun update_applied log = { + log with + lastApplied = log.lastApplied + 1 + } + + (* Commits a message in the log. *) + fun update_commit log new_index = { + log with + commitIndex = (max new_index log.commitIndex) + } + + (* Rolls the log back one entry. *) + fun rollback_log log = + let val loglog = log.log + in case loglog of + (_ :: prev_log) => { + log with + log = prev_log + } + | [] => {log with log = []} + end + + (* Get the entry of the latest log entry. *) + fun get_log_index log = (length log.log) + log.snapshot.lastIncludedIndex + + (*Determines whether or not all log changes have been committed. *) + fun log_is_committed log = (get_log_index log = log.commitIndex) + + (* Rolls the log back n time. *) + fun rollback_log_to log n = + if n < (get_log_index log) then + let val log = rollback_log log + in (rollback_log_to log n) + end + else log + + (* Get the term of the latest entry of the log, or, if empty, the last + included index of the snapshot. *) + fun get_latest_entry_term log = + case log.log of + [] => log.snapshot.lastIncludedTerm + | h :: _ => h.term + + (* Get the term of the latest log entry. *) + fun get_latest_log_term log = get_latest_entry_term log + + (* Get the message of the latest log entry. *) + fun get_latest_log_command log = + case log.log of + [] => 0 (* Should not be reachable. *) + | h :: _ => h.command + + fun get_nth_command log index = nth (reverse log.log) (index - log.snapshot.lastIncludedIndex) + + (* Returns a slice of all entries after log-index n. *) + fun get_commands_after_nth entries n last_included = + let val log_slice = slice (n - last_included) (length entries) (reverse entries) + in log_slice + end + + (* Get a snapshot of all committed entries. *) + fun get_snapshot state log = + if log.commitIndex > 0 andalso + (log.commitIndex - log.snapshot.lastIncludedIndex) <= length log.log then + let val lastCommitted = get_nth_command log log.commitIndex + in set_snapshot state log.commitIndex lastCommitted.term end + else empty_snapshot + + + (* Applies a snapshot to the log. *) + fun apply_snapshot snapshot log = + let val newCommitIndex = + if log.commitIndex < snapshot.lastIncludedIndex then snapshot.lastIncludedIndex + else log.commitIndex + val uncommitted_entries = get_commands_after_nth log.log newCommitIndex log.snapshot.lastIncludedIndex + val newLastApplied = + if log.lastApplied < snapshot.lastIncludedIndex then snapshot.lastIncludedIndex + else log.lastApplied + in { log with + log = uncommitted_entries, + commitIndex = newCommitIndex, + lastApplied = newLastApplied, + snapshot = snapshot } + end + + (* Asks the state-machine whether or not to snapshot. *) + fun evaluate_snapshot_cond state snapshot_cond log = + if (log.lastApplied - log.snapshot.lastIncludedIndex) > snapshot_cond then + apply_snapshot (get_snapshot state log) log + else log + (* EXPORT END *) diff --git a/examples/raft-troupe/libs/nodes/candidate.trp b/examples/raft-troupe/libs/nodes/candidate.trp new file mode 100644 index 0000000..cca1e06 --- /dev/null +++ b/examples/raft-troupe/libs/nodes/candidate.trp @@ -0,0 +1,70 @@ +(* EXPORT START *) +and candidate node = + let val p_id = self() + + (* A candidate cannot vote for anyone and has no leader *) + val node = {node with voted_for = (), leader = ()} + val nonce = mkuuid() + + (* Sends a vote request to all followers *) + val latestLogIndex = get_log_index node.log + val prevLogTerm = get_latest_log_term node.log + + + (* Becoming a leader requires majority vote *) + val req_votes = ((length node.all_nodes) / 2) + + fun won_election () = + let val (sides, _, _) = node.state_machine + in + send_sides node.log sides; + leader_node ({ + node with leader_info = (new_leader node.all_nodes node.log), + leader = (p_id)}) + end + + fun wait_for_votes (follower_votes, vote_amount) = + let + fun loop () = receive [ + (* Received a vote from a follower we have not already + received a vote from *) + hn ("YES_VOTE", follower_id) when (not (contains follower_id follower_votes)) => + wait_for_votes ((append follower_votes [follower_id]), vote_amount + 1), + + (*We received a NO_VOTE from a follower in a later term. + This can only happen if there is a leader/candidate in this + term, and as such, we convert to a follower *) + hn ("NO_VOTE", other_term) when other_term > node.term => + follower node, + + (* Received vote request from candidate in later term *) + hn ("REQUEST_VOTE", (c_term, other_c_id, c_log_index, c_log_term)) when c_term > node.term => + send(other_c_id, (YES_VOTE, node.id)); + follower ({ node with term = c_term, voted_for = other_c_id}), + + (* Received message from leader in a term at least as + up-to-date as ours. Because of this, we must have lost the + election *) + hn ("APPEND_ENTRIES", x, l_id, other_term, prevIndex, prevTerm, commitIndex) when other_term >= node.term => + follower ({ node with leader = l_id}), + + (* Election timeout, send out another request vote *) + hn ("VOTE_TIMEOUT", x) when x = nonce => candidate {node with term = node.term + 1}, + + (* Halts the candidate *) + hn ("DEBUG_PAUSE") => + let fun loop () = receive [ + hn ("DEBUG_CONTINUE") => (), + hn x => loop () + ] + in loop () end, + hn _ => loop () + ] + in if vote_amount >= req_votes then won_election () else loop () + end + in + send_to_all node.all_nodes (REQUEST_VOTE, (node.term, p_id, latestLogIndex, prevLogTerm)) (p_id); + start_random_timeout (fn () => send(p_id, (VOTE_TIMEOUT, nonce))) node.settings; + wait_for_votes ([node.id], 1) +end +(* EXPORT END *) \ No newline at end of file diff --git a/examples/raft-troupe/libs/nodes/follower.trp b/examples/raft-troupe/libs/nodes/follower.trp new file mode 100644 index 0000000..dffb569 --- /dev/null +++ b/examples/raft-troupe/libs/nodes/follower.trp @@ -0,0 +1,142 @@ +(* EXPORT START *) +and follower node = + let val nonce = mkuuid() + val p_id = self() + val _ = start_random_timeout (fn () => send(p_id, (ELECTION_TIMEOUT, nonce))) node.settings + (* Sends a YES_VOTE to a candidate *) + fun vote_for c_id c_term node = + send(c_id, (YES_VOTE, node.id)); + { node with term = c_term, voted_for = c_id } + fun loop node = let + fun start_election () = + candidate ({node with term = node.term + 1}) + val _ = receive [ + (* Starts an election *) + hn ("ELECTION_TIMEOUT", x) when x = nonce => start_election (), + + (* Sends a re-vote to a candidate we already voted for *) + hn ("REQUEST_VOTE", (c_term, c_id, c_log_index, c_log_term)) when c_id = node.voted_for => + follower (vote_for c_id c_term node), + + (* If we receive a vote request, vote yes if: the log is a + up-to-date and the term of the candidate is later than our + current. Vote no otherwise *) + hn ("REQUEST_VOTE", (c_term, c_id, c_log_index, c_log_term)) => + let val latestLogIndex = get_log_index node.log + val latestLogTerm = get_latest_log_term node.log + val old_term = node.term + val node = {node with term = max c_term old_term} + fun no_vote () = + send(c_id, NO_VOTE); + follower node + fun yes_vote () = + follower (vote_for c_id c_term ({node with term = c_term})) + in + if latestLogIndex > c_log_index + orelse latestLogTerm <> c_log_term + orelse node.term = old_term then no_vote () + else yes_vote () + end, + + (* When receiving a snapshot from a leader in a later or + same term, acknowledge if it contains entries past our + current log index. Update leader and term accordingly. *) + hn ("SNAPSHOT", x, l_id, leader_term) => + let val node = {node with leader = + if node.leader = () orelse node.term < leader_term then l_id + else node.leader} + + val {snapshot, lastIncludedIndex, lastIncludedTerm} = x + val log_term = get_latest_log_term node.log + val log_index = get_log_index node.log + + val accepting = + if leader_term < node.term then false + else if lastIncludedIndex <= log_index then false + else true + + val newlog = if accepting then apply_snapshot x node.log else node.log + val new_sm = if accepting then snapshot else node.state_machine + val reject = + fn () => send (l_id, (REJECT, (p_id, {term = node.term, leader = node.leader}, (get_log_index newlog)))) + val ack = + fn () => send (l_id, (ACKNOWLEDGE, (p_id, get_log_index newlog))) + + val node = { + node with term = (if node.term < leader_term then leader_term else node.term), + state_machine = new_sm, + log = newlog} + in (if accepting then ack () + else reject ()); + follower node + end, + + (* When receiving entries from a leader in a later or + same term, acknowledge if it contains entries past our + current log index. And if the latest log index matches ours. + Update log accordingly.*) + hn ("APPEND_ENTRIES", x, l_id, leader_term, latestLogIndex, prevLogTerm, leaderCommit) => + let val node = {node with leader = + if node.leader = () orelse node.term <= leader_term then l_id + else node.leader} + val accepting = + if leader_term < node.term then false + else if latestLogIndex > (get_log_index node.log) then false + else if (get_latest_log_term node.log) <> prevLogTerm andalso prevLogTerm > 0 then false + else true + val prev_commit = node.log.commitIndex + val newlog = + if accepting then + let val log = rollback_log_to node.log latestLogIndex + val log = add_entries_to_log log x leader_term + in update_commit log (min leaderCommit (get_log_index log)) + end + else node.log + val reject = + fn () => send (l_id, (REJECT, (p_id, {term = node.term, leader = node.leader}, (get_log_index newlog)))) + val ack = + fn () => send (l_id, (ACKNOWLEDGE, (p_id, get_log_index newlog))) + + val node = {node with term = (if node.term < leader_term then leader_term else node.term)} + val (applied_log, new_sm) = apply_log newlog node.state_machine false node.settings.leader_dialer_settings + val snapshot_log = + if prev_commit < applied_log.commitIndex then + evaluate_snapshot_cond new_sm node.snapshot_cond applied_log + else + applied_log + in + (if accepting then ack () + else reject ()); + follower {node with log = snapshot_log, state_machine = new_sm} + end, + + (* If client sends update, sends the leader's id *) + hn (("RAFT_UPDATE", x), dialer_id, _) => + send(dialer_id, (NOT_LEADER, node.leader)); + loop node, + + (* Prints the log *) + hn ("DEBUG_PRINTLOG") => + pretty_print_log node.id node.log; + loop node, + + (* Halts the follower *) + hn ("DEBUG_PAUSE") => + let fun paused () = receive [ + hn ("DEBUG_CONTINUE") => (), + hn _ => paused () + ] + in + paused (); + loop node + end, + + (* Start an election, electing this follower to a candidate *) + hn ("DEBUG_TIMEOUT") => start_election (), + hn _ => loop node + ] + in () + end + in loop node +end +(* EXPORT END *) \ No newline at end of file diff --git a/examples/raft-troupe/libs/nodes/leader.trp b/examples/raft-troupe/libs/nodes/leader.trp new file mode 100644 index 0000000..b57874b --- /dev/null +++ b/examples/raft-troupe/libs/nodes/leader.trp @@ -0,0 +1,164 @@ +(* EXPORT START *) +fun leader_node node = + let val p_id = self() + (* Appends appends all entries from a follower's nextIndex to the leader's log index*) + fun append_entries node follower_pid = + let val nextIndex = get_next_index node.leader_info follower_pid + val logIndex = get_log_index node.log + in if logIndex + 1 >= nextIndex.next then + let + val latestLogIndex = nextIndex.next - 1 + in + (* Sends the snapshot if the followers nextIndex is before the Snapshot's lastIncludedIndex *) + if nextIndex.next <= node.log.snapshot.lastIncludedIndex + then send(follower_pid, (SNAPSHOT, node.log.snapshot, p_id, node.term)) + else + let val entries = get_commands_after_nth node.log.log latestLogIndex node.log.snapshot.lastIncludedIndex + val afterSnapshot = latestLogIndex - node.log.snapshot.lastIncludedIndex + val prevEntryTerm = + if afterSnapshot > 0 then (get_nth_command node.log latestLogIndex).term + else node.log.snapshot.lastIncludedTerm + in send(follower_pid, (APPEND_ENTRIES, entries, p_id, node.term, latestLogIndex, prevEntryTerm, node.log.commitIndex)) + end + end + (* A follower should never get more entries than the leader *) + else () + end + + (* Convert leader to follower *) + fun demote term leader voted_for node = + {node with + term = term, + leader = leader, + leader_info = (), + voted_for = voted_for} + + fun append_update node msg callback serial = + let val latestLogIndex = get_log_index node.log + val prevLogTerm = get_latest_log_term node.log + val log = append_message node.log msg callback node.term serial + val leader_info = update_match_index node.leader_info p_id (get_log_index log) + val leader_info = update_next_index leader_info p_id ((get_log_index log) + 1) + val node = {node with log = log, leader_info = leader_info} + in node + end + + (* Applies all committed log entries that have not already been applied *) + fun apply_committed node = + let val prev_commit = node.log.commitIndex + val highest_commit = calc_highest_commit (map (fn x => x.match) node.leader_info.matchIndex) + val node = { node with log = update_commit node.log highest_commit } + val (applied_log, new_sm) = apply_log node.log node.state_machine true node.settings.leader_dialer_settings + val snapshot_log = + if prev_commit < highest_commit then + evaluate_snapshot_cond new_sm node.snapshot_cond applied_log + else + applied_log + val (_, status, _) = new_sm + val node = { node with log = snapshot_log, state_machine = new_sm} + in + case status of + "SUS" => if log_is_committed node.log then append_update node () (fn () => ()) (mkuuid()) + else node + | _ => node + end + + val nonce = mkuuid () + + fun loop node = + receive [ + (* Halts the leader *) + hn "DEBUG_PAUSE" => + let fun pause () = receive [ + hn ("DEBUG_CONTINUE") => loop node, + hn x => pause () + ] + in pause () end, + + hn ("SEND_HEARTBEAT", x) when nonce = x => + leader_node node, + + (* Message has not been appended before *) + hn (("RAFT_UPDATE", x), dialer_id, serial_n) => + let val (cond, sks) = apply_serialkey node.serialkeys serial_n + in if cond then let + val (_, stat, _) = node.state_machine + val node = case stat of + "SUS" => send(dialer_id, (DIALER_SM_BUSY, serial_n)); node + | "DONE" => send(dialer_id, (DIALER_SM_DONE, serial_n)); node + | "WAIT" => + if log_is_committed node.log then + let fun replication_cb () = send (dialer_id, (DIALER_ACK, serial_n)) + in append_update node x replication_cb serial_n end + else send(dialer_id, (DIALER_SM_BUSY, serial_n)); node + in leader_node node end + else send(dialer_id, (DIALER_ACK, serial_n)); + loop node + end, + + (* If append is successful on a follower*) + hn ("ACKNOWLEDGE", (peer, logIndex)) => + let val prev_index = get_log_index node.log + val node = { node with leader_info = update_match_index node.leader_info peer logIndex } + val node = { node with leader_info = update_next_index node.leader_info peer (logIndex + 1) } + val node = apply_committed node + val next_index = get_next_index node.leader_info peer + in (if not node.settings.TIE_COMMITS_TO_HEARTBEAT andalso + prev_index < get_log_index node.log then + map (fn x => append_entries node x) + (filter (fn x => + let val next_index = get_next_index node.leader_info x + in x <> p_id andalso next_index.next > logIndex end) node.all_nodes) + else ()); + loop node + end, + + (* If append is unsuccessful *) + hn ("REJECT", (peer, terminfo, logIndex)) => + if node.term >= terminfo.term then + let val node = { node with leader_info = update_next_index node.leader_info peer (logIndex + 1) } + in loop node + end + else follower (demote terminfo.term terminfo.leader () + node), + + (* If another node has been elected as a candidate, and + their term is in front of ours, convert to a follower *) + hn ("REQUEST_VOTE", (c_term, c_id, c_log_index, c_log_term)) when c_term > node.term => + send(c_id, (YES_VOTE, node.id)); + follower (demote c_term () c_id node), + + hn ("REQUEST_VOTE", (c_term, c_id, c_log_index, c_log_term)) => + send(c_id, (NO_VOTE, node.id)); + loop ({node with term = max c_term node.term}), + + (* If we receive snapshot from a leader in a higher term, + convert to follower *) + hn ("SNAPSHOT", snapshot, l_id, other_term) when other_term > node.term => follower (demote other_term l_id () node), + + (* If we receive AppendEntries from a leader in a higher term, + convert to follower *) + hn ("APPEND_ENTRIES", x, l_id, other_term, prevIndex, prevTerm, commitIndex) when other_term > node.term => follower (demote other_term l_id () node), + + (* Prints log *) + hn "DEBUG_PRINTLOG" => + pretty_print_log node.id node.log; + loop node, + + (* Applies a snapshot *) + hn "DEBUG_APPLYSNAPSHOT" => + let + val snapshot = get_snapshot node.state_machine node.log + val node = case snapshot.snapshot of + () => node + | _ => {node with log = apply_snapshot snapshot node.log} + in loop node end, + hn _ => loop node + ] + in + (* Append entries for each follower *) + map (fn x => append_entries node x) (filter (fn x => x <> p_id) node.all_nodes); + start_timeout (fn () => send (p_id, (SEND_HEARTBEAT, nonce))) node.settings.HEARTBEAT_INTERVAL; + loop node +end +(* EXPORT END *) \ No newline at end of file diff --git a/examples/raft-troupe/libs/quickselect.trp b/examples/raft-troupe/libs/quickselect.trp new file mode 100644 index 0000000..9cf6c62 --- /dev/null +++ b/examples/raft-troupe/libs/quickselect.trp @@ -0,0 +1,25 @@ +import lists +let (* EXPORT START *) + fun is_even i = i mod 2 = 0 + + (* Using QuickSelect, finds the kth element of a list. *) + fun quickselect list k = + case list of + [] => "ERROR: Empty list" + | h :: t => + let val (ys, zs) = partition (fn x => x > h) t + val l = length ys + in + if k < l then quickselect ys k + else if k > l then quickselect zs (k-l-1) + else h + end + + (* Returns the median of a list. *) + fun median list = + let val len = length list + val middle = if is_even len then len / 2 - 1 else (len - 1) / 2 + in quickselect list (middle) + end + (* EXPORT END *) +end \ No newline at end of file diff --git a/examples/raft-troupe/libs/state_machines/fib.trp b/examples/raft-troupe/libs/state_machines/fib.trp new file mode 100644 index 0000000..6008abe --- /dev/null +++ b/examples/raft-troupe/libs/state_machines/fib.trp @@ -0,0 +1,27 @@ +datatype Atoms = WAIT|SUS|DONE + +(* EXPORT START *) + +(* Calculates the nth Fibonacci number*) +fun fib_raft cb n = + (* O(n) recursive solution*) + let fun loop cb n a b = case n of + (* Send result of recursive loop to cb *) + 1 => ([(cb, b)], WAIT, fib_input) + (* Add suspended states to ensure computation when doing recursion*) + | n => + ([], SUS, fn () => + let val n = n - 1 + in ([], SUS, fn () => loop cb n b (a + b)) end) + + in if n <= 1 then ([(cb, n)], WAIT, fib_input) + else loop cb n 0 1 +end + +and fib_input x = case x of +(* Compute the nth fibonacci number if input matches *) +(callback, n) => fib_raft callback n +(* Ignores otherwise*) +| _ => ([], WAIT, fib_input) + +(* EXPORT END *) diff --git a/examples/raft-troupe/libs/state_machines/keyval-cps.trp b/examples/raft-troupe/libs/state_machines/keyval-cps.trp new file mode 100644 index 0000000..2a6e8a0 --- /dev/null +++ b/examples/raft-troupe/libs/state_machines/keyval-cps.trp @@ -0,0 +1,30 @@ +import lists + +datatype Atoms = WAIT|SUS|DONE + +let (* EXPORT START *) + fun keyval () = + let + + fun main store x = case x of + ("GET", key, callback) => ( + [(callback, first (filter (fn (x,_) => x key) store))], + WAIT, + main store + ) + + | ("SET", (key, value)) => ( + [], + WAIT, + main ((key, value) :: store) + ) + + | _ => ([], WAIT, main store) + + in ([], WAIT, main []) + end (* EXPORT END *) + +in test () +end + + diff --git a/examples/raft-troupe/libs/state_machines/ping.trp b/examples/raft-troupe/libs/state_machines/ping.trp new file mode 100644 index 0000000..bf6676e --- /dev/null +++ b/examples/raft-troupe/libs/state_machines/ping.trp @@ -0,0 +1,16 @@ +import lists +datatype Atoms = WAIT|SUS|DONE + +let (* EXPORT START *) + + fun main cluster x = case x of + (callback, x) => + ([(callback, (cluster, x + 1))], WAIT, main cluster) + |_ => ([], WAIT, main cluster) + + fun ping_server cluster = + ([], WAIT, main cluster) + + (* EXPORT END *) +in () +end diff --git a/examples/raft-troupe/node.trp b/examples/raft-troupe/node.trp new file mode 100755 index 0000000..a7bcd7f --- /dev/null +++ b/examples/raft-troupe/node.trp @@ -0,0 +1,324 @@ +import lists + +(* + Log = { + snapshot: Snapshot + log: Entry[], + lastApplied: int, + internalChanges: int, + commitIndex: int, + latestSerials: SerialKey[] + } + Snapshot = { + snapshot: Some state + lastIncludedIndex: int, + lastIncludedTerm: int + } + Entry = { + term: int, + command: message, + serial: string + } + SerialKey = { + id: clusterId[] | pid, + key: (logIndex, number) | nonce + } +*) + +(* + LeaderInfo = { + nextIndex = { + peer: p, + next: int + }[], + matchIndex = { + peer: p, + match: int + }[] + } +*) + +(* + StateMachine = { + set_hook : fn (x: string) => x + get_hook : fn (x: string, callback_pid: string) => x + get_snapshot_hook : fn(callback_pid: string) => x + get_changes_hook : fn (callback_pid: string) => x + snapshot_condition_hook : fn (log_summary: LogSummary, callback_pid: string) => x: bool + } + LogSummary = { + log_size: int, + entries_since_snap: int + } +*) + +(* + Node = { + all_nodes: string[], + id: string, + log: Log, + term: int, + voted_for: string, + leader: string, + leader_info: LeaderInfo, + snapshot_condition: fn logSummary => ... : boolean + state_machine: ([SIDE-EFFECTS], STATUS, STEP-FUNC) + total_nodes: int, + verbose: boolean + } +*) + +(* + RaftProcesses = { + type: Client | Cluster, + id: pid | Clusterid[] + } +*) + +let + (* Constants *) + val WAIT = "WAIT" + val SUS = "SUS" + val DONE = "DONE" + val SEND_HEARTBEAT = "SEND_HEARTBEAT" + val RAFT_UPDATE = "RAFT_UPDATE" + val NOT_LEADER = "NOT_LEADER" + val ACKNOWLEDGE = "ACKNOWLEDGE" + val REJECT = "REJECT" + val ELECTION_TIMEOUT = "ELECTION_TIMEOUT" + val REQUEST_VOTE = "REQUEST_VOTE" + val YES_VOTE = "YES_VOTE" + val NO_VOTE = "NO_VOTE" + val VOTE_TIMEOUT = "VOTE_TIMEOUT" + val APPEND_ENTRIES = "APPEND_ENTRIES" + val SNAPSHOT = "SNAPSHOT" + val ADD_NODES = "ADD_NODES" + val DIAL = "DIAL" + val DIALER_ACK = "DIALER_ACK" + val DIALER_SM_BUSY = "DIALER_SM_BUSY" + val DIALER_SM_DONE = "DIALER_SM_DONE" + val DIALER_CLIENT_MSG = "DIALER_CLIENT_MSG" + val DIALER_MESSAGE_TIMEOUT = "DIALER_MESSAGE_TIMEOUT" + val DIALER_BUSY_TIMEOUT = "DIALER_BUSY_TIMEOUT" + val SEND_TO_NTH = "SEND_TO_NTH" + val SEND_TO_ALL = "SEND_TO_ALL" + val DEBUG_PRINTLOG = "DEBUG_PRINTLOG" + val DEBUG_PAUSE = "DEBUG_PAUSE" + val DEBUG_CONTINUE = "DEBUG_CONTINUE" + val DEBUG_APPLYSNAPSHOT = "DEBUG_APPLYSNAPSHOT" + val DEBUG_SNAPSHOT_COND = "DEBUG_SNAPSHOT_COND" + val DEBUG_TIMEOUT = "DEBUG_TIMEOUT" + val FUNCTION_DONE = "FUNCTION_DONE" + val ERROR_TIMEOUT = "ERROR_TIMEOUT" + val CLUSTER = "CLUSTER" + val CLIENT = "CLIENT" + + fun not a = a = false + val contains = elem + fun send_to_all processes msg sender = map (fn x => send(x, msg)) (filter (fn x => x <> sender) processes) + + fun send_to_nth processes msg n = send((nth (reverse processes) n), msg) + + fun max a b = if a < b then b else a + + fun min a b = if a > b then b else a + + (* #IMPORT libs/quickselect.trp *) + + (* #IMPORT libs/log.trp *) + + (* #IMPORT libs/leader-info.trp *) + + (* Executes a function after a given timeout. *) + fun start_timeout func duration = + let fun timeout () = + let val time = duration + val _ = sleep time + in func () + end + val p_id = self() + in spawn timeout + end + + (* Send message after a delay. *) + fun send_delay (to, m) delay = + sleep delay; + send (to, m) + + (* Starts a random timeout with lower=2sec and upper=4sec *) + fun start_random_timeout func settings = start_timeout func (settings.ELECTION_TIMEOUT_LOWER + ((random ()) * (settings.ELECTION_TIMEOUT_UPPER - settings.ELECTION_TIMEOUT_LOWER))) + + (* #IMPORT ./libs/dialer.trp *) + + (* Send the side-effect-messages to dialers or clusters *) + fun send_sides log sides dialer_settings = + (* Add message to key-value-store, sorting by the recipients. *) + let fun add_msg id msg sk dict = case dict of + [] => [(id, [(msg, sk)])] + | (other_id, msgs) :: t => + if id = other_id then + (id, (msg, sk) :: t) + else (other_id, msgs) :: add_msg id msg sk t + (* Generate key-value-store of all message, sorting by recipients. *) + val (sorted_msgs, _) = case sides of + [] => ([], 0) + | x => foldl (fn ((callback, msg), (acc, seq)) => + (add_msg callback msg ({ id = callback, key = (log.lastApplied, seq)}) acc, seq + 1) + ) ([], 1) x + (* Sends all messages. *) + in map (fn x => raft_send x dialer_settings) sorted_msgs + end + + (* Applies all log-entries that have been committed, but not applied *) + fun apply_log log state_machine is_leader dialer_settings = + (* If any non-applied, committed logs apply... *) + if log.lastApplied < log.commitIndex then + (* Get the latest non-applied committed entry *) + let val entry = get_nth_command log (log.lastApplied + 1) + val command = entry.command + (* Update log to apply entry and apply entry on state-machine*) + val log = update_applied log + val (sides, status, step) = state_machine + val (new_sides, new_status, new_step) = step command + (* If leader is applying, execute side-effects. *) + in (if is_leader then + entry.callback (); + send_sides log new_sides dialer_settings + else ()); + apply_log log (new_sides, new_status, new_step) is_leader dialer_settings end + else (log, state_machine) + + (* #IMPORT ./libs/nodes/leader.trp *) + (* #IMPORT ./libs/nodes/candidate.trp *) + (* #IMPORT ./libs/nodes/follower.trp *) + + (* A node is dormant until it has received the references of all other + dormant_node ({node with all_nodes = append node.all_nodes x}) + ] + else follower node + + (* Defines a default node, being a follower in term 1 without a leader and + the state-machine in its beginning state *) + fun default_node id all_nodes node_amount state_machine settings = + let val node = { + all_nodes = all_nodes, + id = id, + log = empty_log, + term = 1, + voted_for = (), + leader = (), + leader_info = (), + state_machine = case state_machine of + (_, _, _) => state_machine + | _ => ([], WAIT, fn x => x ()), + snapshot_cond = settings.MAXIMUM_LOG_SIZE, + node_amount = node_amount, + serialkeys = [], + settings = settings + } + in dormant_node node + end + + (* Spawn a state-machine on a seperate thread, creates a record*) + fun initiate_node state_machine node_amount id settings = + spawn (fn () => default_node id [] node_amount state_machine settings) + + (* Sends a list of all nodes to all nodes *) + fun add_refs nodes = + map (fn x => send(x, (ADD_NODES, nodes))) nodes + + (* Spawn n nodes*) + fun initiate_nodes n state_machine settings = + let val part_init = initiate_node state_machine n + fun spawn_nodes n acc_id = + case n of + 0 => [] + | x => append + (spawn_nodes (x - 1) (acc_id ^ "I")) + [(part_init acc_id settings)] + + val nodes = spawn_nodes n "I" + in + add_refs nodes; + nodes + end + + (* Spawn a state-machine on some alias *) + fun initiate_distributed_node state_machine node_amount id alias settings = + spawn(alias, fn () => (default_node id [] node_amount state_machine settings)) + + fun initiate_distributed_nodes aliases state_machine settings = + let val part_init = initiate_distributed_node state_machine (length(aliases)) + fun spawn_nodes acc acc_id = + case acc of + [] => [] + | h :: t => + append (spawn_nodes t (acc_id ^ "I")) [part_init acc_id h settings] + val nodes = spawn_nodes aliases "I" + in + add_refs nodes; + nodes + end + + val default_dialer_settings = { + DIALER_NOLEADER_TIMEOUT = 500, + DIALER_NOMSG_TIMEOUT = 2000, + DIALER_SM_BUSY_TIMEOUT = 1000 + } + + val default_local_settings = { + ELECTION_TIMEOUT_LOWER = 2000, + ELECTION_TIMEOUT_UPPER = 4000, + HEARTBEAT_INTERVAL = 500, + TIE_COMMITS_TO_HEARTBEAT = true, + MAXIMUM_LOG_SIZE = 50, + leader_dialer_settings = default_dialer_settings + } + + val default_distributed_settings = { + ELECTION_TIMEOUT_LOWER = default_local_settings.ELECTION_TIMEOUT_LOWER, + ELECTION_TIMEOUT_UPPER = default_local_settings.ELECTION_TIMEOUT_UPPER, + HEARTBEAT_INTERVAL = default_local_settings.HEARTBEAT_INTERVAL, + MAXIMUM_LOG_SIZE = default_local_settings.MAXIMUM_LOG_SIZE, + TIE_COMMITS_TO_HEARTBEAT = false, + leader_dialer_settings = default_local_settings.leader_dialer_settings + } + + + (* Spawns a dialer, dialing into a cluster. *) + fun raft_dial (cluster, client_id, dialer_settings) = + spawn(fn () => dialer cluster client_id dialer_settings) + | raft_dial (cluster, client_id) = raft_dial (cluster, client_id, default_dialer_settings) + + (* Spawns a distributed Raft network, which can be dialed into to + communicate with their state-machines *) + fun raft_spawn_alias (state_machine, aliases, settings) = + initiate_distributed_nodes aliases state_machine settings + | raft_spawn_alias (state_machine, aliases) = + raft_spawn_alias (state_machine, aliases, default_distributed_settings) + + (* Spawns a Raft network, which can be contacted to + communicate with their state-machines *) + fun raft_spawn (state_machine, n, settings) = + initiate_nodes n state_machine settings + | raft_spawn (state_machine, n) = raft_spawn (state_machine, n, default_local_settings) + +in [("raft_spawn_alias", raft_spawn_alias), + ("raft_spawn", raft_spawn), + ("raft_dial", raft_dial), + ("default_dialer_settings", default_dialer_settings), + ("default_distributed_settings", default_distributed_settings), + ("default_local_settings", default_local_settings), + ("WAIT", WAIT), + ("SUS", SUS), + ("DONE", DONE), + ("CLIENT", CLIENT), + ("CLUSTER", CLUSTER), + ("RAFT_UPDATE", RAFT_UPDATE) + ] +end diff --git a/examples/raft-troupe/tests/bad_actor.trp b/examples/raft-troupe/tests/bad_actor.trp new file mode 100644 index 0000000..280687f --- /dev/null +++ b/examples/raft-troupe/tests/bad_actor.trp @@ -0,0 +1,52 @@ +let (*EXPORT START *) + fun bad_actor cluster nodes_amount stress_interval verbose = let + val pid = self () + val dialer = raft_dial (cluster, pid) + val threshhold = nodes_amount / 2 + + fun create_nodes nodes 0 = nodes + | create_nodes nodes n = create_nodes ((n, false) :: nodes) (n - 1) + + val nodes = create_nodes [] nodes_amount + + fun toggle_node nodes n = let + val status = first (map (fn (_, s) => s) (filter (fn (node, _) => node = n) nodes) ) + val new_nodes = (n, (not status)) :: (filter (fn (node, _) => node <> n) nodes) + in ( + if status then + send (dialer, (SEND_TO_NTH, n, DEBUG_CONTINUE)) + else + send (dialer, (SEND_TO_NTH, n, DEBUG_PAUSE)) + ); new_nodes + end + + fun loop nodes = let + val selected = floor ((random ()) * nodes_amount + 1) + val selected_status = map (fn (_, s) => s) (filter (fn (n, _) => n = selected) nodes) + val attempted_change = + length ( + filter (fn s => s) ( + (not selected_status) :: ( + map (fn (_, s) => s) ( + filter (fn (n, _) => n <> selected) nodes + ) + ) + ) + ) + val new_nodes = + if attempted_change < threshhold then + (if verbose then print "Toggling node" else ()); + toggle_node nodes selected + else + (if verbose then print "Tried to toggle node, but too many failures" else ()); + nodes + in + sleep stress_interval; + loop new_nodes + end + + in + loop nodes + end + (* EXPORT END *) +in diff --git a/examples/raft-troupe/tests/evaluation/fib_eval.trp b/examples/raft-troupe/tests/evaluation/fib_eval.trp new file mode 100644 index 0000000..3c0e00f --- /dev/null +++ b/examples/raft-troupe/tests/evaluation/fib_eval.trp @@ -0,0 +1,53 @@ +datatype Atoms = WAIT|SUS|DONE + +let +(* EXPORT START *) +fun measure_time f = + let val time_start = getTime() + in + f (); + (getTime() - time_start) + end + + fun progress_cps cps = case getType cps of + "function" => progress_cps (cps ()) + | x => cps + + fun standard_fib n = + let fun loop n a b = case n of + 1 => b + | n => loop (n - 1) b (a + b) + in if n <= 1 then n + else loop n 0 1 + end + + fun cps_fib n = + let fun loop n a b = case n of + 1 => b + | n => (fn () => + let val n = (n - 1) + in (fn () => loop n b (a + b)) + end) + in if n <= 1 then n + else loop n 0 1 + end + + fun measure_fib dialer n = + let val time_start = getTime() + in + send(dialer, (RAFT_UPDATE, ({type = CLIENT, id = dialer}, n))); + receive [ hn _ => () ]; + getTime () - time_start + end + + fun measure_runtimes ns = + map (fn n => + let val standard_time = (measure_time (fn () => standard_fib n)) + val cps_time = (measure_time (fn () => progress_cps (cps_fib n))) + in (n, standard_time, cps_time) end) ns + + +(* EXPORT END *) + val (sides, stat, step) = fib_input (self(), 15) +in print (sides, stat, step) +end \ No newline at end of file diff --git a/examples/raft-troupe/tests/ping-tests.trp b/examples/raft-troupe/tests/ping-tests.trp new file mode 100644 index 0000000..2782c57 --- /dev/null +++ b/examples/raft-troupe/tests/ping-tests.trp @@ -0,0 +1,35 @@ +(* EXPORT START *) +fun ping_badactor () = let + val pid = self () + val ping_cluster = raft_spawn ((), ["@node1", "@node2", "@node3", "@node4", "@node5"]) + val dialer = raft_dial (ping_cluster, pid) + val _ = + send(dialer, (RAFT_UPDATE, (fn () => ping_server { type = CLUSTER, id = ping_cluster}))); + spawn (fn () => bad_actor ping_cluster 11 1000 true) + + fun loop x = let + val _ = send (dialer, (RAFT_UPDATE, ({type=CLIENT, id=dialer}, x))) + val new_x = receive [hn (c, x) => x] + in + print new_x; + if new_x > x then + loop new_x + else + print "ERROR IN PING" + end +in loop 0 +end + + +fun ping_pong_cluster_test () = + let val pid = self () + val ping_cluster = raft_spawn () + val pong_cluster = raft_spawn () + val dialer = raft_dial (ping_cluster, pid) + val dialer2 = raft_dial (pong_cluster, pid) + in + send(dialer, (RAFT_UPDATE, (fn () => ping_server { type = CLUSTER, id = ping_cluster}))); + send(dialer2, (RAFT_UPDATE, (fn () => ping_server { type = CLUSTER, id = pong_cluster}))); + send_delay(dialer, (RAFT_UPDATE, ({ type = CLUSTER, id = pong_cluster}, 0))) 5000 +end +(* EXPORT END*) diff --git a/examples/raft-troupe/zero.trp b/examples/raft-troupe/zero.trp new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/examples/raft-troupe/zero.trp @@ -0,0 +1 @@ +0 \ No newline at end of file