forked from mfp/oraft
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathoraft.ml
More file actions
1233 lines (1094 loc) · 48.1 KB
/
Copy pathoraft.ml
File metadata and controls
1233 lines (1094 loc) · 48.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
open Printf
open Lwt
module Map = BatMap
module List = BatList
module Option = BatOption
module Array = BatArray
module Kernel =
struct
type status = Leader | Follower | Candidate
type term = Int64.t
type index = Int64.t
type rep_id = string
type client_id = string
type req_id = client_id * Int64.t
type address = string
type config =
Simple_config of simple_config * passive_peers
| Joint_config of simple_config * simple_config * passive_peers
and simple_config = (rep_id * address) list
and passive_peers = (rep_id * address) list
module REPID = struct type t = rep_id let compare = String.compare end
module IM = Map.Make(Int64)
module RM = Map.Make(REPID)
module RS = Set.Make(REPID)
module CONFIG :
sig
type t
val make : node_id:rep_id -> index:index -> config -> t
val status : t -> [`Normal | `Joint]
val target : t -> (simple_config * passive_peers) option
(** Return whether we are the only active node in the latest config. *)
val is_alone : t -> bool
(** Returns all peers (including passive). *)
val peers : t -> (rep_id * address) list
val address : rep_id -> t -> address option
(** Returns whether the node is included in the last committed
* configuration. *)
val mem_committed : rep_id -> t -> [`Active | `Passive | `Not_included]
(** Returns whether the node is an active member of the configuration. *)
val mem_active : rep_id -> t -> bool
val update : (index * config) list -> t -> t
val join : index -> ?passive:passive_peers -> simple_config ->
t -> (t * config) option
val drop : at_or_after:index -> t -> t
val has_quorum : rep_id list -> t -> bool
val quorum_min : (rep_id -> Int64.t) -> t -> Int64.t
val last_commit : t -> config
val current : t -> config
(* returns the new config and the simple_config we must transition to (if
* any) when we have just committed a joint config *)
val commit : index -> t -> t * (simple_config * passive_peers) option
end =
struct
module M = Map.Make(String)
type t =
{ id : rep_id;
committed : (index * config * addr_map * addr_map);
latest : (index * config * addr_map * addr_map);
q : (index * config * addr_map * addr_map) list;
}
and addr_map = address M.t
let addr_map_of_config =
List.fold_left (fun s (id, addr) -> M.add id addr s) M.empty
let active_of_config = function
Simple_config (c, _) -> addr_map_of_config c
| Joint_config (c1, c2, _) -> addr_map_of_config (c1 @ c2)
let all_nodes_of_config = function
Simple_config (c, p) -> addr_map_of_config (c @ p)
| Joint_config (c1, c2, p) -> addr_map_of_config (p @ c1 @ c2)
let make ~node_id:id ~index config =
let active = active_of_config config in
let all = all_nodes_of_config config in
{ id;
committed = (index, config, active, all);
latest = (index, config, active, all);
q = [];
}
let quorum c = List.length c / 2 + 1
let target t = match t.latest with
| (_, Simple_config _, _, _) -> None
| (_, Joint_config (_, c2, p), _, _) -> Some (c2, p)
let has_quorum votes t =
let aux_quorum c =
List.fold_left (fun s x -> if List.mem_assoc x c then s + 1 else s) 0 votes >=
quorum c
in
match t.latest with
| _, Simple_config (c, _), _, _ -> aux_quorum c
| _, Joint_config (c1, c2, _), _, _ -> aux_quorum c1 && aux_quorum c2
let quorum_min_simple get ?only c =
let vs = List.map (fun (id, _) -> get id) (Option.default c only) |>
List.sort Int64.compare |> List.rev
in
try List.nth vs (quorum c - 1) with _ -> 0L
let set_diff l1 l2 =
List.filter (fun x -> not (List.mem x l2)) l1
let quorum_min get t = match t.latest with
_, Simple_config (c, _), _, _ -> quorum_min_simple get c
| _, Joint_config (c1, c2, _), _, _ ->
(* If new nodes are added, we require a quorum (len (c1) / 2 + 1)
* amongst the nodes that are not removed when going from c1 to c2,
* i.e.
* set(c1) - (set(c1) - set(c2))
*
* This is required to handle cases like:
*
* n0 n1 n2 -> n1 n2 n3
* transitional config: Joint_config ([n0 n1 n2], [n1 n2 n3])
*
* We need a consensus involving n1 + n2.
* n0 + n1 would not do, since the moment we transition to
* [n1 n2 n3] if n1 were to fail [n2 n3] would still have a quorum
* and could lose the committed config entry that completed the
* transition (as well as other committed operations).
*
* As a special case, transitions like (i.e., only 1 active node
* in prior config):
*
* n0 -> n1
*
* require the following quora: n0 n1
*
*
* OTOH, if we're merely removing nodes, we just want a "normal"
* consensus in both the previous and next configurations:
*
* n0 n1 -> n1
* Joint_config ([n0 n1], [n1]) required quora n0+n1 n1
*
* n0 -> n1
* Joint_config ([n0], [n1]) required quora n0 n1
* *)
if List.length c1 > 1 &&
List.exists (fun (id, _) -> not (List.mem_assoc id c1)) c2 then
let only = set_diff c1 (set_diff c1 c2) in
min (quorum_min_simple get ~only c1) (quorum_min_simple get c2)
else
min (quorum_min_simple get c1) (quorum_min_simple get c2)
let join index ?passive:p c2 t = match t.latest with
| (idx, Simple_config (c1, passive), _, _) when index > idx ->
let p = Option.default passive p in
let conf = Joint_config (c1, c2, p) in
let active = active_of_config conf in
let all = all_nodes_of_config conf in
let latest = (index, conf, active, all) in
let q = latest :: t.q in
let t = { id = t.id; committed = t.committed; latest; q; } in
Some (t, conf)
| _ -> None
let update l t =
let idx, _, _, _ = t.latest in
let l = List.sort (fun (i1, _) (i2, _) -> - Int64.compare i1 i2) l |>
List.take_while (fun (idx', _) -> idx' > idx) |>
List.map
(fun (idx, c) ->
(idx, c, active_of_config c, all_nodes_of_config c)) in
let q = l @ t.q in
let latest = match q with
| [] -> t.committed
| x :: _ -> x
in
{ id = t.id; committed = t.committed; latest; q; }
let status t = match t.latest with
| (_, Simple_config _, _, _) -> `Normal
| (_, Joint_config _, _, _) -> `Joint
let drop ~at_or_after:n t =
let q = List.drop_while (fun (idx, _, _, _) -> idx >= n) t.q in
let latest = match q with
| [] -> t.committed
| x :: _ -> x
in
{ id = t.id; committed = t.committed; latest; q; }
let commit index t =
try
let committed = List.find (fun (n, _, _, _) -> n <= index) t.q in
let q = List.take_while (fun (n, _, _, _) -> n > index) t.q in
let target = match q, committed with
| [], (_, Joint_config (_, c2, passive), _, _) ->
Some (c2, passive)
| _ -> None in
let t = { t with committed; q; } in
(t, target)
with Not_found -> (t, None)
let last_commit { committed = (_, c, _, _); _ } = c
let current { latest = (_, c, _, _); _ } = c
let peers { id; latest = (_, _, _, all); _ } =
M.remove id all |> M.bindings
let is_alone { id; latest = (_, _, active, _); _ } =
M.mem id active && (M.remove id active |> M.is_empty)
let address id { latest = (_, _, _, all); } =
M.Exceptionless.find id all
let mem_committed id { committed = (_, _, active, all); _ } =
if M.mem id active then `Active
else if M.mem id all then `Passive
else `Not_included
let mem_active id { latest = (_, _, active, _); _ } =
M.mem id active
end
module LOG : sig
type 'a t
val empty : prev_log_index:index -> prev_log_term:term -> 'a t
val to_list : 'a t -> (index * 'a * term) list
val of_list : prev_log_index:index -> prev_log_term:term ->
(index * 'a * term) list -> 'a t
val append : term:term -> 'a -> 'a t -> 'a t
val last_index : 'a t -> (term * index)
(** @return new log and index of first conflicting entry (if any) *)
val append_many : (index * ('a * term)) list -> 'a t -> 'a t * index option
val get_range : from_inclusive:index -> to_inclusive:index -> 'a t ->
(index * ('a * term)) list
val get_term : index -> 'a t -> term option
val trim_prefix : prev_log_index:index -> prev_log_term:term -> 'a t -> 'a t
val prev_log_index : 'a t -> index
val prev_log_term : 'a t -> term
end =
struct
type 'a t =
{
prev_log_index : index;
prev_log_term : term;
last_index : index;
last_term : term;
entries : ('a * term) IM.t;
}
let empty ~prev_log_index ~prev_log_term =
{ prev_log_index; prev_log_term;
last_index = prev_log_index;
last_term = prev_log_term;
entries = IM.empty;
}
let trim_prefix ~prev_log_index ~prev_log_term t =
let _, _, entries = IM.split prev_log_index t.entries in
{ t with entries; prev_log_index; prev_log_term; }
let prev_log_index t = t.prev_log_index
let prev_log_term t = t.prev_log_term
let to_list t =
IM.bindings t.entries |> List.map (fun (i, (x, t)) -> (i, x, t))
let of_list ~prev_log_index ~prev_log_term = function
[] -> empty ~prev_log_index ~prev_log_term
| l ->
let entries =
List.fold_left
(fun m (idx, x, term) -> IM.add idx (x, term) m)
IM.empty l in
let (prev_log_index, (_, prev_log_term)) = IM.min_binding entries in
let (last_index, (_, last_term)) = IM.max_binding entries in
{ prev_log_index; prev_log_term; last_index; last_term; entries; }
let append ~term:last_term x t =
let last_index = Int64.succ t.last_index in
let entries = IM.add last_index (x, last_term) t.entries in
{ t with last_index; last_term; entries; }
let last_index t = (t.last_term, t.last_index)
let get idx t =
try
Some (IM.find idx t.entries)
with Not_found -> None
let append_many l t = match l with
[] -> (t, None)
| l ->
let nonconflicting, conflict_idx =
try
let idx, term =
List.find
(fun (idx, (_, term)) ->
match get idx t with
Some (_, term') when term <> term' -> true
| _ -> false)
l in
let entries, _, _ = IM.split idx t.entries in
(entries, Some idx)
with Not_found ->
(t.entries, None)
in
let entries =
List.fold_left
(fun m (idx, (x, term)) -> IM.add idx (x, term) m)
nonconflicting l in
let last_index, last_term =
try
let idx, (_, term) = IM.max_binding entries in
(idx, term)
with _ -> t.prev_log_index, t.prev_log_term
in
({ t with last_index; last_term; entries; }, conflict_idx)
let get_range ~from_inclusive ~to_inclusive t =
if from_inclusive = t.last_index &&
to_inclusive >= t.last_index
then
[ from_inclusive, IM.find from_inclusive t.entries ]
else
let _, _, post = IM.split (Int64.pred from_inclusive) t.entries in
let pre, _, _ = if to_inclusive = Int64.max_int then (post, None, post)
else IM.split (Int64.succ to_inclusive) post
in
IM.bindings pre
let get_term idx t =
if t.last_index = idx then
Some t.last_term
else
try
Some (snd (IM.find idx t.entries))
with Not_found ->
if idx = t.prev_log_index then Some t.prev_log_term else None
end
type 'a state =
{
(* persistent *)
current_term : term;
voted_for : rep_id option;
log : 'a entry LOG.t;
id : rep_id;
config : CONFIG.t;
(* volatile *)
state : status;
commit_index : index;
last_applied : index;
leader_id : rep_id option;
(* volatile on leaders *)
next_index : index RM.t;
match_index : index RM.t;
pongs : index RM.t;
ping_index : Int64.t;
acked_ro_op : Int64.t;
snapshot_transfers : RS.t;
votes : RS.t;
}
and 'a entry = Nop | Op of 'a | Config of config
type 'a message =
Request_vote of request_vote
| Vote_result of vote_result
| Append_entries of 'a append_entries
| Append_result of append_result
| Ping of ping
| Pong of ping
and request_vote =
{
term : term;
candidate_id : rep_id;
last_log_index : index;
last_log_term : term;
}
and vote_result =
{
term : term;
vote_granted : bool;
}
and 'a append_entries =
{
term : term;
leader_id : rep_id;
prev_log_index : index;
prev_log_term : term;
entries : (index * ('a entry * term)) list;
leader_commit : index;
}
and append_result =
{
term : term;
result : actual_append_result;
}
and actual_append_result =
Append_success of index (* last log entry included in msg we respond to *)
| Append_failure of index
(* Index of log entry preceding those in message we respond to or
* the index following the last entry we do have.
* *)
and ping = { term : term; n : Int64.t; }
type 'a action =
Apply of (index * 'a * term) list
| Become_candidate
| Become_follower of rep_id option
| Become_leader
| Changed_config
| Exec_readonly of Int64.t
| Redirect of rep_id option * 'a
| Reset_election_timeout
| Reset_heartbeat
| Send of rep_id * address * 'a message
| Send_snapshot of rep_id * address * index * config
| Stop
end
include Kernel
let quorum_of_config config = Array.length config / 2 + 1
let quorum_of_peers peers = (1 + Array.length peers) / 2 + 1
let vote_result s vote_granted =
Vote_result { term = s.current_term; vote_granted; }
let append_result s result = Append_result { term = s.current_term; result }
let append_ok ~last_log_index s =
append_result s (Append_success last_log_index)
let append_fail ~prev_log_index s =
append_result s (Append_failure prev_log_index)
let update_commit_index s =
(* Find last N such that log[N].term = current term AND
* a majority of peers has got match_index[peer] >= N. *)
(* We require majorities in both the old and the new configutation during
* the joint consensus. *)
let get_match_index id =
if id = s.id then LOG.last_index s.log |> snd
else try RM.find id s.match_index with Not_found -> 0L in
let i = CONFIG.quorum_min get_match_index s.config in
(* i is the largest N such that at least half of the peers have
* match_Index[peer] >= N; we have to enforce the other restriction:
* log[N].term = current *)
let commit_index' =
match LOG.get_term i s.log with
| Some term when term = s.current_term -> i
| _ -> s.commit_index in
(* increate monotonically *)
let commit_index = max s.commit_index commit_index' in
if commit_index = s.commit_index then s
else { s with commit_index }
let step_down term s =
{ s with current_term = term;
voted_for = None;
leader_id = None;
state = Follower;
}
let try_commit s =
let prev = s.last_applied in
if prev = s.commit_index then
(s, [])
else
let s = { s with last_applied = s.commit_index } in
let entries = LOG.get_range
~from_inclusive:(Int64.succ prev)
~to_inclusive:s.commit_index
s.log in
let ops = List.filter_map
(function
(index, (Op x, term)) -> Some (index, x, term)
| (_, ((Nop | Config _), _)) -> None)
entries in
(* We check whether the node was included in the configuration before
* the commit. We make it stop only if it was, and it is no longer in
* the newly committed one *)
let was_included = CONFIG.mem_committed s.id s.config <> `Not_included in
let config, wanted_config = CONFIG.commit s.commit_index s.config in
let changed_config = List.exists
(function (_, (Config _, _)) -> true | _ -> false)
entries in
(* if we're the leader and wanted_config = Some conf, add a new log
* entry for the wanted configuration [conf]; it will be replicated on
* the next heartbeat *)
let s = match wanted_config, s.state with
| Some (c, passive), Leader ->
let conf = Simple_config (c, passive) in
let log = LOG.append ~term:s.current_term
(Config conf) s.log in
let index = LOG.last_index log |> snd in
let config = CONFIG.update [index, conf] config in
{ s with log; config }
| _ -> { s with config } in
let actions = match ops with [] -> [] | l -> [Apply ops] in
(* "In Raft the leader steps down immediately after committing a
* configuration entry that does not include itself." *)
let s, actions = match s.state, CONFIG.mem_committed s.id s.config with
| (Leader | Candidate), `Passive ->
let actions = actions @ [Become_follower None] in
(step_down s.current_term s, actions)
| _, `Not_included when was_included ->
let actions = actions @ [Become_follower None; Stop] in
(step_down s.current_term s, actions)
| _, _ -> (s, actions) in
let actions = if changed_config then Changed_config :: actions
else actions
in
(s, actions)
let heartbeat s =
let prev_log_term, prev_log_index = LOG.last_index s.log in
Append_entries { term = s.current_term; leader_id = s.id;
prev_log_term; prev_log_index; entries = [];
leader_commit = s.commit_index }
type 'a send_entries =
Snapshot | Send_entries of 'a | Snapshot_in_progress
let send_entries s from =
match LOG.get_term (Int64.pred from) s.log with
None -> None
| Some prev_log_term ->
Some
(Append_entries
{ prev_log_term;
term = s.current_term;
leader_id = s.id;
prev_log_index = Int64.pred from;
entries = LOG.get_range
~from_inclusive:from
~to_inclusive:Int64.max_int
s.log;
leader_commit = s.commit_index;
})
let send_entries_or_snapshot s peer from =
if RS.mem peer s.snapshot_transfers then
Snapshot_in_progress
else
match send_entries s from with
None -> Snapshot
| Some x -> Send_entries x
let broadcast s msg =
CONFIG.peers s.config |> List.map (fun (id, addr) -> Send (id, addr, msg))
let receive_msg s peer = function
(* Reject vote requests/results and Append_entries (which should not be
* received anyway since passive members cannot become leaders) from passive
* members *)
| Append_entries _ | Ping _ | Pong _
| Request_vote _ | Vote_result _ when not (CONFIG.mem_active peer s.config) ->
(s, [])
(* " If a server receives a request with a stale term number, it rejects the
* request." *)
| Request_vote { term; _ } | Ping { term; _ } when term < s.current_term ->
begin match CONFIG.address peer s.config with
None -> (s, [])
| Some addr ->
(s, [Send (peer, addr, vote_result s false)])
end
| Pong { term; _ } when term <> s.current_term -> (s, [])
(* "Current terms are exchanged whenever servers communicate; if one
* server’s current term is smaller than the other, then it updates its
* current term to the larger value. If a candidate or leader discovers that
* its term is out of date, it immediately reverts to follower state."
* *)
| Vote_result { term; _ }
| Append_result { term; _ }
| Ping { term; _ } when term > s.current_term ->
(step_down term s, [Become_follower None])
| Ping { term; n; } (* term = current_term *) -> begin
match s.leader_id, CONFIG.address peer s.config with
Some leader, Some addr when leader = peer ->
(s, [Send (peer, addr, Pong { term; n; })])
| _ ->
(* We ignore Ping messages not coming from the leader.
* They should not happen, since followers/candidates do not send
* Pings. *)
(s, [])
end
| Pong { term; n; } ->
let pongs = RM.modify_def 0L peer (max n) s.pongs in
let get_last_pong id =
if id = s.id then s.ping_index
else Option.default 0L (RM.Exceptionless.find id pongs) in
let acked = CONFIG.quorum_min get_last_pong s.config in
let actions = if acked > s.acked_ro_op then [ Exec_readonly acked ]
else [] in
let s = { s with pongs; acked_ro_op = max s.acked_ro_op acked; } in
(s, actions)
| Request_vote { term; candidate_id; last_log_index; last_log_term; }
when term > s.current_term ->
let s = step_down term s in
(* "If votedFor is null or candidateId, and candidate's log is at
* least as up-to-date as receiver’s log, grant vote"
*
* [voted_for] is None since it was reset above when we updated
* [current_term]
*
* "Raft determines which of two logs is more up-to-date
* by comparing the index and term of the last entries in the
* logs. If the logs have last entries with different terms, then
* the log with the later term is more up-to-date. If the logs
* end with the same term, then whichever log is longer is
* more up-to-date."
* *)
begin match CONFIG.address peer s.config with
None -> (s, [ Become_follower None ])
| Some addr ->
if (last_log_term, last_log_index) < LOG.last_index s.log then
(s, [Become_follower None; Send (peer, addr, vote_result s false)])
else
let s = { s with voted_for = Some candidate_id } in
(s, [Become_follower (Some candidate_id);
Send (peer, addr, vote_result s true)])
end
| Request_vote { term; candidate_id; last_log_index; last_log_term; } -> begin
(* case term = current_term *)
match s.state, s.voted_for, CONFIG.address peer s.config with
_, Some candidate, Some addr when candidate <> candidate_id ->
(s, [Send (peer, addr, vote_result s false)])
| (Candidate | Leader), _, Some addr ->
(s, [Send (peer, addr, vote_result s false)])
| Follower, _, Some addr
(* voted for None or Some candidate equal to candidate_id *) ->
(* "If votedFor is null or candidateId, and candidate's log is at
* least as up-to-date as receiver’s log, grant vote"
*
* "Raft determines which of two logs is more up-to-date
* by comparing the index and term of the last entries in the
* logs. If the logs have last entries with different terms, then
* the log with the later term is more up-to-date. If the logs
* end with the same term, then whichever log is longer is
* more up-to-date."
* *)
if (last_log_term, last_log_index) < LOG.last_index s.log then
(s, [Send (peer, addr, vote_result s false)])
else
let s = { s with voted_for = Some candidate_id } in
(s, [Send (peer, addr, vote_result s true)])
| _, _, None -> (s, [])
end
(* " If a server receives a request with a stale term number, it rejects the
* request." *)
| Append_entries { term; prev_log_index; _ } when term < s.current_term ->
begin match CONFIG.address peer s.config with
None -> (s, [])
| Some addr -> (s, [Send (peer, addr, append_fail ~prev_log_index s)])
end
| Append_entries
{ term; prev_log_index; prev_log_term; entries; leader_commit; } -> begin
(* "Current terms are exchanged whenever servers communicate; if one
* server’s current term is smaller than the other, then it updates
* its current term to the larger value. If a candidate or leader
* discovers that its term is out of date, it immediately reverts to
* follower state." *)
let s, actions =
if term > s.current_term || s.state = Candidate then
let s = { s with current_term = term;
state = Follower;
leader_id = Some peer;
(* set voted_for so that no other candidates are
* accepted during the new term *)
voted_for = Some peer;
}
in
(s, [Become_follower (Some peer)])
else (* term = s.current_term && s.state <> Candidate *)
(s, [Reset_election_timeout]) in
(* "Reply false if log doesn’t contain an entry at prevLogIndex
* whose term matches prevLogTerm" *)
(* In the presence of snapshots, prev_log_index might refer to a log
* entry that was removed (subsumed by the snapshot), in which case
* we must use as prev_log_index the index of the latest entry in
* the snapshot (equal to the prev_log_index of the log), and as
* prev_log_term the term of the corresponding entry in the
* Append_entries message. *)
let prev_log_index, prev_log_term, entries =
if prev_log_index >= LOG.prev_log_index s.log then
(prev_log_index, prev_log_term, entries)
else
try
let prev_idx = LOG.prev_log_index s.log in
let prev_term = List.assoc prev_idx entries |> snd in
let entries = List.drop_while
(fun (idx', _) -> idx' <= prev_idx) entries
in
(prev_idx, prev_term, entries)
with _ -> (prev_log_index, prev_log_term, entries)
in
match
LOG.get_term prev_log_index s.log, CONFIG.address peer s.config
with
None, Some addr ->
(* we don't have the entry at prev_log_index; we use the
* successor of the last entry we do have as the
* prev_log_index in the failure msg, thus allowing to rewind
* next_index in the leader quickly *)
let prev_log_index = LOG.last_index s.log |> snd |> Int64.succ in
(s, Send (peer, addr, append_fail ~prev_log_index s) :: actions)
| Some term', Some addr when prev_log_term <> term' ->
(s, Send (peer, addr, append_fail ~prev_log_index s) :: actions)
| _, Some addr ->
let log, c_idx = LOG.append_many entries s.log in
let config = match c_idx with
| None -> s.config
| Some idx ->
CONFIG.drop ~at_or_after:idx s.config in
(* "a server always uses the latest configuration in its log,
* regardless of whether the entry is committed" *)
let new_configs = List.filter_map
(function
(idx, (Config c, _)) -> Some (idx, c)
| _ -> None)
entries in
let config = CONFIG.update new_configs config in
let last_index = snd (LOG.last_index log) in
let commit_index = if leader_commit > s.commit_index then
min leader_commit last_index
else s.commit_index in
let reply = append_ok ~last_log_index:last_index s in
let s = { s with commit_index; log; config;
leader_id = Some peer; } in
let s, commits = try_commit s in
let actions = List.concat
[ [Send (peer, addr, reply)];
commits;
actions ]
in
(s, actions)
| _, None -> (s, [])
end
| Vote_result { term; _ } when term < s.current_term ->
(s, [])
| Vote_result { term; vote_granted; } when s.state <> Candidate ->
(s, [])
| Vote_result { term; vote_granted; } ->
if not vote_granted then
(s, [])
else
(* "If votes received from majority of servers: become leader" *)
let votes = RS.add peer s.votes in
let s = { s with votes } in
if not (CONFIG.has_quorum (RS.elements votes) s.config) then
(s, [])
else
(* become leader! *)
(* So as to have the leader know which entries are committed
* (one of the 2 requirements to support read-only operations in
* the leader, the other being making sure it's still the leader
* with a hearbeat exchange), we have it commit a blank
* no-op/config request entry at the start of its term, which also
* serves as the initial heartbeat. If we're in a joint consensus,
* we send a config request for the target configuration instead
* of a Nop. *)
let entry = match CONFIG.target s.config with
| None -> Nop
| Some (c, passive) -> Config (Simple_config (c, passive)) in
let log = LOG.append ~term:s.current_term entry s.log in
(* With a regular (empty) hearbeat, this applies:
* "When a leader first comes to power, it initializes all
* nextIndex values to the index just after the last one in its
* log"
* However, in this case we want to send the Nop too, so no Int64.succ.
* *)
let next_idx = LOG.last_index log |> snd (* |> Int64.succ *) in
let next_index = List.fold_left
(fun m (peer, _) -> RM.add peer next_idx m)
RM.empty (CONFIG.peers s.config) in
let match_index = List.fold_left
(fun m (peer, _) -> RM.add peer 0L m)
RM.empty (CONFIG.peers s.config) in
let s = { s with log; next_index; match_index;
state = Leader;
leader_id = Some s.id;
pongs = RM.empty;
ping_index = 1L;
acked_ro_op = 0L;
snapshot_transfers = RS.empty;
} in
(* This heartbeat is replaced by the broadcast of the no-op
* explained above:
*
(* "Upon election: send initial empty AppendEntries RPCs
* (heartbeat) to each server; repeat during idle periods to
* prevent election timeouts" *)
let sends = broadcast s (heartbeat s) in
*)
let msg = send_entries s next_idx in
let sends = CONFIG.peers s.config |>
List.filter_map
(fun (peer, addr) ->
Option.map (fun m -> Send (peer, addr, m)) msg)
in
(s, (Become_leader :: sends))
| Append_result { term; _ } when term < s.current_term || s.state <> Leader ->
(s, [])
| Append_result { result = Append_success last_log_index; _ } ->
let next_index = RM.modify_def
0L peer
(fun idx -> max idx (Int64.succ last_log_index))
s.next_index in
let match_index = RM.modify_def
last_log_index peer
(max last_log_index) s.match_index in
let s = update_commit_index { s with next_index; match_index } in
let s, actions = try_commit s in
(s, (Reset_election_timeout :: actions))
| Append_result { result = Append_failure prev_log_index; _ } ->
(* "After a rejection, the leader decrements nextIndex and retries
* the AppendEntries RPC. Eventually nextIndex will reach a point
* where the leader and follower logs match." *)
let next_index = RM.modify_def
prev_log_index peer
(fun idx -> min idx prev_log_index)
s.next_index in
let s = { s with next_index } in
let idx = RM.find peer next_index in
match send_entries_or_snapshot s peer idx, CONFIG.address peer s.config with
| Snapshot, Some addr ->
(* Must send snapshot *)
let config = CONFIG.last_commit s.config in
let transfers = RS.add peer s.snapshot_transfers in
let s = { s with snapshot_transfers = transfers } in
(s, [Reset_election_timeout; Send_snapshot (peer, addr, idx, config)])
| Snapshot_in_progress, _ -> (s, [])
| Send_entries msg, Some addr ->
(s, [Reset_election_timeout; Send (peer, addr, msg)])
| _ , None -> (s, [])
let election_timeout s = match s.state with
(* passive nodes do not trigger elections *)
| _ when not (CONFIG.mem_active s.id s.config) -> (s, [Reset_election_timeout])
(* if we're the active only node in the cluster, win the elections right
* away *)
| Follower | Candidate when CONFIG.is_alone s.config ->
let s = { s with current_term = Int64.succ s.current_term;
state = Leader;
votes = RS.singleton s.id;
leader_id = Some s.id;
voted_for = Some s.id;
pongs = RM.empty;
ping_index = 1L;
acked_ro_op = 0L;
snapshot_transfers = RS.empty;
}
in (s, [Become_leader])
| Leader when CONFIG.is_alone s.config ->
(s, [Reset_election_timeout])
(* we have the leader step down if it does not get any append_result
* within an election timeout *)
| Leader
(* "If election timeout elapses without receiving AppendEntries RPC from
* current leader or granting vote to candidate: convert to candidate" *)
| Follower
(* "If election timeout elapses: start new election" *)
| Candidate ->
(* "On conversion to candidate, start election:
* * Increment currentTerm
* * Vote for self
* * Reset election timeout
* * Send RequestVote RPCs to all other servers"
* *)
let s = { s with current_term = Int64.succ s.current_term;
state = Candidate;
votes = RS.singleton s.id;
leader_id = None;
voted_for = Some s.id;
} in
let term_, idx_ = LOG.last_index s.log in
let msg = Request_vote
{
term = s.current_term;
candidate_id = s.id;
last_log_index = idx_;
last_log_term = term_;
} in
let sends = broadcast s msg in
(s, (Become_candidate :: sends))
let heartbeat_timeout s = match s.state with
Follower | Candidate -> (s, [Reset_heartbeat])
| Leader when CONFIG.is_alone s.config ->
let s, actions = update_commit_index s |> try_commit in
(s, Reset_heartbeat :: actions)
| Leader ->
let s, sends =
CONFIG.peers s.config |>
List.fold_left
(fun (s, sends) (peer, addr) ->
let idx = try RM.find peer s.next_index
with Not_found -> LOG.last_index s.log |> snd
in
match send_entries_or_snapshot s peer idx with
Snapshot_in_progress -> (s, sends)
| Send_entries msg -> (s, Send (peer, addr, msg) :: sends)
| Snapshot ->
(* must send snapshot if cannot send log *)
let transfers = RS.add peer s.snapshot_transfers in
let s = { s with snapshot_transfers = transfers } in
let config = CONFIG.last_commit s.config in
(s, Send_snapshot (peer, addr, idx, config) :: sends))
(s, [])
in
(s, (Reset_heartbeat :: sends))
let client_command x s = match s.state with
Follower | Candidate -> (s, [Redirect (s.leader_id, x)])
| Leader when CONFIG.is_alone s.config ->
let log = LOG.append ~term:s.current_term (Op x) s.log in
let s = update_commit_index { s with log; } in
try_commit s
| Leader ->
let log = LOG.append ~term:s.current_term (Op x) s.log in
let s, actions =
CONFIG.peers s.config |>
(* We update next_index to be past the last entry we send to each
* peer. This way, we don't send entries repeatedly to a peer when we
* get several commands before the peer has ACKed them. *)
List.fold_left
(fun (s, next_index, actions) (peer, addr) ->
let next = LOG.last_index s.log |> snd |> Int64.succ in
let idx = try RM.find peer s.next_index with Not_found -> next in
let next_index = RM.add peer next s.next_index in
match send_entries_or_snapshot s peer idx with
Snapshot ->
(* must send snapshot if cannot send log *)
let transfers = RS.add peer s.snapshot_transfers in
let s = { s with snapshot_transfers = transfers } in
let config = CONFIG.last_commit s.config in
let actions = Send_snapshot (peer, addr, idx, config) ::
actions
in (s, next_index, actions)
| Snapshot_in_progress -> (s, next_index, actions)