• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 15733724825

18 Jun 2025 01:09PM UTC coverage: 68.248% (+0.02%) from 68.231%
15733724825

push

github

web-flow
Merge pull request #9962 from yyforyongyu/fix-panic

chainio: use package logger instead of instance logger

5 of 7 new or added lines in 1 file covered. (71.43%)

50 existing lines in 13 files now uncovered.

134464 of 197024 relevant lines covered (68.25%)

22189.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.37
/chainio/dispatcher.go
1
package chainio
2

3
import (
4
        "errors"
5
        "fmt"
6
        "sync"
7
        "sync/atomic"
8
        "time"
9

10
        "github.com/btcsuite/btclog/v2"
11
        "github.com/lightningnetwork/lnd/chainntnfs"
12
        "github.com/lightningnetwork/lnd/lnutils"
13
        "golang.org/x/sync/errgroup"
14
)
15

16
// DefaultProcessBlockTimeout is the timeout value used when waiting for one
17
// consumer to finish processing the new block epoch.
18
var DefaultProcessBlockTimeout = 60 * time.Second
19

20
// ErrProcessBlockTimeout is the error returned when a consumer takes too long
21
// to process the block.
22
var ErrProcessBlockTimeout = errors.New("process block timeout")
23

24
// BlockbeatDispatcher is a service that handles dispatching new blocks to
25
// `lnd`'s subsystems. During startup, subsystems that are block-driven should
26
// implement the `Consumer` interface and register themselves via
27
// `RegisterQueue`. When two subsystems are independent of each other, they
28
// should be registered in different queues so blocks are notified concurrently.
29
// Otherwise, when living in the same queue, the subsystems are notified of the
30
// new blocks sequentially, which means it's critical to understand the
31
// relationship of these systems to properly handle the order.
32
type BlockbeatDispatcher struct {
33
        wg sync.WaitGroup
34

35
        // notifier is used to receive new block epochs.
36
        notifier chainntnfs.ChainNotifier
37

38
        // beat is the latest blockbeat received.
39
        beat Blockbeat
40

41
        // consumerQueues is a map of consumers that will receive blocks. Its
42
        // key is a unique counter and its value is a queue of consumers. Each
43
        // queue is notified concurrently, and consumers in the same queue is
44
        // notified sequentially.
45
        consumerQueues map[uint32][]Consumer
46

47
        // counter is used to assign a unique id to each queue.
48
        counter atomic.Uint32
49

50
        // quit is used to signal the BlockbeatDispatcher to stop.
51
        quit chan struct{}
52

53
        // queryHeightChan is used to receive queries on the current height of
54
        // the dispatcher.
55
        queryHeightChan chan *query
56
}
57

58
// query is used to fetch the internal state of the dispatcher.
59
type query struct {
60
        // respChan is used to send back the current height back to the caller.
61
        //
62
        // NOTE: This channel must be buffered.
63
        respChan chan int32
64
}
65

66
// newQuery creates a query to be used to fetch the internal state of the
67
// dispatcher.
68
func newQuery() *query {
5✔
69
        return &query{
5✔
70
                respChan: make(chan int32, 1),
5✔
71
        }
5✔
72
}
5✔
73

74
// NewBlockbeatDispatcher returns a new blockbeat dispatcher instance.
75
func NewBlockbeatDispatcher(n chainntnfs.ChainNotifier) *BlockbeatDispatcher {
9✔
76
        return &BlockbeatDispatcher{
9✔
77
                notifier:        n,
9✔
78
                quit:            make(chan struct{}),
9✔
79
                consumerQueues:  make(map[uint32][]Consumer),
9✔
80
                queryHeightChan: make(chan *query, 1),
9✔
81
        }
9✔
82
}
9✔
83

84
// RegisterQueue takes a list of consumers and registers them in the same
85
// queue.
86
//
87
// NOTE: these consumers are notified sequentially.
88
func (b *BlockbeatDispatcher) RegisterQueue(consumers []Consumer) {
10✔
89
        qid := b.counter.Add(1)
10✔
90

10✔
91
        b.consumerQueues[qid] = append(b.consumerQueues[qid], consumers...)
10✔
92
        clog.Infof("Registered queue=%d with %d blockbeat consumers", qid,
10✔
93
                len(consumers))
10✔
94

10✔
95
        for _, c := range consumers {
21✔
96
                clog.Debugf("Consumer [%s] registered in queue %d", c.Name(),
11✔
97
                        qid)
11✔
98
        }
11✔
99
}
100

101
// Start starts the blockbeat dispatcher - it registers a block notification
102
// and monitors and dispatches new blocks in a goroutine. It will refuse to
103
// start if there are no registered consumers.
104
func (b *BlockbeatDispatcher) Start() error {
7✔
105
        // Make sure consumers are registered.
7✔
106
        if len(b.consumerQueues) == 0 {
8✔
107
                return fmt.Errorf("no consumers registered")
1✔
108
        }
1✔
109

110
        // Start listening to new block epochs. We should get a notification
111
        // with the current best block immediately.
112
        blockEpochs, err := b.notifier.RegisterBlockEpochNtfn(nil)
6✔
113
        if err != nil {
7✔
114
                return fmt.Errorf("register block epoch ntfn: %w", err)
1✔
115
        }
1✔
116

117
        clog.Infof("BlockbeatDispatcher is starting with %d consumer queues",
5✔
118
                len(b.consumerQueues))
5✔
119
        defer clog.Debug("BlockbeatDispatcher started")
5✔
120

5✔
121
        b.wg.Add(1)
5✔
122
        go b.dispatchBlocks(blockEpochs)
5✔
123

5✔
124
        return nil
5✔
125
}
126

