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  }