@@ -2,7 +2,6 @@ package redis
2
2
3
3
import (
4
4
"context"
5
- "errors"
6
5
"fmt"
7
6
"strings"
8
7
"sync"
@@ -13,13 +12,6 @@ import (
13
12
"github.com/go-redis/redis/v8/internal/proto"
14
13
)
15
14
16
- const (
17
- pingTimeout = time .Second
18
- chanSendTimeout = time .Minute
19
- )
20
-
21
- var errPingTimeout = errors .New ("redis: ping timeout" )
22
-
23
15
// PubSub implements Pub/Sub commands as described in
24
16
// http://redis.io/topics/pubsub. Message receiving is NOT safe
25
17
// for concurrent use by multiple goroutines.
@@ -43,9 +35,12 @@ type PubSub struct {
43
35
cmd * Cmd
44
36
45
37
chOnce sync.Once
46
- msgCh chan * Message
47
- allCh chan interface {}
48
- ping chan struct {}
38
+ msgCh * channel
39
+ allCh * channel
40
+ }
41
+
42
+ func (c * PubSub ) init () {
43
+ c .exit = make (chan struct {})
49
44
}
50
45
51
46
func (c * PubSub ) String () string {
@@ -54,10 +49,6 @@ func (c *PubSub) String() string {
54
49
return fmt .Sprintf ("PubSub(%s)" , strings .Join (channels , ", " ))
55
50
}
56
51
57
- func (c * PubSub ) init () {
58
- c .exit = make (chan struct {})
59
- }
60
-
61
52
func (c * PubSub ) connWithLock (ctx context.Context ) (* pool.Conn , error ) {
62
53
c .mu .Lock ()
63
54
cn , err := c .conn (ctx , nil )
@@ -418,110 +409,160 @@ func (c *PubSub) ReceiveMessage(ctx context.Context) (*Message, error) {
418
409
}
419
410
}
420
411
412
+ func (c * PubSub ) getContext () context.Context {
413
+ if c .cmd != nil {
414
+ return c .cmd .ctx
415
+ }
416
+ return context .Background ()
417
+ }
418
+
419
+ //------------------------------------------------------------------------------
420
+
421
421
// Channel returns a Go channel for concurrently receiving messages.
422
422
// The channel is closed together with the PubSub. If the Go channel
423
423
// is blocked full for 30 seconds the message is dropped.
424
424
// Receive* APIs can not be used after channel is created.
425
425
//
426
426
// go-redis periodically sends ping messages to test connection health
427
427
// and re-subscribes if ping can not not received for 30 seconds.
428
- func (c * PubSub ) Channel () <- chan * Message {
429
- return c .ChannelSize (100 )
430
- }
431
-
432
- // ChannelSize is like Channel, but creates a Go channel
433
- // with specified buffer size.
434
- func (c * PubSub ) ChannelSize (size int ) <- chan * Message {
428
+ func (c * PubSub ) Channel (opts ... ChannelOption ) <- chan * Message {
435
429
c .chOnce .Do (func () {
436
- c .initPing ( )
437
- c .initMsgChan (size )
430
+ c .msgCh = newChannel ( c , opts ... )
431
+ c .msgCh . initMsgChan ()
438
432
})
439
433
if c .msgCh == nil {
440
434
err := fmt .Errorf ("redis: Channel can't be called after ChannelWithSubscriptions" )
441
435
panic (err )
442
436
}
443
- if cap (c .msgCh ) != size {
444
- err := fmt .Errorf ("redis: PubSub.Channel size can not be changed once created" )
445
- panic (err )
446
- }
447
- return c .msgCh
437
+ return c .msgCh .msgCh
438
+ }
439
+
440
+ // ChannelSize is like Channel, but creates a Go channel
441
+ // with specified buffer size.
442
+ //
443
+ // Deprecated: use Channel(WithChannelSize(size)), remove in v9.
444
+ func (c * PubSub ) ChannelSize (size int ) <- chan * Message {
445
+ return c .Channel (WithChannelSize (size ))
448
446
}
449
447
450
448
// ChannelWithSubscriptions is like Channel, but message type can be either
451
449
// *Subscription or *Message. Subscription messages can be used to detect
452
450
// reconnections.
453
451
//
454
452
// ChannelWithSubscriptions can not be used together with Channel or ChannelSize.
455
- func (c * PubSub ) ChannelWithSubscriptions (ctx context.Context , size int ) <- chan interface {} {
453
+ func (c * PubSub ) ChannelWithSubscriptions (_ context.Context , size int ) <- chan interface {} {
456
454
c .chOnce .Do (func () {
457
- c .initPing ( )
458
- c .initAllChan (size )
455
+ c .allCh = newChannel ( c , WithChannelSize ( size ) )
456
+ c .allCh . initAllChan ()
459
457
})
460
458
if c .allCh == nil {
461
459
err := fmt .Errorf ("redis: ChannelWithSubscriptions can't be called after Channel" )
462
460
panic (err )
463
461
}
464
- if cap (c .allCh ) != size {
465
- err := fmt .Errorf ("redis: PubSub.Channel size can not be changed once created" )
466
- panic (err )
462
+ return c .allCh .allCh
463
+ }
464
+
465
+ type ChannelOption func (c * channel )
466
+
467
+ // WithChannelSize specifies the Go chan size that is used to buffer incoming messages.
468
+ //
469
+ // The default is 100 messages.
470
+ func WithChannelSize (size int ) ChannelOption {
471
+ return func (c * channel ) {
472
+ c .chanSize = size
467
473
}
468
- return c .allCh
469
474
}
470
475
471
- func (c * PubSub ) getContext () context.Context {
472
- if c .cmd != nil {
473
- return c .cmd .ctx
476
+ // WithChannelHealthCheckInterval specifies the health check interval.
477
+ // PubSub will ping Redis Server if it does not receive any messages within the interval.
478
+ // To disable health check, use zero interval.
479
+ //
480
+ // The default is 3 seconds.
481
+ func WithChannelHealthCheckInterval (d time.Duration ) ChannelOption {
482
+ return func (c * channel ) {
483
+ c .checkInterval = d
474
484
}
475
- return context .Background ()
476
485
}
477
486
478
- func (c * PubSub ) initPing () {
487
+ // WithChannelSendTimeout specifies that channel send timeout after which
488
+ // the message is dropped.
489
+ //
490
+ // The default is 60 seconds.
491
+ func WithChannelSendTimeout (d time.Duration ) ChannelOption {
492
+ return func (c * channel ) {
493
+ c .chanSendTimeout = d
494
+ }
495
+ }
496
+
497
+ type channel struct {
498
+ pubSub * PubSub
499
+
500
+ msgCh chan * Message
501
+ allCh chan interface {}
502
+ ping chan struct {}
503
+
504
+ chanSize int
505
+ chanSendTimeout time.Duration
506
+ checkInterval time.Duration
507
+ }
508
+
509
+ func newChannel (pubSub * PubSub , opts ... ChannelOption ) * channel {
510
+ c := & channel {
511
+ pubSub : pubSub ,
512
+
513
+ chanSize : 100 ,
514
+ chanSendTimeout : time .Minute ,
515
+ checkInterval : 3 * time .Second ,
516
+ }
517
+ for _ , opt := range opts {
518
+ opt (c )
519
+ }
520
+ if c .checkInterval > 0 {
521
+ c .initHealthCheck ()
522
+ }
523
+ return c
524
+ }
525
+
526
+ func (c * channel ) initHealthCheck () {
479
527
ctx := context .TODO ()
480
528
c .ping = make (chan struct {}, 1 )
529
+
481
530
go func () {
482
531
timer := time .NewTimer (time .Minute )
483
532
timer .Stop ()
484
533
485
- healthy := true
486
534
for {
487
- timer .Reset (pingTimeout )
535
+ timer .Reset (c . checkInterval )
488
536
select {
489
537
case <- c .ping :
490
- healthy = true
491
538
if ! timer .Stop () {
492
539
<- timer .C
493
540
}
494
541
case <- timer .C :
495
- pingErr := c .Ping (ctx )
496
- if healthy {
497
- healthy = false
498
- } else {
499
- if pingErr == nil {
500
- pingErr = errPingTimeout
501
- }
502
- c .mu .Lock ()
503
- c .reconnect (ctx , pingErr )
504
- healthy = true
505
- c .mu .Unlock ()
542
+ if pingErr := c .pubSub .Ping (ctx ); pingErr != nil {
543
+ c .pubSub .mu .Lock ()
544
+ c .pubSub .reconnect (ctx , pingErr )
545
+ c .pubSub .mu .Unlock ()
506
546
}
507
- case <- c .exit :
547
+ case <- c .pubSub . exit :
508
548
return
509
549
}
510
550
}
511
551
}()
512
552
}
513
553
514
554
// initMsgChan must be in sync with initAllChan.
515
- func (c * PubSub ) initMsgChan (size int ) {
555
+ func (c * channel ) initMsgChan () {
516
556
ctx := context .TODO ()
517
- c .msgCh = make (chan * Message , size )
557
+ c .msgCh = make (chan * Message , c .chanSize )
558
+
518
559
go func () {
519
560
timer := time .NewTimer (time .Minute )
520
561
timer .Stop ()
521
562
522
563
var errCount int
523
564
for {
524
- msg , err := c .Receive (ctx )
565
+ msg , err := c .pubSub . Receive (ctx )
525
566
if err != nil {
526
567
if err == pool .ErrClosed {
527
568
close (c .msgCh )
@@ -548,38 +589,36 @@ func (c *PubSub) initMsgChan(size int) {
548
589
case * Pong :
549
590
// Ignore.
550
591
case * Message :
551
- timer .Reset (chanSendTimeout )
592
+ timer .Reset (c . chanSendTimeout )
552
593
select {
553
594
case c .msgCh <- msg :
554
595
if ! timer .Stop () {
555
596
<- timer .C
556
597
}
557
598
case <- timer .C :
558
599
internal .Logger .Printf (
559
- c .getContext (),
560
- "redis: %s channel is full for %s (message is dropped)" ,
561
- c ,
562
- chanSendTimeout ,
563
- )
600
+ ctx , "redis: %s channel is full for %s (message is dropped)" ,
601
+ c , c .chanSendTimeout )
564
602
}
565
603
default :
566
- internal .Logger .Printf (c . getContext () , "redis: unknown message type: %T" , msg )
604
+ internal .Logger .Printf (ctx , "redis: unknown message type: %T" , msg )
567
605
}
568
606
}
569
607
}()
570
608
}
571
609
572
610
// initAllChan must be in sync with initMsgChan.
573
- func (c * PubSub ) initAllChan (size int ) {
611
+ func (c * channel ) initAllChan () {
574
612
ctx := context .TODO ()
575
- c .allCh = make (chan interface {}, size )
613
+ c .allCh = make (chan interface {}, c .chanSize )
614
+
576
615
go func () {
577
- timer := time .NewTimer (pingTimeout )
616
+ timer := time .NewTimer (time . Minute )
578
617
timer .Stop ()
579
618
580
619
var errCount int
581
620
for {
582
- msg , err := c .Receive (ctx )
621
+ msg , err := c .pubSub . Receive (ctx )
583
622
if err != nil {
584
623
if err == pool .ErrClosed {
585
624
close (c .allCh )
@@ -601,29 +640,23 @@ func (c *PubSub) initAllChan(size int) {
601
640
}
602
641
603
642
switch msg := msg .(type ) {
604
- case * Subscription :
605
- c .sendMessage (msg , timer )
606
643
case * Pong :
607
644
// Ignore.
608
- case * Message :
609
- c .sendMessage (msg , timer )
645
+ case * Subscription , * Message :
646
+ timer .Reset (c .chanSendTimeout )
647
+ select {
648
+ case c .allCh <- msg :
649
+ if ! timer .Stop () {
650
+ <- timer .C
651
+ }
652
+ case <- timer .C :
653
+ internal .Logger .Printf (
654
+ ctx , "redis: %s channel is full for %s (message is dropped)" ,
655
+ c , c .chanSendTimeout )
656
+ }
610
657
default :
611
- internal .Logger .Printf (c . getContext () , "redis: unknown message type: %T" , msg )
658
+ internal .Logger .Printf (ctx , "redis: unknown message type: %T" , msg )
612
659
}
613
660
}
614
661
}()
615
662
}
616
-
617
- func (c * PubSub ) sendMessage (msg interface {}, timer * time.Timer ) {
618
- timer .Reset (pingTimeout )
619
- select {
620
- case c .allCh <- msg :
621
- if ! timer .Stop () {
622
- <- timer .C
623
- }
624
- case <- timer .C :
625
- internal .Logger .Printf (
626
- c .getContext (),
627
- "redis: %s channel is full for %s (message is dropped)" , c , pingTimeout )
628
- }
629
- }
0 commit comments