1- // Copyright IBM Corp. 2013, 2025
1+ // Copyright IBM Corp. 2013, 2026
22// SPDX-License-Identifier: MPL-2.0
33
44package raft
@@ -10,7 +10,7 @@ import (
1010 "sync/atomic"
1111 "time"
1212
13- "github.com/hashicorp/go-metrics/compat"
13+ metrics "github.com/hashicorp/go-metrics/compat"
1414)
1515
1616const (
@@ -73,6 +73,14 @@ type followerReplication struct {
7373 // lastContactLock protects 'lastContact'.
7474 lastContactLock sync.RWMutex
7575
76+ // lastReplicationStart is updated to the current time whenever a
77+ // replicateTo method call is started, and is cleared when the replicateTo
78+ // method returns. This is used by heartbeats to check if replication has
79+ // stalled for too long.
80+ lastReplicationStart time.Time
81+ // lastReplicationStartLock protects 'lastReplicationStart'.
82+ lastReplicationStartLock sync.RWMutex
83+
7684 // failures counts the number of failed RPCs since the last success, which is
7785 // used to apply backoff.
7886 failures uint64
@@ -132,6 +140,30 @@ func (s *followerReplication) setLastContact() {
132140 s .lastContactLock .Unlock ()
133141}
134142
143+ // resetLastReplicationStart clears the marker for the start of the last replication
144+ // attempt
145+ func (s * followerReplication ) resetLastReplicationStart () {
146+ s .lastReplicationStartLock .Lock ()
147+ s .lastReplicationStart = time.Time {}
148+ s .lastReplicationStartLock .Unlock ()
149+ }
150+
151+ // setLastReplicationStart sets the marker for the start of the last replication
152+ // attempt to the current time
153+ func (s * followerReplication ) setLastReplicationStart () {
154+ s .lastReplicationStartLock .Lock ()
155+ s .lastReplicationStart = time .Now ()
156+ s .lastReplicationStartLock .Unlock ()
157+ }
158+
159+ // getLastReplicationStart gets the start of the last replication attempt
160+ func (s * followerReplication ) getLastReplicationStart () time.Time {
161+ s .lastReplicationStartLock .RLock ()
162+ t := s .lastReplicationStart
163+ s .lastReplicationStartLock .RUnlock ()
164+ return t
165+ }
166+
135167// replicate is a long running routine that replicates log entries to a single
136168// follower.
137169func (r * Raft ) replicate (s * followerReplication ) {
@@ -140,17 +172,22 @@ func (r *Raft) replicate(s *followerReplication) {
140172 defer close (stopHeartbeat )
141173 r .goFunc (func () { r .heartbeat (s , stopHeartbeat ) })
142174
175+ defer s .resetLastReplicationStart ()
176+
143177RPC:
144178 shouldStop := false
145179 for ! shouldStop {
180+ s .resetLastReplicationStart ()
146181 select {
147182 case maxIndex := <- s .stopCh :
148183 // Make a best effort to replicate up to this index
149184 if maxIndex > 0 {
185+ s .setLastReplicationStart ()
150186 r .replicateTo (s , maxIndex )
151187 }
152188 return
153189 case deferErr := <- s .triggerDeferErrorCh :
190+ s .setLastReplicationStart ()
154191 lastLogIdx , _ := r .getLastLog ()
155192 shouldStop = r .replicateTo (s , lastLogIdx )
156193 if ! shouldStop {
159196 deferErr .respond (fmt .Errorf ("replication failed" ))
160197 }
161198 case <- s .triggerCh :
199+ s .setLastReplicationStart ()
162200 lastLogIdx , _ := r .getLastLog ()
163201 shouldStop = r .replicateTo (s , lastLogIdx )
164202 // This is _not_ our heartbeat mechanism but is to ensure
@@ -167,12 +205,14 @@ RPC:
167205 // can't do this to keep them unblocked by disk IO on the
168206 // follower. See https://github.com/hashicorp/raft/issues/282.
169207 case <- randomTimeout (r .config ().CommitTimeout ):
208+ s .setLastReplicationStart ()
170209 lastLogIdx , _ := r .getLastLog ()
171210 shouldStop = r .replicateTo (s , lastLogIdx )
172211 }
173212
174213 // If things looks healthy, switch to pipeline mode
175214 if ! shouldStop && s .allowPipeline {
215+ s .resetLastReplicationStart ()
176216 goto PIPELINE
177217 }
178218 }
@@ -409,6 +449,30 @@ func (r *Raft) heartbeat(s *followerReplication, stopCh chan struct{}) {
409449 s .peerLock .RUnlock ()
410450
411451 start := time .Now ()
452+
453+ lastReplicationStart := s .getLastReplicationStart ()
454+ if ! lastReplicationStart .IsZero () {
455+ maxLastReplication := r .config ().HeartbeatTimeout * 10
456+ if lastReplicationStart .Add (maxLastReplication ).Before (start ) {
457+ r .logger .Warn ("delaying heartbeat for peer because replication is stalled" ,
458+ "peer" , peer .Address ,
459+ "timeout" , maxLastReplication ,
460+ "replication_started" , lastReplicationStart ,
461+ )
462+ // Replication has been stalled for too long. Delay the next
463+ // heartbeat to allow a follower to take over, but don't exit the
464+ // loop yet in case replication unblocks during the delay
465+ select {
466+ case <- s .notifyCh :
467+ case <- randomTimeout (r .config ().HeartbeatTimeout ):
468+ case <- stopCh :
469+ return
470+ }
471+
472+ continue
473+ }
474+ }
475+
412476 if err := r .trans .AppendEntries (peer .ID , peer .Address , & req , & resp ); err != nil {
413477 nextBackoffTime := cappedExponentialBackoff (failureWait , failures , maxFailureScale , r .config ().HeartbeatTimeout / 2 )
414478 r .logger .Error ("failed to heartbeat to" , "peer" , peer .Address , "backoff time" ,
@@ -472,16 +536,19 @@ func (r *Raft) pipelineReplicate(s *followerReplication) error {
472536 shouldStop := false
473537SEND:
474538 for ! shouldStop {
539+ s .resetLastReplicationStart ()
475540 select {
476541 case <- finishCh :
477542 break SEND
478543 case maxIndex := <- s .stopCh :
479544 // Make a best effort to replicate up to this index
480545 if maxIndex > 0 {
546+ s .setLastReplicationStart ()
481547 r .pipelineSend (s , pipeline , & nextIndex , maxIndex )
482548 }
483549 break SEND
484550 case deferErr := <- s .triggerDeferErrorCh :
551+ s .setLastReplicationStart ()
485552 lastLogIdx , _ := r .getLastLog ()
486553 shouldStop = r .pipelineSend (s , pipeline , & nextIndex , lastLogIdx )
487554 if ! shouldStop {
@@ -490,13 +557,16 @@ SEND:
490557 deferErr .respond (fmt .Errorf ("replication failed" ))
491558 }
492559 case <- s .triggerCh :
560+ s .setLastReplicationStart ()
493561 lastLogIdx , _ := r .getLastLog ()
494562 shouldStop = r .pipelineSend (s , pipeline , & nextIndex , lastLogIdx )
495563 case <- randomTimeout (r .config ().CommitTimeout ):
496564 lastLogIdx , _ := r .getLastLog ()
565+ s .setLastReplicationStart ()
497566 shouldStop = r .pipelineSend (s , pipeline , & nextIndex , lastLogIdx )
498567 }
499568 }
569+ s .resetLastReplicationStart ()
500570
501571 // Stop our decoder, and wait for it to finish
502572 close (stopCh )
0 commit comments