127
// Stop shuts down the blockbeat dispatcher.
128
func (b *BlockbeatDispatcher) Stop() {
5✔
129
        clog.Info("BlockbeatDispatcher is stopping")
5✔
130
        defer clog.Debug("BlockbeatDispatcher stopped")
5✔
131

5✔
132
        // Signal the dispatchBlocks goroutine to stop.
5✔
133
        close(b.quit)
5✔
134
        b.wg.Wait()
5✔
135
}
5✔
136

137
func (b *BlockbeatDispatcher) log() btclog.Logger {
13✔
138
        // There's no guarantee that the `b.beat` is initialized when the
13✔
139
        // dispatcher shuts down, especially in the case where the node is
13✔
140
        // running as a remote signer, which doesn't have a chainbackend. In
13✔
141
        // that case we will use the package logger.
13✔
142
        if b.beat == nil {
13✔
NEW
143
                return clog
×
NEW
144
        }
×
145

146
        return b.beat.logger()
13✔
147
}
148

149
// dispatchBlocks listens to new block epoch and dispatches it to all the
150
// consumers. Each queue is notified concurrently, and the consumers in the
151
// same queue are notified sequentially.
152
//
153
// NOTE: Must be run as a goroutine.
154
func (b *BlockbeatDispatcher) dispatchBlocks(
155
        blockEpochs *chainntnfs.BlockEpochEvent) {
6✔
156

6✔
157
        defer b.wg.Done()
6✔
158
        defer blockEpochs.Cancel()
6✔
159

6✔
160
        for {
14✔
161
                select {
8✔
162
                case blockEpoch, ok := <-blockEpochs.Epochs:
4✔
163
                        if !ok {
4✔
164
                                clog.Debugf("Block epoch channel closed")
×
165

×
166
                                return
×
167
                        }
×
168

169
                        // Log a separator so it's easier to identify when a
170
                        // new block arrives for subsystems.
171
                        clog.Debugf("%v", lnutils.NewSeparatorClosure())
4✔
172

4✔
173
                        clog.Debugf("Received new block %v at height %d, "+
4✔
174
                                "notifying consumers...", blockEpoch.Hash,
4✔
175
                                blockEpoch.Height)
4✔
176

4✔
177
                        // Record the time it takes the consumer to process
4✔
178
                        // this block.
4✔
179
                        start := time.Now()
4✔
180

4✔
181
                        // Update the current block epoch.
4✔
182
                        b.beat = NewBeat(*blockEpoch)
4✔
183

4✔
184
                        // Notify all consumers.
4✔
185
                        err := b.notifyQueues()
4✔
186
                        if err != nil {
4✔
187
                                b.log().Errorf("Notify block failed: %v", err)
×
188
                        }
×
189

190
                        b.log().Debugf("Notified all consumers on new block "+
4✔
191
                                "in %v", time.Since(start))
4✔
192

193
                // A query has been made to fetch the current height, we now
194
                // send the height from its current beat.
195
                case query := <-b.queryHeightChan:
4✔
196
                        // The beat may not be set yet, e.g., during the startup
4✔
197
                        // the query is made before the block epoch being sent.
4✔
198
                        height := int32(0)
4✔
199
                        if b.beat != nil {
8✔
200
                                height = b.beat.Height()
4✔
201
                        }
4✔
202

203
                        query.respChan <- height
4✔
204

205
                case <-b.quit:
5✔
206
                        b.log().Debugf("BlockbeatDispatcher quit signal " +
5✔
207
                                "received")
5✔
208

5✔
209
                        return
5✔
210
                }
211
        }
212
}
213

214
// CurrentHeight returns the current best height known to the dispatcher. 0 is
215
// returned if the dispatcher is shutting down.
216
func (b *BlockbeatDispatcher) CurrentHeight() int32 {
5✔
217
        query := newQuery()
5✔
218

5✔
219
        select {
5✔
220
        case b.queryHeightChan <- query:
4✔
221

222
        case <-b.quit:
1✔
223
                clog.Debugf("BlockbeatDispatcher quit before query")
1✔
224
                return 0
1✔
225
        }
226

227
        select {
4✔
228
        case height := <-query.respChan:
4✔
229
                clog.Debugf("Responded current height: %v", height)
4✔
230
                return height
4✔
231

232
        case <-b.quit:
×
233
                clog.Debugf("BlockbeatDispatcher quit before response")
×
234
                return 0
×
235
        }
236
}
237

