Skip to content

bft: hold the view while the view changer writes - #741

Closed
pfi79 wants to merge 0 commit into
hyperledger:mainfrom
pfi79:735-4-view-guard
Closed

pfi79 wants to merge 0 commit into
hyperledger:mainfrom
pfi79:735-4-view-guard

Conversation

@pfi79

@pfi79 pfi79 commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

AbortView stopped the current view, but the controller could restart it through the sync path before the view changer wrote its record. That allowed a commit to land after a NewView or ViewChange record, leaving a WAL sequence that recovery rejects ([NewView, Commit] or [PP, ViewChange, Commit]).

Add AbortViewAndHold/ReleaseViewWrite and a viewChangeLock so no view can be started or write between the abort and the record write. Also enqueue a decision instead of dropping it when the view is aborted, and let AbortView return without waiting for the controller loop.

#736

Comment thread internal/bft/controller.go Outdated
Comment on lines +194 to +201
// holdView keeps the controller from starting the given view again until the
// view changer asks for a new one.
func (c *Controller) holdView(view uint64) {
c.currViewLock.Lock()
defer c.currViewLock.Unlock()

c.minRestartView = max(c.minRestartView, view+1)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minRestartView is only set while viewChangeLock is held, and changeView already waits on that lock. ReleaseViewWrite then resets it to 0, so isViewHeld never returns true outside the unit test that calls holdView directly. Could it be removed? If it's meant to stay, the holdView comment ("until the view changer asks for a new one") doesn't match what it does.

(Written by Claude Code.)

Comment on lines +1053 to +1058
case <-view.AbortChan():
// The view is stopping while the controller is busy with a long sync or
// delivery. Do not block the view, or the abort would wait for it, but do
// not drop the decision either: hand it to the controller in the background.
go c.enqueueDecision(d)
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goroutine can put an aborted view's decision on decisionChan after the next view has started, or after the view changer has committed the same in-flight proposal. Is double delivery ruled out somewhere? A decision left in the buffered channel could already do this before the PR, so this may be fine.

(Written by Claude Code.)

@pfi79

pfi79 commented Oct 28, 2026

Copy link
Copy Markdown
Contributor Author

@HagarMeir fixed

@HagarMeir

Copy link
Copy Markdown
Contributor

Thanks for the fixes. This is a lot of concurrency change in one PR, and I find it hard to follow. Could you split it? Something like:

  1. Decide doesn't block or drop a decision when its view is aborted (enqueueDecision)
  2. AbortView aborts directly instead of through the controller loop
  3. The view changer aborts and holds the view before writing its ViewChange or NewView record
  4. sync() aborts the view before saving the NewView record, including the maybePruneInFlight move

Each one would then come with its own tests. Also, ViewController is internal, so AbortViewAndHold/ReleaseViewWrite could go on it directly instead of through the viewHolder type assertion.

(Written by Claude Code.)

@pfi79

pfi79 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the fixes. This is a lot of concurrency change in one PR, and I find it hard to follow. Could you split it? Something like:

  1. Decide doesn't block or drop a decision when its view is aborted (enqueueDecision)
  2. AbortView aborts directly instead of through the controller loop
  3. The view changer aborts and holds the view before writing its ViewChange or NewView record
  4. sync() aborts the view before saving the NewView record, including the maybePruneInFlight move

Each one would then come with its own tests. Also, ViewController is internal, so AbortViewAndHold/ReleaseViewWrite could go on it directly instead of through the viewHolder type assertion.

(Written by Claude Code.)

Please take a look, I've simplified the code. But if you think you need to split it into several pull requests, I'll do it.

@HagarMeir

Copy link
Copy Markdown
Contributor

Thanks for the fixes. This is a lot of concurrency change in one PR, and I find it hard to follow. Could you split it? Something like:

  1. Decide doesn't block or drop a decision when its view is aborted (enqueueDecision)
  2. AbortView aborts directly instead of through the controller loop
  3. The view changer aborts and holds the view before writing its ViewChange or NewView record
  4. sync() aborts the view before saving the NewView record, including the maybePruneInFlight move

Each one would then come with its own tests. Also, ViewController is internal, so AbortViewAndHold/ReleaseViewWrite could go on it directly instead of through the viewHolder type assertion.
(Written by Claude Code.)

Please take a look, I've simplified the code. But if you think you need to split it into several pull requests, I'll do it.

yes please try to split the work
thank you

@pfi79
pfi79 marked this pull request as draft October 29, 2026 15:08
@pfi79
pfi79 force-pushed the 735-4-view-guard branch 2 times, most recently from 06afc1d to 1b363c2 Compare November 1, 2026 12:03
@pfi79
pfi79 force-pushed the 735-4-view-guard branch 2 times, most recently from 693c5c8 to 2c2da71 Compare November 2, 2026 09:42
@pfi79 pfi79 closed this Nov 2, 2026
@pfi79
pfi79 deleted the 735-4-view-guard branch November 2, 2026 10:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants