-
-
Notifications
You must be signed in to change notification settings - Fork 121
Expand file tree
/
Copy pathbroker_redis.go
More file actions
1987 lines (1786 loc) · 64.4 KB
/
Copy pathbroker_redis.go
File metadata and controls
1987 lines (1786 loc) · 64.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package centrifuge
import (
"bytes"
"context"
"errors"
"fmt"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
_ "embed"
"github.com/centrifugal/centrifuge/internal/convert"
"github.com/centrifugal/centrifuge/internal/epoch"
"github.com/centrifugal/centrifuge/internal/redispartition"
"github.com/centrifugal/protocol"
"github.com/redis/rueidis"
)
var (
errPubSubConnUnavailable = errors.New("redis: pub/sub connection temporary unavailable")
)
const (
// redisSubscribeBatchLimit is a maximum number of channels to include in a single
// batch subscribe call.
redisSubscribeBatchLimit = 512
// redisControlChannelSuffix is a suffix for control channel.
redisControlChannelSuffix = ".control"
// redisNodeChannelPrefix is a suffix for node channel.
redisNodeChannelPrefix = ".node."
// redisClientChannelPrefix is a prefix before channel name for client messages.
redisClientChannelPrefix = ".client."
// redisPubSubShardChannelSuffix is a suffix in channel name which we use to establish a sharded PUB/SUB connection.
redisPubSubShardChannelSuffix = ".shard"
)
var _ Broker = (*RedisBroker)(nil)
var _ Controller = (*RedisBroker)(nil)
type pubSubStart struct {
once sync.Once
errCh chan error
}
type controlPubSubStart struct {
once sync.Once
errCh chan error
}
type shardWrapper struct {
shard *RedisShard
subClientsMu sync.Mutex
subClients [][]rueidis.DedicatedClient
pubSubStartChannels [][]*pubSubStart
controlPubSubStart *controlPubSubStart
logFields map[string]any
pubSubRunner brokerPubSubRunner
}
// brokerPubSubRunner abstracts the subscriber-side pub/sub strategy for
// RedisBroker. One instance per shard, selected at construction time. init is
// called from NewRedisBroker (no goroutines), run from RegisterBrokerEventHandler
// (launches subscribers and returns; readiness is signaled via
// shardWrapper.pubSubStartChannels). subClientsIndex maps a partition-derived
// cluster shard index to the first-dimension index used in
// shardWrapper.subClients (default is identity; non-default strategies may
// remap, e.g. partition → node).
type brokerPubSubRunner interface {
init(s *shardWrapper, shard *RedisShard) error
run(s *shardWrapper, h BrokerEventHandler) error
subClientsIndex(clusterShardIdx int) int
}
// RedisBroker uses Redis to implement Broker functionality. This broker allows
// scaling Centrifuge-based server to many instances and load balance client
// connections between them. Centrifuge nodes will be connected over Redis PUB/SUB.
// RedisBroker supports standalone Redis, Redis in master-replica setup with Sentinel,
// Redis Cluster. Also, it supports client-side consistent sharding between isolated
// Redis setups.
// By default, Redis >= 5 required (due to the fact RedisBroker uses STREAM data
// structure to keep publication history for a channel).
type RedisBroker struct {
controlRound uint64
node *Node
sharding bool
config RedisBrokerConfig
shards []*shardWrapper
// partitionTags is non-nil when UsePrecomputedPartitionTags is enabled;
// indexed by partition index, returns the hash tag string. Read-only
// after construction (shared with the package-level precomputed table).
partitionTags []string
publishIdempotentScript *rueidis.Lua
historyListScript *rueidis.Lua
historyStreamScript *rueidis.Lua
addHistoryListScript *rueidis.Lua
addHistoryStreamScript *rueidis.Lua
shardChannel string
messagePrefix string
controlChannel string
nodeChannel string
closeOnce sync.Once
closeCh chan struct{}
}
// RedisBrokerConfig is a config for Broker.
type RedisBrokerConfig struct {
// Prefix to use before every channel name and key in Redis. By default,
// RedisBroker will use prefix "centrifuge".
Prefix string
// Shards is a slice of RedisShard to use. At least one shard must be provided.
// Data will be consistently sharded by channel over provided Redis shards.
Shards []*RedisShard
// UseLists allows enabling usage of Redis LIST instead of STREAM data
// structure to keep history. LIST support exist mostly for backward
// compatibility since STREAM seems superior. If you have a use case
// where you need to turn on this option in new setup - please share,
// otherwise LIST support can be removed at some point in the future.
// Iteration over history in reversed order not supported with lists.
UseLists bool
// Subscribe on replica Redis nodes. This only works for Redis Cluster
// and Sentinel setups and requires replica client to be initialized in
// each RedisShard using RedisShardConfig.ReplicaClientEnabled.
SubscribeOnReplica bool
// SkipPubSub enables mode when Redis broker only saves history, without
// publishing to channels and using PUB/SUB.
SkipPubSub bool
// Name of broker, for observability purposes – i.e. becomes part of metrics/logs.
// By default, empty string is used.
Name string
// NumShardedPubSubPartitions when greater than zero allows turning on a mode in which
// broker will use Redis Cluster with sharded PUB/SUB feature available in
// Redis >= 7: https://redis.io/docs/manual/pubsub/#sharded-pubsub
//
// To achieve sharded PUB/SUB efficiency RedisBroker reduces 16384 Redis Cluster
// slots to the NumShardedPubSubPartitions value and starts a separate PUB/SUB for each
// partition. This is necessary because in Centrifuge case one node can work with
// thousands of different channels – and we can't afford running a separate
// PUB/SUB connection for each of 16384 possible slots. We re-use partition
// connection for many channels and make sure that all channels in the partition
// point to the same Redis Cluster slot.
//
// By default, sharded PUB/SUB is not used in Redis Cluster case - Centrifuge uses
// globally distributed PUBLISH commands in Redis Cluster where each publish is
// distributed to all nodes in Redis Cluster.
//
// Note (!), that turning on NumShardedPubSubPartitions will cause Centrifuge to generate
// different key names for history and different Redis channel names than in the base
// Redis Cluster mode due to reasons outlined above.
NumShardedPubSubPartitions int
// UsePrecomputedPartitionTags switches sharded PUB/SUB partition hash tags
// from the bare partition index ("0", "1", ...) to a precomputed table
// chosen so CRC16 hash slots distribute evenly across any cluster size.
// The bare-index scheme can collide badly on larger clusters — see
// https://github.com/centrifugal/centrifuge/issues/554. Enabling this
// option spreads partitions across slots so cluster nodes get balanced
// PUB/SUB load even at higher shard counts.
//
// When enabled, NumShardedPubSubPartitions must equal one of the sizes
// returned by redispartition.PrecomputedSizes() (16, 32, 64, 128, 256,
// 512, 1024, 2048, 4096). Construction returns an error otherwise. The
// exact tag table is bundled in internal/redispartition/precomputed.go;
// the tags are stable and will not change.
//
// Toggling this option changes the Redis key/channel naming scheme, so
// flipping it on a running deployment requires a coordinated restart
// with state cleared (existing channels and history will appear under
// different keys).
//
// Default: false (backward-compatible bare-index tags).
UsePrecomputedPartitionTags bool
// numSubscribeShards defines how many subscribe shards will be used by Centrifuge.
// Each subscribe shard uses a dedicated connection to Redis for making subscriptions.
// Zero value means 1.
numSubscribeShards int
// numResubscribeShards defines how many subscriber goroutines will be used by
// Centrifuge for resubscribing process for each subscribe shard. Zero value tells
// Centrifuge to use 16 subscriber goroutines per subscribe shard.
numResubscribeShards int
// pubSubProbeInterval configures how often the broker verifies that its
// PUB/SUB connections still deliver messages — both the client ones and
// the control connection. A connection that received nothing for this
// interval gets a small probe published to its service channel through
// the regular publish path; if a further interval passes without
// receiving anything, the connection is considered stale and
// re-established. This protects from the state where a connection
// remains healthy on the TCP level but is attached to a Redis node which
// no longer receives published traffic — possible after a failover (see
// centrifugal/centrifugo#1189). Probes are only sent on idle connections,
// so the mechanism costs nothing under regular message flow.
// Zero value means defaultPubSubProbeInterval, values below
// minPubSubProbeInterval are raised to it, a negative value disables
// probing. Not exported: the default is expected to work everywhere, the
// knob only exists for tests.
pubSubProbeInterval time.Duration
// numPubSubProcessors allows configuring number of workers which will process
// messages coming from Redis PUB/SUB. Zero value tells Centrifuge to use the
// number calculated as:
// runtime.NumCPU / numSubscribeShards / NumShardedPubSubPartitions (if used) (minimum 1).
numPubSubProcessors int
// LoadSHA1 enables loading SHA1 from Redis via SCRIPT LOAD instead of calculating
// it on the client side. This is useful for FIPS compliance.
LoadSHA1 bool
}
// NewRedisBroker initializes Redis Broker.
func NewRedisBroker(n *Node, config RedisBrokerConfig) (*RedisBroker, error) {
if len(config.Shards) == 0 {
return nil, errors.New("broker: no Redis shards provided in configuration")
}
if config.SubscribeOnReplica {
for i, s := range config.Shards {
if s.replicaClient == nil {
return nil, fmt.Errorf("broker: SubscribeOnReplica enabled but no replica client initialized in shard[%d] (ReplicaClientEnabled option)", i)
}
}
}
if len(config.Shards) > 1 {
n.logger.log(newLogEntry(LogLevelInfo, fmt.Sprintf("broker: Redis sharding enabled: %d shards", len(config.Shards)), map[string]any{"broker_name": config.Name}))
}
if config.Prefix == "" {
config.Prefix = "centrifuge"
}
if config.numSubscribeShards == 0 {
config.numSubscribeShards = 1
}
if config.numResubscribeShards == 0 {
config.numResubscribeShards = 16
}
config.pubSubProbeInterval = normalizePubSubProbeInterval(config.pubSubProbeInterval)
if config.numPubSubProcessors == 0 {
config.numPubSubProcessors = runtime.NumCPU() / config.numSubscribeShards
if config.NumShardedPubSubPartitions > 0 {
config.numPubSubProcessors /= config.NumShardedPubSubPartitions
}
if config.numPubSubProcessors < 1 {
config.numPubSubProcessors = 1
}
}
var partitionTags []string
if config.UsePrecomputedPartitionTags {
if config.NumShardedPubSubPartitions <= 0 {
return nil, errors.New("broker: UsePrecomputedPartitionTags requires NumShardedPubSubPartitions > 0")
}
tags, err := redispartition.FindTags(config.NumShardedPubSubPartitions)
if err != nil {
return nil, fmt.Errorf("broker: %w", err)
}
partitionTags = tags
}
shardWrappers := make([]*shardWrapper, 0, len(config.Shards))
for _, s := range config.Shards {
logFields := map[string]any{
"shard": s.string(),
}
if config.Name != "" {
logFields["broker_name"] = config.Name
}
shardWrappers = append(shardWrappers, &shardWrapper{shard: s, logFields: logFields})
}
b := &RedisBroker{
node: n,
config: config,
shards: shardWrappers,
partitionTags: partitionTags,
sharding: len(config.Shards) > 1,
publishIdempotentScript: rueidis.NewLuaScript(publishIdempotentSource, rueidis.WithLoadSHA1(config.LoadSHA1)),
historyStreamScript: rueidis.NewLuaScript(historyStreamSource, rueidis.WithLoadSHA1(config.LoadSHA1)),
historyListScript: rueidis.NewLuaScript(historyListSource, rueidis.WithLoadSHA1(config.LoadSHA1)),
addHistoryStreamScript: rueidis.NewLuaScript(addHistoryStreamSource, rueidis.WithLoadSHA1(config.LoadSHA1)),
addHistoryListScript: rueidis.NewLuaScript(addHistoryListSource, rueidis.WithLoadSHA1(config.LoadSHA1)),
closeCh: make(chan struct{}),
}
b.shardChannel = config.Prefix + redisPubSubShardChannelSuffix
b.messagePrefix = config.Prefix + redisClientChannelPrefix
b.nodeChannel = string(b.nodeChannelID(n.ID()))
b.controlChannel = config.Prefix + redisControlChannelSuffix
for _, sw := range b.shards {
shard := sw.shard
if !shard.isCluster && b.config.NumShardedPubSubPartitions > 0 {
return nil, errors.New("can use sharded PUB/SUB feature (non-zero number of pub/sub partitions) only with Redis Cluster")
}
if newBrokerPubSubRunnerHook != nil {
sw.pubSubRunner = newBrokerPubSubRunnerHook(b, shard)
}
if sw.pubSubRunner == nil {
sw.pubSubRunner = &defaultBrokerPubSubRunner{broker: b}
}
if err := sw.pubSubRunner.init(sw, shard); err != nil {
return nil, err
}
}
return b, nil
}
// newBrokerPubSubRunnerHook is an optional package-private hook used by
// auxiliary modules to install an alternative pub/sub runner. The default
// implementation returns nil so the broker falls back to defaultBrokerPubSubRunner.
var newBrokerPubSubRunnerHook func(b *RedisBroker, shard *RedisShard) brokerPubSubRunner
// defaultBrokerPubSubRunner is the standard partition-sharded pub/sub runner.
// All per-shard state lives on the shardWrapper, the runner is stateless beyond
// a back-pointer to the broker.
type defaultBrokerPubSubRunner struct {
broker *RedisBroker
}
func (r *defaultBrokerPubSubRunner) init(s *shardWrapper, shard *RedisShard) error {
b := r.broker
subChannels := make([][]rueidis.DedicatedClient, 0)
pubSubStartChannels := make([][]*pubSubStart, 0)
if b.useShardedPubSub(shard) {
for i := 0; i < b.config.NumShardedPubSubPartitions; i++ {
subChannels = append(subChannels, make([]rueidis.DedicatedClient, 0))
pubSubStartChannels = append(pubSubStartChannels, make([]*pubSubStart, 0))
}
} else {
subChannels = append(subChannels, make([]rueidis.DedicatedClient, 0))
pubSubStartChannels = append(pubSubStartChannels, make([]*pubSubStart, 0))
}
for i := 0; i < len(subChannels); i++ {
for j := 0; j < b.config.numSubscribeShards; j++ {
subChannels[i] = append(subChannels[i], nil)
pubSubStartChannels[i] = append(pubSubStartChannels[i], &pubSubStart{errCh: make(chan error, 1)})
}
}
s.subClients = subChannels
s.pubSubStartChannels = pubSubStartChannels
return nil
}
func (r *defaultBrokerPubSubRunner) subClientsIndex(clusterShardIdx int) int {
return clusterShardIdx
}
func (r *defaultBrokerPubSubRunner) run(s *shardWrapper, h BrokerEventHandler) error {
b := r.broker
if b.config.SkipPubSub {
return nil
}
for i := 0; i < len(s.subClients); i++ { // Cluster shards.
clusterShardIndex := i
for j := 0; j < len(s.subClients[i]); j++ { // PUB/SUB shards.
pubSubShardIndex := j
logFields := getBaseLogFields(s)
logFields["pub_sub_shard"] = pubSubShardIndex
// Allocated outside runForever: probe state must survive loop
// restarts, that is the whole point of it.
probeState := &pubSubProbeState{}
go b.runForever(func() {
select {
case <-b.closeCh:
return
default:
}
b.runPubSub(s, logFields, h, clusterShardIndex, pubSubShardIndex, b.useShardedPubSub(s.shard), probeState, func(err error) {
s.pubSubStartChannels[clusterShardIndex][pubSubShardIndex].once.Do(func() {
s.pubSubStartChannels[clusterShardIndex][pubSubShardIndex].errCh <- err
})
})
})
}
}
return nil
}
var (
//go:embed internal/redis_lua/broker_publish_idempotent.lua
publishIdempotentSource string
//go:embed internal/redis_lua/broker_history_add_list.lua
addHistoryListSource string
//go:embed internal/redis_lua/broker_history_add_stream.lua
addHistoryStreamSource string
//go:embed internal/redis_lua/broker_history_list.lua
historyListSource string
//go:embed internal/redis_lua/broker_history_stream.lua
historyStreamSource string
)
func (b *RedisBroker) getShard(channel string) *shardWrapper {
if !b.sharding {
return b.shards[0]
}
return b.shards[consistentIndex(channel, len(b.shards))]
}
func (b *RedisBroker) RegisterControlEventHandler(h ControlEventHandler) error {
for _, wrapper := range b.shards {
wrapper.controlPubSubStart = &controlPubSubStart{errCh: make(chan error, 1)}
err := b.runControlShard(wrapper, h)
if err != nil {
return err
}
}
return nil
}
// PublishControl - see Broker.PublishControl.
func (b *RedisBroker) PublishControl(data []byte, nodeID string, _ string) error {
currentRound := atomic.AddUint64(&b.controlRound, 1)
index := currentRound % uint64(len(b.shards))
s := b.shards[index]
return b.publishControl(s, data, nodeID)
}
func (b *RedisBroker) publishControl(s *shardWrapper, data []byte, nodeID string) error {
var chID channelID
if nodeID == "" {
chID = channelID(b.controlChannel)
} else {
chID = b.nodeChannelID(nodeID)
}
cmd := s.shard.client.B().Publish().Channel(string(chID)).Message(convert.BytesToString(data)).Build()
resp := s.shard.client.Do(context.Background(), cmd)
return resp.Error()
}
// RegisterBrokerEventHandler – see Broker.RegisterBrokerEventHandler.
func (b *RedisBroker) RegisterBrokerEventHandler(h BrokerEventHandler) error {
// Run all shards.
for _, wrapper := range b.shards {
if err := wrapper.pubSubRunner.run(wrapper, h); err != nil {
return err
}
if err := b.checkCapabilities(wrapper.shard); err != nil {
return fmt.Errorf("capability error on shard [%s]: %v", wrapper.shard.string(), err)
}
}
for i := 0; i < len(b.shards); i++ {
if b.shards[i].controlPubSubStart != nil {
<-b.shards[i].controlPubSubStart.errCh
}
for j := 0; j < len(b.shards[i].pubSubStartChannels); j++ {
for k := 0; k < len(b.shards[i].pubSubStartChannels[j]); k++ {
if !b.config.SkipPubSub {
<-b.shards[i].pubSubStartChannels[j][k].errCh
}
}
}
}
return nil
}
func (b *RedisBroker) checkCapabilities(shard *RedisShard) error {
if !b.config.UseLists {
// Check whether Redis Streams supported.
if result := shard.client.Do(context.Background(), shard.client.B().Xrange().Key(b.config.Prefix+".__.check.stream").Start("0-0").End("0-0").Build()); result.Error() != nil {
if strings.Contains(result.Error().Error(), "unknown command") {
return errors.New("STREAM only available since Redis >= 5, consider upgrading Redis or using LIST structure for history")
}
return result.Error()
}
}
if b.useShardedPubSub(shard) {
// Check whether Redis Cluster sharded PUB/SUB supported.
if result := shard.client.Do(context.Background(), shard.client.B().Spublish().Channel(b.config.Prefix+".__check.spublish").Message("").Build()); result.Error() != nil {
if strings.Contains(result.Error().Error(), "unknown command") {
return errors.New("this Redis version does not support cluster sharded PUB/SUB feature")
}
return result.Error()
}
}
return nil
}
// runForever keeps another function running indefinitely.
// The reason this loop is not inside the function itself is
// so that defer can be used to clean-up nicely.
func (b *RedisBroker) runForever(fn func()) {
for {
select {
case <-b.closeCh:
return
default:
}
fn()
select {
case <-b.closeCh:
return
case <-time.After(250 * time.Millisecond):
// Wait for a while to prevent busy loop when reconnecting to Redis.
}
}
}
func getBaseLogFields(s *shardWrapper) map[string]any {
baseLogFields := make(map[string]any, len(s.logFields))
for k, v := range s.logFields {
baseLogFields[k] = v
}
return baseLogFields
}
func (b *RedisBroker) runControlShard(s *shardWrapper, h ControlEventHandler) error {
baseLogFields := getBaseLogFields(s)
go b.runForever(func() {
select {
case <-b.closeCh:
return
default:
}
b.runControlPubSub(s.shard, baseLogFields, h, func(err error) {
s.controlPubSubStart.once.Do(func() {
s.controlPubSubStart.errCh <- err
})
})
})
return nil
}
func (b *RedisBroker) Close(_ context.Context) error {
b.closeOnce.Do(func() {
close(b.closeCh)
})
return nil
}
func (b *RedisBroker) runControlPubSub(s *RedisShard, logFields map[string]any, eventHandler ControlEventHandler, startOnce func(error)) {
b.node.logger.log(newLogEntry(LogLevelDebug, "running Redis control PUB/SUB", getPubSubStartLogFields(s, logFields)))
defer func() {
b.node.logger.log(newLogEntry(LogLevelDebug, "stopping Redis control PUB/SUB", logFields))
}()
controlChannel := b.controlChannel
nodeChannel := b.nodeChannel
// probeChannel is a per-node service channel used only by liveness
// probes. The node channel itself carries real control commands, so
// probes get their own channel and are recognized by channel name.
probeChannel := nodeChannel + ".probe"
done := make(chan struct{})
var doneOnce sync.Once
closeDoneOnce := func() {
doneOnce.Do(func() {
close(done)
})
}
defer closeDoneOnce()
client := s.client
if b.config.SubscribeOnReplica {
client = s.replicaClient
}
conn, cancel := client.Dedicate()
defer cancel()
defer conn.Close()
numProcessors := runtime.NumCPU()
// Run workers to spread message processing work over worker goroutines.
workCh := make(chan rueidis.PubSubMessage, controlPubSubProcessorBufferSize)
for i := 0; i < numProcessors; i++ {
go func() {
for {
select {
case <-done:
return
case msg := <-workCh:
err := eventHandler.HandleControl(convert.StringToBytes(msg.Message))
if err != nil {
b.node.metrics.incRedisBrokerPubSubErrors(b.config.Name, "handle_control_message")
b.node.logger.log(newErrorLogEntry(err, "error handling control message", logFields))
}
}
}
}()
}
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-done:
return
case <-ticker.C:
b.node.metrics.redisBrokerPubSubBufferedMessages.WithLabelValues(b.config.Name, "control", "0").Set(float64(len(workCh)))
}
}
}()
// receivedCount counts messages delivered by this connection, feeding the
// liveness probing below. See the pubSubProbeInterval option docs.
var receivedCount atomic.Uint64
wait := conn.SetPubSubHooks(rueidis.PubSubHooks{
OnMessage: func(msg rueidis.PubSubMessage) {
receivedCount.Add(1)
if msg.Channel == probeChannel {
// Liveness probe: consumed right here by updating
// receivedCount, must not reach the control handler.
return
}
select {
case workCh <- msg:
case <-done:
default:
// Buffer is full, drop the message. It's expected that PUB/SUB layer
// only provides at most once delivery guarantee.
// Blocking here will block Redis connection read loop which is not a
// good thing and can lead to slower command processing and potentially
// to deadlocks (see https://github.com/redis/rueidis/issues/596).
b.node.metrics.redisBrokerPubSubDroppedMessages.WithLabelValues(b.config.Name, "control").Inc()
}
},
})
err := conn.Do(context.Background(), conn.B().Subscribe().Channel(controlChannel, nodeChannel, probeChannel).Build()).Error()
if err != nil {
startOnce(err)
b.node.metrics.incRedisBrokerPubSubErrors(b.config.Name, "subscribe_control_channel")
b.node.logger.log(newErrorLogEntry(err, "control pub/sub subscribe error", logFields))
return
}
startOnce(nil)
// Same liveness probing as in runPubSubLoop: a control connection that
// received nothing for a full interval gets a probe published to its
// per-node probe channel through the regular publish path; a further
// silent interval means the connection is attached to a node that does
// not deliver published traffic — restart it. A stale control connection
// is as harmful as a stale client one: the node stops receiving control
// commands and other nodes' pings, silently.
//
// In a running node this machinery is dormant: every node publishes node
// info to the control channel each nodeInfoPublishInterval, that traffic
// keeps the connection non-idle, and probes are never sent. Probing only
// activates when the pings are absent (connection stopped delivering, or
// a broker used without Node.Run) — the check stays valid without
// depending on the ping schedule.
probeInterval := b.config.pubSubProbeInterval
var probeTickerCh <-chan time.Time
if probeInterval > 0 {
probeTicker := time.NewTicker(probeInterval)
defer probeTicker.Stop()
probeTickerCh = probeTicker.C
}
var seenCount uint64
// probeSent is non-nil while a probe attempt is outstanding and becomes
// true once Redis has accepted that PUBLISH — see the same pattern in
// runPubSubLoop.
//
// No restart backoff here, unlike the client loop: restarting the control
// connection resubscribes three fixed channels, so even a restart loop
// against a wedged node costs nothing, while delaying restarts would
// prolong control starvation.
var probeSent *atomic.Bool
for {
select {
case err := <-wait:
if err != nil {
b.node.metrics.incRedisBrokerPubSubErrors(b.config.Name, "control_connection")
b.node.logger.log(newErrorLogEntry(err, "control pub/sub connection error", logFields))
}
return
case <-done:
return
case <-s.closeCh:
return
case <-probeTickerCh:
cur := receivedCount.Load()
if cur != seenCount {
seenCount = cur
probeSent = nil
continue
}
if probeSent != nil && probeSent.Load() {
b.node.metrics.incRedisBrokerPubSubErrors(b.config.Name, "control_probe_timeout")
b.node.logger.log(newLogEntry(LogLevelWarn, "no control PUB/SUB message received since liveness probe was sent, restarting control PUB/SUB connection", logFields))
return
}
// The outstanding probe (if any) never reached Redis: the publish
// path is broken, which says nothing about the receive path.
// Start a fresh attempt instead of restarting the connection.
sent := &atomic.Bool{}
probeSent = sent
go func() {
ctx, cancel := context.WithTimeout(context.Background(), probeInterval)
defer cancel()
cmd := s.client.B().Publish().Channel(probeChannel).Message(pubSubProbeMessage).Build()
if pubErr := s.client.Do(ctx, cmd).Error(); pubErr != nil {
b.node.metrics.incRedisBrokerPubSubErrors(b.config.Name, "control_probe_publish")
b.node.logger.log(newErrorLogEntry(pubErr, "error publishing control PUB/SUB probe", logFields))
return
}
sent.Store(true)
}()
}
}
}
const (
controlPubSubProcessorBufferSize = 4096
)
// makePubSubCallbacks builds the pubSubCallbacks used by the pub/sub runner.
func (b *RedisBroker) makePubSubCallbacks(s *shardWrapper) pubSubCallbacks {
return pubSubCallbacks{
handleMessage: func(isCluster bool, handler BrokerEventHandler, ch string, data []byte) error {
return b.handleRedisClientMessage(isCluster, handler, channelID(ch), data)
},
shardChannelID: func(clusterIdx, psIdx int, sharded bool) string {
return string(b.pubSubShardChannelID(clusterIdx, psIdx, sharded))
},
messageChannelID: func(ch string) string {
return string(b.messageChannelID(s.shard, ch))
},
shardForChannel: func(ch string) *RedisShard {
return b.getShard(ch).shard
},
extraResubscribeChannels: func() []string {
return b.node.extraBrokerPubSubChannels(b)
},
}
}
func (b *RedisBroker) runPubSub(s *shardWrapper, logFields map[string]any, eventHandler BrokerEventHandler, clusterShardIndex, psShardIndex int, useShardedPubSub bool, probeState *pubSubProbeState, startOnce func(error)) {
cb := b.makePubSubCallbacks(s)
numPartitions := b.config.NumShardedPubSubPartitions
if numPartitions == 0 {
numPartitions = 1
}
runPubSubLoop(
s.shard,
&s.subClientsMu,
s.subClients,
cb,
b.node,
b.config.Name,
b.node.metrics.brokerPubSub,
b.config.SubscribeOnReplica,
b.config.pubSubProbeInterval,
probeState,
b.config.numPubSubProcessors,
b.config.numResubscribeShards,
b.config.numSubscribeShards,
numPartitions,
logFields,
eventHandler,
clusterShardIndex, psShardIndex,
useShardedPubSub,
startOnce,
)
}
func (b *RedisBroker) useShardedPubSub(s *RedisShard) bool {
return s.isCluster && b.config.NumShardedPubSubPartitions > 0
}
// Publish - see Broker.Publish.
func (b *RedisBroker) Publish(ch string, data []byte, opts PublishOptions) (PublishResult, error) {
return b.publish(b.getShard(ch), ch, data, opts)
}
func (b *RedisBroker) publish(s *shardWrapper, ch string, data []byte, opts PublishOptions) (PublishResult, error) {
protoPub := &protocol.Publication{
Data: data,
Info: infoToProto(opts.ClientInfo),
Tags: opts.Tags,
Time: time.Now().UnixMilli(),
Key: opts.Key,
Removed: opts.Removed,
Score: opts.score,
Offset: opts.Offset,
Epoch: opts.Epoch,
PrevData: opts.PrevData,
Version: opts.Version,
}
if opts.HistorySize <= 0 || opts.HistoryTTL <= 0 {
// In no history case we communicate delta flag over Publication field. This field is then
// cleaned up before passing to the Node layer when handling Redis message.
protoPub.Delta = opts.UseDelta
}
byteMessage, err := protoPub.MarshalVT()
if err != nil {
return PublishResult{}, err
}
publishChannel := b.messageChannelID(s.shard, ch)
useShardedPublish := b.useShardedPubSub(s.shard)
var publishCommand = "publish"
if useShardedPublish {
publishCommand = "spublish"
}
idempotencyKey := opts.IdempotencyKey
resultKey := b.resultCacheKey(s.shard, ch, idempotencyKey)
var resultExpire string
if idempotencyKey != "" {
if opts.IdempotentResultTTL != 0 {
resultExpire = strconv.Itoa(int(opts.IdempotentResultTTL.Seconds()))
} else {
resultExpire = strconv.Itoa(defaultIdempotentResultExpireSeconds)
}
}
publishChannelStr := string(publishChannel)
if b.config.SkipPubSub {
publishChannelStr = ""
}
if opts.HistorySize <= 0 || opts.HistoryTTL <= 0 {
var resp rueidis.RedisResult
if useShardedPublish {
if resultExpire == "" {
if publishChannelStr == "" {
return PublishResult{}, nil
}
cmd := s.shard.client.B().Spublish().Channel(string(publishChannel)).Message(convert.BytesToString(byteMessage)).Build()
resp = s.shard.client.Do(context.Background(), cmd)
} else {
resp = b.publishIdempotentScript.Exec(
context.Background(),
s.shard.client,
[]string{string(resultKey)},
[]string{
convert.BytesToString(byteMessage),
publishChannelStr,
publishCommand,
resultExpire,
},
)
}
} else {
if resultExpire == "" {
if publishChannelStr == "" {
return PublishResult{}, nil
}
cmd := s.shard.client.B().Publish().Channel(string(publishChannel)).Message(convert.BytesToString(byteMessage)).Build()
resp = s.shard.client.Do(context.Background(), cmd)
} else {
resp = b.publishIdempotentScript.Exec(
context.Background(),
s.shard.client,
[]string{string(resultKey)},
[]string{
convert.BytesToString(byteMessage),
publishChannelStr,
publishCommand,
resultExpire,
},
)
}
}
return PublishResult{}, resp.Error()
}
historyMetaKey := b.historyMetaKey(s.shard, ch)
historyMetaTTL := opts.HistoryMetaTTL
if historyMetaTTL == 0 {
historyMetaTTL = b.node.config.HistoryMetaTTL
}
historyMetaTTLSeconds := int(historyMetaTTL.Seconds())
var streamKey channelID
var size int
var script *rueidis.Lua
if b.config.UseLists {
streamKey = b.historyListKey(s.shard, ch)
size = opts.HistorySize - 1
script = b.addHistoryListScript
} else {
streamKey = b.historyStreamKey(s.shard, ch)
size = opts.HistorySize
script = b.addHistoryStreamScript
}
var useDelta string
if opts.UseDelta {
useDelta = "1"
}
version := "0"
if opts.Version > 0 {
version = strconv.Itoa(int(opts.Version))
}
versionEpoch := opts.VersionEpoch
replies, err := script.Exec(
context.Background(),
s.shard.client,
[]string{string(streamKey), string(historyMetaKey), string(resultKey)},
[]string{
convert.BytesToString(byteMessage),
strconv.Itoa(size),
strconv.Itoa(int(opts.HistoryTTL.Seconds())),
publishChannelStr,
strconv.Itoa(historyMetaTTLSeconds),
epoch.Generate(),
publishCommand,
resultExpire,
useDelta,
version,
versionEpoch,
},
).ToArray()
if err != nil {
return PublishResult{}, err
}
if len(replies) != 2 && len(replies) != 3 && len(replies) != 4 {
return PublishResult{}, errors.New("wrong Redis reply")
}
offset, err := replies[0].AsInt64()
if err != nil {
return PublishResult{}, errors.New("wrong Redis reply offset")
}
epoch, err := replies[1].ToString()
if err != nil {
return PublishResult{}, errors.New("wrong Redis reply epoch")
}
result := PublishResult{StreamPosition: StreamPosition{Offset: uint64(offset), Epoch: epoch}}
if len(replies) >= 3 {
fromCacheStr, err := replies[2].ToString()
if err != nil {
return PublishResult{}, errors.New("wrong Redis reply from cache flag")
}
if fromCacheStr == "1" {
result.Suppressed = true
result.SuppressReason = SuppressReasonIdempotency
}
}
if len(replies) >= 4 {
skippedStr, err := replies[3].ToString()
if err != nil {
return PublishResult{}, errors.New("wrong Redis reply skipped flag")
}
if skippedStr == "1" {
result.Suppressed = true
result.SuppressReason = SuppressReasonVersion
}
}
return result, nil
}
// PublishJoin - see Broker.PublishJoin.
func (b *RedisBroker) PublishJoin(ch string, info *ClientInfo) error {
return b.publishJoin(b.getShard(ch), ch, info)
}
func (b *RedisBroker) publishJoin(s *shardWrapper, ch string, info *ClientInfo) error {
byteMessage, err := infoToProto(info).MarshalVT()
if err != nil {
return err
}
chID := b.messageChannelID(s.shard, ch)
var resp rueidis.RedisResult
if b.useShardedPubSub(s.shard) {
cmd := s.shard.client.B().Spublish().Channel(string(chID)).Message(convert.BytesToString(append(joinTypePrefix, byteMessage...))).Build()
resp = s.shard.client.Do(context.Background(), cmd)
} else {
cmd := s.shard.client.B().Publish().Channel(string(chID)).Message(convert.BytesToString(append(joinTypePrefix, byteMessage...))).Build()
resp = s.shard.client.Do(context.Background(), cmd)
}
return resp.Error()
}