Repository navigation
Fix lagging node view change deadlock #697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -135,12 +135,16 @@ func (vs *voteSet) registerVote(voter uint64, message *protos.Message) { | |
| vs.votes <- &vote{Message: message, sender: voter} | ||
| } | ||
|
|
||
| // nextViews tracks the highest view each node announced in a view change message, and the | ||
| // highest view this node already rebroadcast on behalf of each of them. | ||
| type nextViews struct { | ||
| n map[uint64]uint64 | ||
| n map[uint64]uint64 | ||
| helped map[uint64]uint64 | ||
| } | ||
|
|
||
| func (nv *nextViews) clear() { | ||
| nv.n = make(map[uint64]uint64) | ||
| nv.helped = make(map[uint64]uint64) | ||
| } | ||
|
|
||
| func (nv *nextViews) registerNext(next uint64, sender uint64) { | ||
|
|
@@ -155,6 +159,18 @@ func (nv *nextViews) sendRecv(next uint64, sender uint64) bool { | |
| return next == nv.n[sender] | ||
| } | ||
|
|
||
| // shouldHelp reports whether this node should rebroadcast the view change a lagging node | ||
| // announced for the given view. Repeating the same announcement back only adds noise that the | ||
| // lagging node cannot act on anyway, so each view is helped at most once per node. | ||
| func (nv *nextViews) shouldHelp(next uint64, sender uint64) bool { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the goal is less log noise, a time-based rate limit per sender (e.g. at most once per |
||
| if next <= nv.helped[sender] { | ||
| return false | ||
| } | ||
|
|
||
| nv.helped[sender] = next | ||
| return true | ||
| } | ||
|
|
||
| type incMsg struct { | ||
| *protos.Message | ||
| sender uint64 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -83,15 +83,17 @@ type ViewChanger struct { | |
| inFlightView *View | ||
| inFlightViewLock sync.RWMutex | ||
|
|
||
| Ticker <-chan time.Time | ||
| lastTick time.Time | ||
| ResendTimeout time.Duration | ||
| lastResend time.Time | ||
| ViewChangeTimeout time.Duration | ||
| startViewChangeTime time.Time | ||
| checkTimeout bool | ||
| backOffFactor uint64 | ||
| syncedForNewView bool | ||
| Ticker <-chan time.Time | ||
| lastTick time.Time | ||
| ResendTimeout time.Duration | ||
| lastResend time.Time | ||
| ViewChangeTimeout time.Duration | ||
| startViewChangeTime time.Time | ||
| checkTimeout bool | ||
| backOffFactor uint64 | ||
| syncedForNewView bool | ||
| syncedForAnnouncedView uint64 | ||
| hasSyncedForAnnouncedView bool | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: |
||
|
|
||
| // Runtime | ||
| MetricsViewChange *api.MetricsViewChange | ||
|
|
@@ -240,15 +242,19 @@ func (v *ViewChanger) checkIfResendViewChange(now time.Time) { | |
| return | ||
| } | ||
| if v.checkTimeout { // during view change process | ||
| // Announce the view after the one this node is in, since that is the view the other | ||
| // nodes count. Announcing the view this node is already in would be equal to their | ||
| // current view, so none of them could count it. | ||
| nextView := v.currView + 1 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This resend can make the cluster skip views over and over. Once a node has a quorum for view k+1,
It also breaks the old recovery path: a peer still in view k that lost the original vote for k+1 used to get it again from this resend. It now gets k+2, which it can't count (it syncs instead). Suggestion: keep resending |
||
| msg := &protos.Message{ | ||
| Content: &protos.Message_ViewChange{ | ||
| ViewChange: &protos.ViewChange{ | ||
| NextView: v.nextView, | ||
| NextView: nextView, | ||
| }, | ||
| }, | ||
| } | ||
| v.Comm.BroadcastConsensus(msg) | ||
| v.Logger.Debugf("Node %d resent a view change message with next view %d", v.SelfID, v.nextView) | ||
| v.Logger.Debugf("Node %d resent a view change message with next view %d", v.SelfID, nextView) | ||
| v.lastResend = now // update last resend time, or at least last time we checked if we should resend | ||
| } | ||
| } | ||
|
|
@@ -282,10 +288,26 @@ func (v *ViewChanger) processMsg(sender uint64, m *protos.Message) { | |
| v.processViewChangeMsg(false) | ||
| return | ||
| } | ||
| if vc.NextView > v.currView+1 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Together with the resend change at L248, this makes honest nodes sync unnecessarily: a node already in k+1 resends k+2, and a peer still at k sees |
||
| // The sender wants to change into a view that is more than one ahead of this node. | ||
| // Either this node is behind and has to catch up, or the sender is byzantine. | ||
| // Syncing resolves both cases: if this node is behind it learns the current view | ||
| // and moves to it, and if the sender is lying nothing changes. | ||
| // It syncs at most once per announced view, so that a faulty node cannot make | ||
| // this node sync over and over again. | ||
| if !v.hasSyncedForAnnouncedView || v.syncedForAnnouncedView != vc.NextView { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This guard doesn't bound syncs against a faulty node. It remembers only the last announced view, so a node that alternates The reverse problem exists too: the slot is shared by all senders and is used even when the sync finds nothing, or when the sync was dropped because Maybe track it per sender, or only sync once f+1 distinct senders announce a view above currView+1 (at least one of them must be honest). |
||
| v.hasSyncedForAnnouncedView = true | ||
| v.syncedForAnnouncedView = vc.NextView | ||
| v.Logger.Debugf("Node %d got a view change message for view %d, while it is in view %d, syncing", v.SelfID, vc.NextView, v.currView) | ||
| v.Synchronizer.Sync() | ||
| } | ||
| return | ||
| } | ||
| if v.nextView == v.currView+1 && // node has already started view change with last view | ||
| vc.NextView > v.realView && | ||
| vc.NextView < v.currView+1 && | ||
| v.nvs.sendRecv(vc.NextView, sender) { | ||
| v.nvs.sendRecv(vc.NextView, sender) && | ||
| v.nvs.shouldHelp(vc.NextView, sender) { | ||
| // Let's help the lagging nodes. | ||
| msg := &protos.Message{ | ||
| Content: &protos.Message_ViewChange{ | ||
|
|
@@ -352,6 +374,8 @@ func (v *ViewChanger) informNewView(view uint64) { | |
| v.checkTimeout = false | ||
| v.backOffFactor = 1 // reset | ||
| v.syncedForNewView = false | ||
| v.hasSyncedForAnnouncedView = false // reset | ||
| v.syncedForAnnouncedView = 0 | ||
| v.RequestsTimer.RestartTimers() | ||
| } | ||
|
|
||
|
|
@@ -422,8 +446,10 @@ func (v *ViewChanger) processViewChangeMsg(restore bool) { | |
| v.currView = v.nextView | ||
| v.MetricsViewChange.CurrentView.Set(float64(v.currView)) | ||
| v.viewChangeMsgs.clear(v.N) | ||
| v.viewDataMsgs.clear(v.N) // clear because currView changed | ||
| v.syncedForNewView = false // clear because currView changed | ||
| v.viewDataMsgs.clear(v.N) // clear because currView changed | ||
| v.syncedForNewView = false // clear because currView changed | ||
| v.hasSyncedForAnnouncedView = false // clear because currView changed | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: this reset is now copied into both |
||
| v.syncedForAnnouncedView = 0 | ||
| msg := v.prepareViewDataMsg() | ||
| leader := v.getLeader() | ||
| if leader == v.SelfID { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adopts the peers' view without their sequence. The old check,
response.Seq == latestDecisionSeq+1, made sureresponse.Viewhad made no decisions yet. That is what makes savingLatestSequence: latestDecisionSeq, DecisionsInView: 0and returningnewProposalSequence = latestDecisionSeq+1correct.With
>, if the peers are in view V at sequence S+5 and the synchronizer returned S, the node enters V believing it has 0 decisions and its next sequence is S+1, while V has already decided S+1..S+4. WithLeaderRotation/DecisionsPerLeaderit then computes the wrong leader and rejects the real leader's proposals. It also persists this NewView, so the wrong metadata comes back after a restart.About the comment ("the synchronizer catches up the decisions"): if the synchronizer had caught up,
latestDecisionViewNumwould already have advancednewViewNumabove. This branch only matters when the synchronizer is behind the peers, which is exactly when the saved metadata is wrong. Could we instead sync again until the sequence matches, or only adopt the view whenresponse.Seq == latestDecisionSeq+1?The new test case with
peerSeq: controllerSeq + 10has the synchronizer returncontrollerSeq, which is this inconsistent state. The test asserts only the view, not the sequence or decision count.