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  }