package redis import ( "context" "sync" "time" "github.com/gomodule/redigo/redis" "github.com/pkg/errors" "github.com/rs/zerolog" "fiskerinc.com/modules/logger" "fiskerinc.com/modules/scheduler" ) const ( blockTimeout = 5 reconnectTimeout = 5 ) type Payload struct { channel string data []byte } // Queues is a struct used for tracking Redis queues // follows the Listener interface type Queues struct { connection ClientPoolInterface queues Set args redis.Args mu sync.RWMutex retryQueue scheduler.Bucket[Payload] } // NewQueues generates a new working Queues object func NewQueues(args ...Pool) *Queues { q := make(Set) var clientPool ClientPoolInterface if len(args) > 0 { clientPool = NewClientPool(args[0]) } else { clientPool = NewClientPool() } return &Queues{ connection: clientPool, queues: q, args: queuesToArgs(q), retryQueue: scheduler.Bucket[Payload]{}, } } // Add adds queue for listener to block on // follows the format "queue:" func (q *Queues) Add(id string) error { q.mu.Lock() defer q.mu.Unlock() ok := q.queues.Add(id) if !ok { return errors.Errorf("%v already in queues", id) } q.args = queuesToArgs(q.queues) logger.At(logger.Debug(), "Queues::Add conn", id).Send() return nil } // Remove queue from listener blocking // follows the format "queue:" func (q *Queues) Remove(id string) error { q.mu.Lock() defer q.mu.Unlock() ok := q.queues.Remove(id) if !ok { return errors.Errorf("%v does not exist in queues", id) } q.args = queuesToArgs(q.queues) logger.At(logger.Debug(), "Queues::Remove conn", id).Send() return nil } func (q *Queues) getArgs() redis.Args { q.mu.RLock() defer q.mu.RUnlock() return q.args } // Listen to redis by blocking on all lists // currently within the set func (q *Queues) Listen(ctx context.Context, handler func(string, []byte) error) error { q.QueryRun(ctx) isListening := true done := make(chan error, 1) go func() { select { case <-ctx.Done(): isListening = false break case <-done: return } }() sampled := logger.Sample(zerolog.LevelSampler{ DebugSampler: &zerolog.BurstSampler{ Burst: 1, Period: 1 * time.Minute, }, }) queueMaps := make(map[string][]byte) for isListening { clear(queueMaps) err := q.Process(handler, sampled, queueMaps) if err != nil { done <- err return errors.WithStack(err) } } return nil } // ListenChannel dumps redis messages to channel rather // than using a traditional handler func (q *Queues) ListenChannel() error { // stub return nil } // Length returns number of queues func (q *Queues) Length() int { return len(q.queues) } // Restart re-initializes queues connection func (q *Queues) Restart() error { q.connection = NewClientPool() return nil } func (c *Queues) timerFunc() { c.retryQueue.Process(func(payload Payload) { logger.Debug().Msgf("QueryRun::closing session %s ", payload.channel) client := c.connection.GetFromPool() // Attempt to stop base64 encoding messages err := client.SafeQueueMessage(payload.channel, string(payload.data)) client.Close() if err != nil { logger.At(logger.Error(), payload.channel, "redis").Err(err).Send() } else { logger.Info().Msgf("Unable to send to websocket, added %s back to queue", payload.channel) } }) } func (c *Queues) QueryRun(ctx context.Context) { ticker := time.NewTicker(100 * time.Millisecond) go func() { for { select { case <-ticker.C: c.timerFunc() case <-ctx.Done(): ticker.Stop() return } } }() } func (q *Queues) retry(key string, payload []byte) { qItem := Payload{ channel: key, data: payload, } q.retryQueue.Schedule(qItem) } func (q *Queues) Process(handler func(string, []byte) error, sampleLogger zerolog.Logger, queueMaps map[string][]byte) error { args := q.getArgs() sleepTime := blockTimeout * time.Millisecond if len(args) == 0 { sampleLogger.Debug().Msgf("Queue:Process no args") time.Sleep(sleepTime) return nil } poppedMap, err := q.lPop(args, sampleLogger, queueMaps) if err != nil && len(poppedMap) == 0 { if err != nil { sampleLogger.Debug().Msgf("Queue:Process lPop failed") logger.At(logger.Error(), "Queue:Process", "redis:unable to LPop").Err(err).Send() } time.Sleep(sleepTime) return nil } for channel, payload := range poppedMap { logger.Debug().Msgf("call handler, key- %s, data- %v", channel, string(payload)) if err = handler(channel, payload); err != nil { logger.At(logger.Error(), channel, "gateway:unable to send: retry in 3 sec").Err(err).Send() q.retry(channel, payload) } else { logger.At(logger.Debug(), channel, "redis"). Str("Gateway:msg", string(payload)). Msgf("VIN %s received queued msg ", channel) } } return nil } func (q *Queues) lPop(args redis.Args, sampleLogger zerolog.Logger, queueMaps map[string][]byte) (map[string][]byte, error) { client := q.connection.GetFromPool() defer client.Close() for _, arg := range args { reply, err := redis.Bytes(client.GetConn().Do("LPOP", arg)) if err != nil { if errors.Is(err, redis.ErrNil) { sampleLogger.Debug().Msgf("LPop::null value returned by redis") continue } logger.At(logger.Error(), "LPop", "redis"). Str("LPop", "").Err(err).Send() return queueMaps, err } logger.Debug().Msgf("LPop: vin %s reply %d ", arg.(string), len(reply)) queueMaps[ParseQueueKey(arg.(string))] = reply } return queueMaps, nil } func queuesToArgs(s Set) redis.Args { a := make(redis.Args, len(s)) i := 0 for e := range s { a[i] = QueueKey(e) i++ } return a }