238
// notifyQueues notifies each queue concurrently about the latest block epoch.
239
func (b *BlockbeatDispatcher) notifyQueues() error {
6✔
240
        // errChans is a map of channels that will be used to receive errors
6✔
241
        // returned from notifying the consumers.
6✔
242
        errChans := make(map[uint32]chan error, len(b.consumerQueues))
6✔
243

6✔
244
        // Notify each queue in goroutines.
6✔
245
        for qid, consumers := range b.consumerQueues {
13✔
246
                b.log().Debugf("Notifying queue=%d with %d consumers", qid,
7✔
247
                        len(consumers))
7✔
248

7✔
249
                // Create a signal chan.
7✔
250
                errChan := make(chan error, 1)
7✔
251
                errChans[qid] = errChan
7✔
252

7✔
253
                // Notify each queue concurrently.
7✔
254
                go func(qid uint32, c []Consumer, beat Blockbeat) {
14✔
255
                        // Notify each consumer in this queue sequentially.
7✔
256
                        errChan <- DispatchSequential(beat, c)
7✔
257
                }(qid, consumers, b.beat)
7✔
258
        }
259

260
        // Wait for all consumers in each queue to finish.
261
        for qid, errChan := range errChans {
13✔
262
                select {
7✔
263
                case err := <-errChan:
7✔
264
                        if err != nil {
8✔
265
                                return fmt.Errorf("queue=%d got err: %w", qid,
1✔
266
                                        err)
1✔
267
                        }
1✔
268

269
                        b.log().Debugf("Notified queue=%d", qid)
6✔
270

271
                case <-b.quit:
×
272
                        b.log().Debugf("BlockbeatDispatcher quit signal " +
×
273
                                "received, exit notifyQueues")
×
274

×
275
                        return nil
×
276
                }
277
        }
278

279
        return nil
5✔
280
}
281

282
// DispatchSequential takes a list of consumers and notify them about the new
283
// epoch sequentially. It requires the consumer to finish processing the block
284
// within the specified time, otherwise a timeout error is returned.
285
func DispatchSequential(b Blockbeat, consumers []Consumer) error {
8✔
286
        for _, c := range consumers {
18✔
287
                // Send the beat to the consumer.
10✔
288
                err := notifyAndWait(b, c, DefaultProcessBlockTimeout)
10✔
289
                if err != nil {
11✔
290
                        b.logger().Errorf("Failed to process block: %v", err)
1✔
291

1✔
292
                        return err
1✔
293
                }
1✔
294
        }
295

296
        return nil
7✔
297
}
298

299
// DispatchConcurrent notifies each consumer concurrently about the blockbeat.
300
// It requires the consumer to finish processing the block within the specified
301
// time, otherwise a timeout error is returned.
302
func DispatchConcurrent(b Blockbeat, consumers []Consumer) error {
3✔
303
        eg := &errgroup.Group{}
3✔
304

3✔
305
        // Notify each queue in goroutines.
3✔
306
        for _, c := range consumers {
6✔
307
                // Notify each consumer concurrently.
3✔
308
                eg.Go(func() error {
6✔
309
                        // Send the beat to the consumer.
3✔
310
                        err := notifyAndWait(b, c, DefaultProcessBlockTimeout)
3✔
311

3✔
312
                        // Exit early if there's no error.
3✔
313
                        if err == nil {
6✔
314
                                return nil
3✔
315
                        }
3✔
316

317
                        b.logger().Errorf("Consumer=%v failed to process "+
×
318
                                "block: %v", c.Name(), err)
×
319

×
320
                        return err
×
321
                })
322
        }
323

324
        // Wait for all consumers in each queue to finish.
325
        if err := eg.Wait(); err != nil {
3✔
326
                return err
×
327
        }
×
328

329
        return nil
3✔
330
}
331

332
// notifyAndWait sends the blockbeat to the specified consumer. It requires the
333
// consumer to finish processing the block within the specified time, otherwise
334
// a timeout error is returned.
335
func notifyAndWait(b Blockbeat, c Consumer, timeout time.Duration) error {
13✔
336
        b.logger().Debugf("Waiting for consumer[%s] to process it", c.Name())
13✔
337

13✔
338
        // Record the time it takes the consumer to process this block.
13✔
339
        start := time.Now()
13✔
340

13✔
341
        errChan := make(chan error, 1)
13✔
342
        go func() {
26✔
343
                errChan <- c.ProcessBlock(b)
13✔
344
        }()
13✔
345

346
        // We expect the consumer to finish processing this block under 30s,
347
        // otherwise a timeout error is returned.
348
        select {
13✔
349
        case err := <-errChan:
12✔
350
                if err == nil {
22✔
351
                        break
10✔
352
                }
353

354
                return fmt.Errorf("%s got err in ProcessBlock: %w", c.Name(),
2✔
355
                        err)
2✔
356

357
        case <-time.After(timeout):
1✔
358
                return fmt.Errorf("consumer %s: %w", c.Name(),
1✔
359
                        ErrProcessBlockTimeout)
1✔
360
        }
361

362
        b.logger().Debugf("Consumer[%s] processed block in %v", c.Name(),
10✔
363
                time.Since(start))
10✔
364

10✔
365
        return nil
10✔
366
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc