@@ -2646,11 +2646,9 @@ commit_pending(#raft_state{log_view = View} = Data0) ->
26462646 skipped ->
26472647 % Since the append failed, we roll back the label state to before
26482648 % we constructed the entries.
2649- ? RAFT_COUNT ('raft.server.sync.skipped' ),
26502649 ? RAFT_LOG_WARNING (leader , Data0 , " skipped pre-heartbeat sync for ~0p log entr(ies)." , [length (Entries )]),
26512650 cancel_pending ({error , commit_stalled }, Data0 );
26522651 {error , Error } ->
2653- ? RAFT_COUNT ('raft.server.sync.error' ),
26542652 ? RAFT_LOG_ERROR (leader , Data0 , " sync failed due to ~0P ." , [Error , 20 ]),
26552653 error (Error )
26562654 end .
@@ -2679,7 +2677,6 @@ collect_pending(#raft_state{pending = Pending} = Data) ->
26792677cancel_pending (_ , # raft_state {pending = []} = Data ) ->
26802678 Data ;
26812679cancel_pending (Reason , # raft_state {queues = Queues , pending = Pending } = Data ) ->
2682- ? RAFT_COUNT ('raft.commit.batch.cancel' ),
26832680 % Pending commits are kept in reverse order.
26842681 [wa_raft_queue :fulfill_incomplete_commit (Queues , Reference , Reason ) || {Reference , _ } <- lists :reverse (Pending )],
26852682 Data # raft_state {pending = []}.
@@ -2966,60 +2963,76 @@ append_entries(
29662963 Entries ,
29672964 EntryCount ,
29682965 # raft_state {
2969- log_view = View ,
2966+ log_view = View0 ,
29702967 commit_index = CommitIndex ,
29712968 current_term = CurrentTerm ,
29722969 leader_id = LeaderId
29732970 } = Data
29742971) ->
2975- % Inspect the locally stored term associated with the previous log entry to discern if
2976- % appending the provided range of log entries is allowed.
2977- case wa_raft_log :term (View , PrevLogIndex ) of
2978- {ok , PrevLogTerm } ->
2979- % If the term of the log entry previous the entries to be applied matches the term stored
2980- % with the previous log entry in the local RAFT log, then this follower can proceed with
2981- % appending to the log.
2982- {ok , NewMatchIndex , NewView } = wa_raft_log :append_at (View , PrevLogIndex + 1 , Entries ),
2983- {ok , true , NewMatchIndex , Data # raft_state {log_view = NewView }};
2984- {ok , LocalPrevLogTerm } ->
2985- % If the term of the log entry proceeding the entries to be applied does not match the log
2986- % entry stored with the previous log entry in the local RAFT log, then we need to truncate
2987- % the log because there is a mismatch between this follower and the leader of the cluster.
2988- ? RAFT_COUNT ({raft , State , 'heartbeat.skip.log_term_mismatch' }),
2989- ? RAFT_LOG_WARNING (State , Data , " rejects appending ~0p log entries in range ~0p to ~0p as previous log entry ~0p has term ~0p locally when leader ~0p expects it to have term ~0p ." ,
2990- [EntryCount , PrevLogIndex + 1 , PrevLogIndex + EntryCount , PrevLogIndex , LocalPrevLogTerm , LeaderId , PrevLogTerm ]),
2991- case PrevLogIndex =< CommitIndex of
2992- true ->
2993- % We cannot validly delete log entries that have already been committed because doing
2994- % so means that we are erasing log entries that may be part of the minimum quorum. If
2995- % we try to do so, then disable this partition as we've violated a critical invariant.
2996- ? RAFT_COUNT ({raft , State , 'heartbeat.error.corruption.excessive_truncation' }),
2997- ? RAFT_LOG_WARNING (State , Data , " fails as progress requires truncation of log entry at ~0p due to log mismatch when log entries up to ~0p were already committed." ,
2998- [PrevLogIndex , CommitIndex ]),
2999- {fatal ,
3000- lists :flatten (
3001- io_lib :format (" Leader ~0p of term ~0p requested truncation of log entry at ~0p due to log term mismatch (local ~0p , leader ~0p ) when log entries up to ~0p were already committed." ,
3002- [LeaderId , CurrentTerm , PrevLogIndex , LocalPrevLogTerm , PrevLogTerm , CommitIndex ]))};
3003- false ->
3004- % We are not deleting already applied log entries, so proceed with truncation.
3005- ? RAFT_LOG_NOTICE (State , Data , " Server[~0p , term ~0p , ~0p ] truncating local log ending at ~0p to past ~0p due to log mismatch." ,
3006- [wa_raft_log :last_index (View ), PrevLogIndex ]),
3007- {ok , NewView } = wa_raft_log :truncate (View , PrevLogIndex ),
3008- {ok , false , wa_raft_log :last_index (NewView ), Data # raft_state {log_view = NewView }}
2972+ % Compare the incoming heartbeat with the local log to determine what
2973+ % actions need to be taken as part of handling this heartbeat.
2974+ case wa_raft_log :check_heartbeat (View0 , PrevLogIndex , [{PrevLogTerm , undefined } | Entries ]) of
2975+ {ok , []} ->
2976+ % No append is required as all the log entries in the heartbeat
2977+ % are already in the local log.
2978+ {ok , true , PrevLogIndex + EntryCount , Data };
2979+ {ok , NewEntries } ->
2980+ % No conflicting log entries were found in the heartbeat, but the
2981+ % heartbeat does contain new log entries to be appended to the end
2982+ % of the log.
2983+ {ok , View1 } = wa_raft_log :append (View0 , NewEntries ),
2984+ {ok , true , PrevLogIndex + EntryCount , Data # raft_state {log_view = View1 }};
2985+ {conflict , ConflictIndex , [{ConflictTerm , _ } | _ ]} when ConflictIndex =< CommitIndex ->
2986+ % A conflict is detected that would result in the truncation of a
2987+ % log entry that the local replica has committed. We cannot validly
2988+ % delete log entries that are already committed because doing so
2989+ % may potenially cause the log entry to be no longer present on a
2990+ % majority of replicas.
2991+ {ok , LocalTerm } = wa_raft_log :term (View0 , ConflictIndex ),
2992+ ? RAFT_COUNT ({raft , State , 'heartbeat.error.corruption.excessive_truncation' }),
2993+ ? RAFT_LOG_WARNING (State , Data , " refuses heartbeat at ~0p to ~0p that requires truncation past ~0p (term ~0p vs ~0p ) when log entries up to ~0p are already committed." ,
2994+ [PrevLogIndex , PrevLogIndex + EntryCount , ConflictIndex , ConflictTerm , LocalTerm , CommitIndex ]),
2995+ Fatal = io_lib :format (" A heartbeat at ~0p to ~0p from ~0p in term ~0p required truncating past ~0p (term ~0p vs ~0p ) when log entries up to ~0p were already committed." ,
2996+ [PrevLogIndex , PrevLogIndex + EntryCount , LeaderId , CurrentTerm , ConflictIndex , ConflictTerm , LocalTerm , CommitIndex ]),
2997+ {fatal , lists :flatten (Fatal )};
2998+ {conflict , ConflictIndex , NewEntries } when ConflictIndex >= PrevLogIndex ->
2999+ % A truncation is required as there is a conflict between the local
3000+ % log and the incoming heartbeat.
3001+ ? RAFT_LOG_NOTICE (State , Data , " handling heartbeat at ~0p by truncating local log ending at ~0p to past ~0p ." ,
3002+ [PrevLogIndex , wa_raft_log :last_index (View0 ), ConflictIndex ]),
3003+ case wa_raft_log :truncate (View0 , ConflictIndex ) of
3004+ {ok , View1 } ->
3005+ case ConflictIndex =:= PrevLogIndex of
3006+ true ->
3007+ % If the conflict precedes the heartbeat's log
3008+ % entries then no append can be performed.
3009+ {ok , false , wa_raft_log :last_index (View1 ), Data # raft_state {log_view = View1 }};
3010+ false ->
3011+ % Otherwise, we can replace the truncated log
3012+ % entries with those from the current heartbeat.
3013+ {ok , View2 } = wa_raft_log :append (View1 , NewEntries ),
3014+ {ok , true , PrevLogIndex + EntryCount , Data # raft_state {log_view = View2 }}
3015+ end ;
3016+ {error , Reason } ->
3017+ ? RAFT_COUNT ({raft , State , 'heartbeat.truncate.error' }),
3018+ ? RAFT_LOG_WARNING (State , Data , " fails to truncate past ~0p while handling heartbeat at ~0p to ~0p due to ~0P " ,
3019+ [ConflictIndex , PrevLogIndex , PrevLogIndex + EntryCount , Reason , 30 ]),
3020+ {ok , false , wa_raft_log :last_index (View0 ), Data }
30093021 end ;
3010- not_found ->
3011- % If the log entry is not found, then ignore and notify the leader of what log entry
3012- % is required by this follower in the reply.
3013- ? RAFT_COUNT ({raft , State , 'heartbeat.skip.missing_previous_log_entry' }),
3022+ {error , out_of_range } ->
3023+ % If the heartbeat is out of range (generally past the end of the
3024+ % log) then ignore and notify the leader of what log entry is
3025+ % required by this replica.
3026+ ? RAFT_COUNT ({raft , State , 'heartbeat.skip.out_of_range' }),
30143027 EntryCount =/= 0 andalso
3015- ? RAFT_LOG_WARNING (State , Data , " skips appending ~0p log entries in range ~0p to ~0p because previous log entry at ~0p is not available in local log covering ~0p to ~0p ." ,
3016- [EntryCount , PrevLogIndex + 1 , PrevLogIndex + EntryCount , PrevLogIndex , wa_raft_log :first_index (View ), wa_raft_log :last_index (View )]),
3017- {ok , false , wa_raft_log :last_index (View ), Data };
3028+ ? RAFT_LOG_WARNING (State , Data , " refuses out of range heartbeat at ~0p to ~0p with local log covering ~0p to ~0p ." ,
3029+ [PrevLogIndex , PrevLogIndex + EntryCount , wa_raft_log :first_index (View0 ), wa_raft_log :last_index (View0 )]),
3030+ {ok , false , wa_raft_log :last_index (View0 ), Data };
30183031 {error , Reason } ->
3019- ? RAFT_COUNT ({raft , State , 'heartbeat.skip.failed_to_read_previous_log_entry ' }),
3020- ? RAFT_LOG_WARNING (State , Data , " skips appending ~0p log entries in range ~0p to ~0p because reading previous log entry at ~0p failed with error ~0P . " ,
3021- [EntryCount , PrevLogIndex + 1 , PrevLogIndex + EntryCount , PrevLogIndex , Reason , 30 ]),
3022- {ok , false , wa_raft_log :last_index (View ), Data }
3032+ ? RAFT_COUNT ({raft , State , 'heartbeat.skip.error ' }),
3033+ ? RAFT_LOG_WARNING (State , Data , " fails to check heartbeat at ~0p to ~0p for validity due to ~0P " ,
3034+ [PrevLogIndex , PrevLogIndex + EntryCount , Reason , 30 ]),
3035+ {ok , false , wa_raft_log :last_index (View0 ), Data }
30233036 end .
30243037
30253038% %------------------------------------------------------------------------------
0 commit comments