fix(finality-grandpa): stop the voter without racing Start or leaking its reader - #4851
fix(finality-grandpa): stop the voter without racing Start or leaking its reader#4851dimartiro wants to merge 2 commits into
Conversation
|
Hey @dimartiro β I've opened #4852, which I think supersedes this. Laying out the reasoning rather than just closing, because all three problems you identified are real and two turned out to be bigger than described. What this PR gets right. I reproduced all three against
Why #4852 takes a different route. Rather than fixing each symptom, it removes the split that produces them. The voter had two lifecycle mechanisms β NewVoter(...) *Voter // constructs and runs
(*Voter) Done() <-chan error // why it stopped, once it has finished
(*Voter) VoterState() VoterState[ID]
// shutdown: close(globalIn)Each of your three dissolves rather than needing machinery:
Where your instinct was right and I was initially wrong. I argued
Four goroutines per round, forever, in a running node. The timer case is the interesting one: it wrapped an unbuffered channel in a Right diagnosis, and the mechanism you reached for was needed β just for the channels that weren't in scope here. |
Changes
Voter.Stopdid not tear down whatNewVoterandStarthad set up.The wait token is now claimed in
NewVoter, not inStart.Startblocks, so every owner runs it in a goroutine β which meansStopcan reachwg.WaitbeforeStartreacheswg.Add(1). That is an Add concurrent with a Wait, andworse than a detector warning:
Waitreturns,Stopproceeds to close channels the voter is about to poll, andStartthen blocks forever. ArunState atomic.Int32decides who releases the token βStartvia its defer, orStopwhenStartnever ran, in which case a laterStartdeclines with an error instead of coming up into a torn-down voter. The CAS is the synchronisation point.wakerChan's forwarding goroutine is now released onStop. It starts inNewVoterand ranges over the input channel, so forglobalInβ which belongs to the caller and is not the voter's to close β nothing ever ended it. It outlivedthe voter holding one item hostage on the unbuffered
out, and kept taking items off a channel a caller may well hand to the next voter.wakerChangains a stop channel and an idempotentclose(), selected on in both waits, andVoter.Stopcalls
v.globalIn.close().Stopis now idempotent. It closes channels, so a second call panicked on a closed channel. An owner that both supervises the voter and shuts the node down reaches it twice β this is the panic go-jam hit when rebuilding the voter acrossan authority-set rotation. The body moves to
stop()behind async.Oncethat records the error, so later callers block until the first has finished and get the same result.Tests
go test -race -count=2 ./pkg/finality-grandpa/...Four new tests in
voter_test.go, all failing on development β and two of them not by failing an assertion but by hanging, which is the point:The existing suite passes with -race -count=2, and golangci-lint is clean on the package.