github.com/anycable/anycable-go@v1.5.1/broadcast/legacy_redis.go (about) 1 package broadcast 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "log/slog" 8 "net/url" 9 "strings" 10 "time" 11 12 "github.com/FZambia/sentinel" 13 rconfig "github.com/anycable/anycable-go/redis" 14 "github.com/anycable/anycable-go/utils" 15 16 "github.com/gomodule/redigo/redis" 17 ) 18 19 // LegacyRedisBroadcaster contains information about Redis pubsub connection 20 type LegacyRedisBroadcaster struct { 21 node Handler 22 url string 23 sentinels string 24 sentinelClient *sentinel.Sentinel 25 sentinelDiscoveryInterval time.Duration 26 pingInterval time.Duration 27 channel string 28 reconnectAttempt int 29 maxReconnectAttempts int 30 uri *url.URL 31 log *slog.Logger 32 tlsVerify bool 33 } 34 35 // NewLegacyRedisBroadcaster returns new RedisSubscriber struct 36 func NewLegacyRedisBroadcaster(node Handler, config *rconfig.RedisConfig, l *slog.Logger) *LegacyRedisBroadcaster { 37 return &LegacyRedisBroadcaster{ 38 node: node, 39 url: config.URL, 40 sentinels: config.Sentinels, 41 sentinelDiscoveryInterval: time.Duration(config.SentinelDiscoveryInterval), 42 channel: config.Channel, 43 pingInterval: time.Duration(config.KeepalivePingInterval), 44 reconnectAttempt: 0, 45 maxReconnectAttempts: config.MaxReconnectAttempts, 46 log: l.With("context", "broadcast").With("provider", "redis"), 47 tlsVerify: config.TLSVerify, 48 } 49 } 50 51 func (LegacyRedisBroadcaster) IsFanout() bool { 52 return true 53 } 54 55 // Start connects to Redis and subscribes to the pubsub channel 56 // if sentinels is set it gets the the master address first 57 func (s *LegacyRedisBroadcaster) Start(done chan (error)) error { 58 // parse URL and check if it is correct 59 redisURL, err := url.Parse(s.url) 60 61 s.uri = redisURL 62 63 if err != nil { 64 return err 65 } 66 67 if s.sentinels != "" { 68 masterName := redisURL.Hostname() 69 70 s.log.Debug("Redis sentinel enabled", "sentinels", s.sentinels, "master", masterName) 71 sentinels := strings.Split(s.sentinels, ",") 72 s.sentinelClient = &sentinel.Sentinel{ 73 Addrs: sentinels, 74 MasterName: masterName, 75 Dial: func(addr string) (redis.Conn, error) { 76 timeout := 500 * time.Millisecond 77 78 sentinelHost := addr 79 dialOptions := []redis.DialOption{ 80 redis.DialConnectTimeout(timeout), 81 redis.DialReadTimeout(timeout), 82 redis.DialReadTimeout(timeout), 83 redis.DialTLSSkipVerify(!s.tlsVerify), 84 } 85 86 sentinelURI, err := url.Parse(fmt.Sprintf("redis://%s", addr)) 87 88 if err == nil { 89 sentinelHost = sentinelURI.Host 90 password, hasPassword := sentinelURI.User.Password() 91 if hasPassword { 92 dialOptions = append(dialOptions, redis.DialPassword(password)) 93 } 94 } 95 96 c, err := redis.Dial( 97 "tcp", 98 sentinelHost, 99 dialOptions..., 100 ) 101 if err != nil { 102 s.log.Debug("failed to connect to sentinel", "addr", addr) 103 return nil, err 104 } 105 s.log.Debug("successfully connected to sentinel", "addr", addr) 106 return c, nil 107 }, 108 } 109 110 go s.discoverSentinels() 111 } 112 113 go s.keepalive(done) 114 115 return nil 116 } 117 118 func (s *LegacyRedisBroadcaster) discoverSentinels() { 119 defer s.sentinelClient.Close() 120 121 // Periodically discover new Sentinels. 122 ctx, cancel := context.WithCancel(context.Background()) 123 defer cancel() 124 125 go func() { 126 err := s.sentinelClient.Discover() 127 if err != nil { 128 s.log.Warn("Failed to discover sentinels") 129 } 130 for { 131 select { 132 case <-ctx.Done(): 133 return 134 135 case <-time.After(s.sentinelDiscoveryInterval * time.Second): 136 err := s.sentinelClient.Discover() 137 if err != nil { 138 s.log.Warn("Failed to discover sentinels") 139 } 140 } 141 } 142 }() 143 } 144 145 func (s *LegacyRedisBroadcaster) keepalive(done chan (error)) { 146 for { 147 if s.sentinelClient != nil { 148 masterAddress, err := s.sentinelClient.MasterAddr() 149 150 if err != nil { 151 s.log.Warn("failed to get master address from sentinel") 152 done <- err 153 return 154 } 155 s.log.Debug("obtained master address from sentinel", "addr", masterAddress) 156 157 s.uri.Host = masterAddress 158 s.url = s.uri.String() 159 } 160 161 if err := s.listen(); err != nil { 162 s.log.Warn("Redis connection failed", "error", err) 163 } 164 165 s.reconnectAttempt++ 166 167 if s.reconnectAttempt >= s.maxReconnectAttempts { 168 done <- errors.New("Redis reconnect attempts exceeded") //nolint:stylecheck 169 return 170 } 171 172 delay := utils.NextRetry(s.reconnectAttempt) 173 174 s.log.Info(fmt.Sprintf("next Redis reconnect attempt in %s", delay)) 175 time.Sleep(delay) 176 177 s.log.Info("reconnecting to Redis...") 178 } 179 } 180 181 // Shutdown is no-op for Redis 182 func (s *LegacyRedisBroadcaster) Shutdown(ctx context.Context) error { 183 return nil 184 } 185 186 func (s *LegacyRedisBroadcaster) listen() error { 187 dialOptions := []redis.DialOption{ 188 redis.DialTLSSkipVerify(!s.tlsVerify), 189 } 190 c, err := redis.DialURL(s.url, dialOptions...) 191 192 if err != nil { 193 return err 194 } 195 196 defer c.Close() 197 198 if s.sentinels != "" { 199 if !sentinel.TestRole(c, "master") { 200 return errors.New("failed master role check") 201 } 202 } 203 204 psc := redis.PubSubConn{Conn: c} 205 if err = psc.Subscribe(s.channel); err != nil { 206 s.log.Error("failed to subscribe to Redis channel", "error", err) 207 return err 208 } 209 210 s.reconnectAttempt = 0 211 212 done := make(chan error, 1) 213 214 go func() { 215 for { 216 switch v := psc.Receive().(type) { 217 case redis.Message: 218 s.log.Debug("received pubsub message") 219 s.node.HandlePubSub(v.Data) 220 case redis.Subscription: 221 s.log.Info("subscribed to Redis channel", "channel", v.Channel) 222 case error: 223 s.log.Error("Redis subscription error", "error", v) 224 done <- v 225 } 226 } 227 }() 228 229 ticker := time.NewTicker(s.pingInterval * time.Second) 230 defer ticker.Stop() 231 232 loop: 233 for err == nil { 234 select { 235 case <-ticker.C: 236 if err = psc.Ping(""); err != nil { 237 break loop 238 } 239 case err := <-done: 240 // Return error from the receive goroutine. 241 return err 242 } 243 } 244 245 psc.Unsubscribe() //nolint:errcheck 246 return <-done 247 }