Skip to content

Commit 1e7bc82

Browse files
authored
refactor(pkg/finality-grandpa): make closing globalIn the voter's shutdown signal (#4852)
1 parent c4b8870 commit 1e7bc82

8 files changed

Lines changed: 476 additions & 137 deletions

File tree

internal/client/consensus/grandpa/grandpa.go

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ type voterWork[
436436
E runtime.Extrinsic,
437437
] struct {
438438
voter *grandpa.Voter[H, N, primitives.AuthoritySignature, primitives.AuthorityID]
439-
voterErrChan <-chan error
439+
voterDone <-chan error
440440
sharedVoterState *SharedVoterState[primitives.AuthorityID]
441441
env *environment[H, N, Hasher, Header, E]
442442
voterCommandsRx <-chan voterCommand
@@ -554,14 +554,9 @@ func (vw *voterWork[H, N, Hasher, Header, E]) rebuildVoter() {
554554
// Repoint shared_voter_state so that the RPC endpoint can query the state
555555
vw.sharedVoterState.reset(voter.VoterState())
556556

557+
// NewVoter runs the voter; Done yields why it stopped, once it has.
557558
vw.voter = voter
558-
errChan := make(chan error)
559-
go func() {
560-
err := voter.Start()
561-
errChan <- err
562-
close(errChan)
563-
}()
564-
vw.voterErrChan = errChan
559+
vw.voterDone = voter.Done()
565560
case voterSetStatePaused[H, N]:
566561
default:
567562
panic("unreachable")
@@ -651,9 +646,9 @@ func (vw *voterWork[H, N, Hasher, Header, E]) handleVoterCommand(command voterCo
651646

652647
func (vw *voterWork[H, N, Hasher, Header, E]) poll() error {
653648
select {
654-
case err := <-vw.voterErrChan:
649+
case err := <-vw.voterDone:
655650
if err == nil {
656-
// voters don't conclude naturally
651+
// nothing here closes globalIn, so the voter has no orderly way to stop
657652
return fmt.Errorf("consensus-grandpa inner voter has concluded: %w", ErrSafety)
658653
}
659654
vc, isVoterCommand := err.(voterCommand)

pkg/finality-grandpa/bridge_state.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ func newWaker() *waker {
1717
}
1818

1919
func (w *waker) wake() {
20+
// Read under the lock and hand the value to the goroutine, which outlives the
21+
// lock and would otherwise race register's write.
2022
w.RLock()
21-
defer w.RUnlock()
22-
if w.wakeCh == nil {
23+
ch := w.wakeCh
24+
w.RUnlock()
25+
if ch == nil {
2326
return
2427
}
2528
go func() {
26-
w.wakeCh <- struct{}{}
29+
ch <- struct{}{}
2730
}()
2831
}
2932

pkg/finality-grandpa/environment_test.go

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ type environment struct {
2626
network *Network
2727
listeners []chan listenerItem
2828
lastCompleteAndConcluded [2]uint64
29-
mtx sync.Mutex
29+
// roundIn holds the inbound channels handed to the voter, per round, so they
30+
// can be closed once the round concludes. RoundData is called more than once
31+
// for a round number, hence a slice.
32+
roundIn map[uint64][]chan SignedMessageError[string, uint32, Signature, ID]
33+
mtx sync.Mutex
3034

3135
concludedCalled chan struct{}
3236
}
@@ -36,6 +40,7 @@ func newEnvironment(network *Network, localID ID) environment {
3640
chain: newDummyChain(),
3741
localID: localID,
3842
network: network,
43+
roundIn: make(map[uint64][]chan SignedMessageError[string, uint32, Signature, ID]),
3944
concludedCalled: make(chan struct{}),
4045
}
4146
}
@@ -84,6 +89,12 @@ func (e *environment) RoundData(
8489
outgoing := make(Output[string, uint32])
8590
incoming := e.network.MakeRoundComms(round, e.localID, outgoing)
8691

92+
// Remember it so Concluded can close it: the voter reads this channel through
93+
// a forwarding goroutine that ends only when the channel does.
94+
e.mtx.Lock()
95+
e.roundIn[round] = append(e.roundIn[round], incoming)
96+
e.mtx.Unlock()
97+
8798
var outgoingFunc = func(m Message[string, uint32]) error {
8899
outgoing <- m
89100
return nil
@@ -123,8 +134,16 @@ func (e *environment) Concluded(
123134
_ HistoricalVotes[string, uint32, Signature, ID],
124135
) error {
125136
e.mtx.Lock()
126-
defer e.mtx.Unlock()
127137
e.lastCompleteAndConcluded[1] = round
138+
incoming := e.roundIn[round]
139+
delete(e.roundIn, round)
140+
e.mtx.Unlock()
141+
142+
// The round is over, so release the inbound channels handed out for it.
143+
for _, in := range incoming {
144+
e.network.StopRoundComms(round, in)
145+
}
146+
128147
go func() {
129148
e.concludedCalled <- struct{}{}
130149
}()
@@ -259,13 +278,29 @@ func (bm *BroadcastNetwork[M, N]) AddNode(f func(N) M, out chan N) (in chan M) {
259278
func (bm *BroadcastNetwork[M, N]) route() {
260279
defer bm.routeWG.Done()
261280
for msg := range bm.receiver {
281+
// Under the lock: RemoveNode closes a node's channel, and closing one a
282+
// producer is about to send on panics. Senders are buffered, so holding it
283+
// across the delivery does not block.
262284
bm.mu.Lock()
263285
bm.history = append(bm.history, msg)
264-
senders := append([]chan M(nil), bm.senders...)
265-
bm.mu.Unlock()
266-
for _, sender := range senders {
286+
for _, sender := range bm.senders {
267287
sender <- msg
268288
}
289+
bm.mu.Unlock()
290+
}
291+
}
292+
293+
// RemoveNode deregisters a node's inbound channel and closes it, shutting down
294+
// the voter reading it. Held under bm.mu so it cannot race a delivery in route.
295+
func (bm *BroadcastNetwork[M, N]) RemoveNode(in chan M) {
296+
bm.mu.Lock()
297+
defer bm.mu.Unlock()
298+
for i, sender := range bm.senders {
299+
if sender == in {
300+
bm.senders = append(bm.senders[:i], bm.senders[i+1:]...)
301+
close(in)
302+
return
303+
}
269304
}
270305
}
271306

@@ -402,6 +437,31 @@ func (n *Network) MakeGlobalComms(
402437
}, out)
403438
}
404439

440+
// StopRoundComms closes one inbound channel handed out by MakeRoundComms. Only
441+
// that node's channel: the round network is shared, and other voters may still
442+
// be in this round.
443+
func (n *Network) StopRoundComms(
444+
roundNumber uint64,
445+
in chan SignedMessageError[string, uint32, Signature, ID],
446+
) {
447+
n.mtx.Lock()
448+
round, ok := n.rounds[roundNumber]
449+
n.mtx.Unlock()
450+
451+
if ok {
452+
round.RemoveNode(in)
453+
}
454+
}
455+
456+
// StopGlobalComms closes the inbound channel handed to a voter by
457+
// MakeGlobalComms, which is how that voter is shut down.
458+
func (n *Network) StopGlobalComms(in chan GlobalInItem[string, uint32, Signature, ID]) {
459+
n.mtx.Lock()
460+
defer n.mtx.Unlock()
461+
462+
n.globalMessages.RemoveNode(in)
463+
}
464+
405465
func (n *Network) SendMessage(message CommunicationIn[string, uint32, Signature, ID]) {
406466
n.globalMessages.SendMessage(GlobalInItem[string, uint32, Signature, ID]{message, nil})
407467
}

pkg/finality-grandpa/timer.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,39 +9,46 @@ import (
99
"time"
1010
)
1111

12+
// timer reports whether a deadline has passed and wakes whoever is polling it
13+
// when that changes. Rounds create several and discard them as they advance, so
14+
// Close releases one whose round finished before it fired.
1215
type timer struct {
13-
wakerChan *wakerChan[error]
16+
waker atomic.Pointer[waker]
17+
stop chan struct{}
1418
closeOnce sync.Once
1519
expired atomic.Bool
1620
}
1721

1822
func newTimer(in <-chan time.Time) *timer {
19-
inErr := make(chan error)
20-
wc := newWakerChan(inErr)
21-
t := timer{wakerChan: wc}
23+
t := timer{stop: make(chan struct{})}
2224
go t.poll(in)
2325
return &t
2426
}
2527

2628
func (t *timer) poll(in <-chan time.Time) {
27-
<-in
28-
t.closeOnce.Do(func() {
29-
t.wakerChan.in <- nil
30-
close(t.wakerChan.in)
31-
})
29+
select {
30+
case <-in:
31+
case <-t.stop:
32+
return
33+
}
34+
// Ordered: waking before expired is set would send the poller back to sleep
35+
// having seen the timer as still pending.
3236
t.expired.Store(true)
37+
if w := t.waker.Load(); w != nil {
38+
w.wake()
39+
}
3340
}
3441

3542
func (t *timer) SetWaker(waker *waker) {
36-
t.wakerChan.setWaker(waker)
43+
t.waker.Store(waker)
3744
}
3845

3946
func (t *timer) Elapsed() (bool, error) {
4047
return t.expired.Load(), nil
4148
}
4249

50+
// Close releases a timer that has not fired. Idempotent, and a no-op once the
51+
// timer has elapsed.
4352
func (t *timer) Close() {
44-
t.closeOnce.Do(func() {
45-
close(t.wakerChan.in)
46-
})
53+
t.closeOnce.Do(func() { close(t.stop) })
4754
}

0 commit comments

Comments
 (0)