github.com/argoproj/argo-events@v1.9.1/eventsources/sources/redis_stream/start.go (about) 1 /* 2 Copyright 2020 BlackRock, Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package redisstream 18 19 import ( 20 "context" 21 "encoding/json" 22 "fmt" 23 "time" 24 25 "github.com/go-redis/redis/v8" 26 "go.uber.org/zap" 27 28 "github.com/argoproj/argo-events/common" 29 "github.com/argoproj/argo-events/common/logging" 30 eventsourcecommon "github.com/argoproj/argo-events/eventsources/common" 31 "github.com/argoproj/argo-events/eventsources/sources" 32 metrics "github.com/argoproj/argo-events/metrics" 33 apicommon "github.com/argoproj/argo-events/pkg/apis/common" 34 "github.com/argoproj/argo-events/pkg/apis/events" 35 "github.com/argoproj/argo-events/pkg/apis/eventsource/v1alpha1" 36 ) 37 38 // EventListener implements Eventing for the Redis event source 39 type EventListener struct { 40 EventSourceName string 41 EventName string 42 EventSource v1alpha1.RedisStreamEventSource 43 Metrics *metrics.Metrics 44 } 45 46 // GetEventSourceName returns name of event source 47 func (el *EventListener) GetEventSourceName() string { 48 return el.EventSourceName 49 } 50 51 // GetEventName returns name of event 52 func (el *EventListener) GetEventName() string { 53 return el.EventName 54 } 55 56 // GetEventSourceType return type of event server 57 func (el *EventListener) GetEventSourceType() apicommon.EventSourceType { 58 return apicommon.RedisStreamEvent 59 } 60 61 // StartListening listens for new data on specified redis streams 62 func (el *EventListener) StartListening(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error) error { 63 log := logging.FromContext(ctx). 64 With(logging.LabelEventSourceType, el.GetEventSourceType(), logging.LabelEventName, el.GetEventName()) 65 log.Info("started processing the Redis stream event source...") 66 defer sources.Recover(el.GetEventName()) 67 68 redisEventSource := &el.EventSource 69 70 opt := &redis.Options{ 71 Addr: redisEventSource.HostAddress, 72 DB: int(redisEventSource.DB), 73 } 74 75 log.Info("retrieving password if it has been configured...") 76 if redisEventSource.Password != nil { 77 password, err := common.GetSecretFromVolume(redisEventSource.Password) 78 if err != nil { 79 return fmt.Errorf("failed to find the secret password %s, %w", redisEventSource.Password.Name, err) 80 } 81 opt.Password = password 82 } 83 84 if redisEventSource.Username != "" { 85 opt.Username = redisEventSource.Username 86 } 87 88 if redisEventSource.TLS != nil { 89 tlsConfig, err := common.GetTLSConfig(redisEventSource.TLS) 90 if err != nil { 91 return fmt.Errorf("failed to get the tls configuration, %w", err) 92 } 93 opt.TLSConfig = tlsConfig 94 } 95 96 log.Infof("setting up a redis client for %s...", redisEventSource.HostAddress) 97 client := redis.NewClient(opt) 98 99 if status := client.Ping(ctx); status.Err() != nil { 100 return fmt.Errorf("failed to connect to host %s and db %d for event source %s, %w", redisEventSource.HostAddress, redisEventSource.DB, el.GetEventName(), status.Err()) 101 } 102 log.Infof("connected to redis server %s", redisEventSource.HostAddress) 103 104 // Create a common consumer group on all streams to start reading from beginning of the streams. 105 // Only proceeds if all the streams are already present 106 consumersGroup := "argo-events-cg" 107 if len(redisEventSource.ConsumerGroup) != 0 { 108 consumersGroup = redisEventSource.ConsumerGroup 109 } 110 for _, stream := range redisEventSource.Streams { 111 // create a consumer group to start reading from the current last entry in the stream (https://redis.io/commands/xgroup-create) 112 if err := client.XGroupCreate(ctx, stream, consumersGroup, "$").Err(); err != nil { 113 // redis package doesn't seem to expose concrete error types 114 if err.Error() != "BUSYGROUP Consumer Group name already exists" { 115 return fmt.Errorf("creating consumer group %s for stream %s on host %s for event source %s, %w", consumersGroup, stream, redisEventSource.HostAddress, el.GetEventName(), err) 116 } 117 log.Infof("Consumer group %q already exists in stream %q", consumersGroup, stream) 118 } 119 } 120 121 readGroupArgs := make([]string, 2*len(redisEventSource.Streams)) 122 copy(readGroupArgs, redisEventSource.Streams) 123 // Start by reading our pending messages(previously read but not acknowledged). 124 streamToLastEntryMapping := make(map[string]string, len(redisEventSource.Streams)) 125 for _, s := range redisEventSource.Streams { 126 streamToLastEntryMapping[s] = "0-0" 127 } 128 129 updateReadGroupArgs := func() { 130 for i, s := range redisEventSource.Streams { 131 readGroupArgs[i+len(redisEventSource.Streams)] = streamToLastEntryMapping[s] 132 } 133 } 134 updateReadGroupArgs() 135 136 msgCount := redisEventSource.MaxMsgCountPerRead 137 if msgCount == 0 { 138 msgCount = 10 139 } 140 141 var msgsToAcknowledge []string 142 for { 143 select { 144 case <-ctx.Done(): 145 log.Infof("Redis stream event source for host %s is stopped", redisEventSource.HostAddress) 146 return nil 147 default: 148 } 149 entries, err := client.XReadGroup(ctx, &redis.XReadGroupArgs{ 150 Group: consumersGroup, 151 Consumer: "argo-events-worker", 152 Streams: readGroupArgs, 153 Count: int64(msgCount), 154 Block: 2 * time.Second, 155 NoAck: false, 156 }).Result() 157 if err != nil { 158 if err == redis.Nil { 159 continue 160 } 161 log.With("streams", redisEventSource.Streams).Errorw("reading streams using XREADGROUP", zap.Error(err)) 162 } 163 164 for _, entry := range entries { 165 if len(entry.Messages) == 0 { 166 // Completed consuming pending messages. Now start consuming new messages 167 streamToLastEntryMapping[entry.Stream] = ">" 168 } 169 170 msgsToAcknowledge = msgsToAcknowledge[:0] 171 172 for _, message := range entry.Messages { 173 if err := el.handleOne(entry.Stream, message, dispatch, log); err != nil { 174 log.With("stream", entry.Stream, "message_id", message.ID).Errorw("failed to process Redis stream message", zap.Error(err)) 175 el.Metrics.EventProcessingFailed(el.GetEventSourceName(), el.GetEventName()) 176 continue 177 } 178 msgsToAcknowledge = append(msgsToAcknowledge, message.ID) 179 } 180 181 if len(msgsToAcknowledge) == 0 { 182 continue 183 } 184 185 // Even if acknowledging fails, since we handled the message, we are good to proceed. 186 if err := client.XAck(ctx, entry.Stream, consumersGroup, msgsToAcknowledge...).Err(); err != nil { 187 log.With("stream", entry.Stream, "message_ids", msgsToAcknowledge).Errorw("failed to acknowledge messages from the Redis stream", zap.Error(err)) 188 } 189 if streamToLastEntryMapping[entry.Stream] != ">" { 190 streamToLastEntryMapping[entry.Stream] = msgsToAcknowledge[len(msgsToAcknowledge)-1] 191 } 192 } 193 updateReadGroupArgs() 194 } 195 } 196 197 func (el *EventListener) handleOne(stream string, message redis.XMessage, dispatch func([]byte, ...eventsourcecommon.Option) error, log *zap.SugaredLogger) error { 198 defer func(start time.Time) { 199 el.Metrics.EventProcessingDuration(el.GetEventSourceName(), el.GetEventName(), float64(time.Since(start)/time.Millisecond)) 200 }(time.Now()) 201 202 log.With("stream", stream, "message_id", message.ID).Info("received a message") 203 eventData := &events.RedisStreamEventData{ 204 Stream: stream, 205 Id: message.ID, 206 Values: message.Values, 207 Metadata: el.EventSource.Metadata, 208 } 209 eventBody, err := json.Marshal(&eventData) 210 if err != nil { 211 return fmt.Errorf("failed to marshal the event data, rejecting the event, %w", err) 212 } 213 log.With("stream", stream).Info("dispatching the event on the data channel...") 214 if err = dispatch(eventBody); err != nil { 215 return fmt.Errorf("failed dispatch a Redis stream event, %w", err) 216 } 217 return nil 218 }