Skip to content

Commit 46df219

Browse files
committed
fix for disk stalls on leader
Have the leader set a timestamp at the start of each replication for each follower, and clear it on either success or failure. The heartbeater will check this timestamp if set, and if has been set for too long, delay the next heartbeat so that the leader can eventually step down. Fixes: #666 Fixes: #503 Fixes: #612 Fixes: #614
1 parent d207e12 commit 46df219

2 files changed

Lines changed: 73 additions & 3 deletions

File tree

raft.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515

1616
"github.com/hashicorp/go-hclog"
1717

18-
"github.com/hashicorp/go-metrics/compat"
18+
metrics "github.com/hashicorp/go-metrics/compat"
1919
)
2020

2121
const (

replication.go

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright IBM Corp. 2013, 2025
1+
// Copyright IBM Corp. 2013, 2026
22
// SPDX-License-Identifier: MPL-2.0
33

44
package 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

1616
const (
@@ -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.
137169
func (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+
143177
RPC:
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 {
@@ -159,6 +196,7 @@ RPC:
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
473537
SEND:
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

Comments
 (0)