github.com/argoproj/argo-events@v1.9.1/sensors/listener.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 sensors
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  	"sync"
    24  	"sync/atomic"
    25  	"time"
    26  
    27  	"github.com/Knetic/govaluate"
    28  	"github.com/antonmedv/expr"
    29  	"github.com/argoproj/argo-events/common"
    30  	"github.com/argoproj/argo-events/common/leaderelection"
    31  	"github.com/argoproj/argo-events/common/logging"
    32  	"github.com/argoproj/argo-events/eventbus"
    33  	eventbuscommon "github.com/argoproj/argo-events/eventbus/common"
    34  	apicommon "github.com/argoproj/argo-events/pkg/apis/common"
    35  	"github.com/argoproj/argo-events/pkg/apis/sensor/v1alpha1"
    36  	sensordependencies "github.com/argoproj/argo-events/sensors/dependencies"
    37  	sensortriggers "github.com/argoproj/argo-events/sensors/triggers"
    38  	cloudevents "github.com/cloudevents/sdk-go/v2"
    39  	cronlib "github.com/robfig/cron/v3"
    40  	"go.uber.org/ratelimit"
    41  	"go.uber.org/zap"
    42  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    43  )
    44  
    45  var rateLimiters = make(map[string]ratelimit.Limiter)
    46  
    47  func subscribeOnce(subLock *uint32, subscribe func()) {
    48  	// acquire subLock if not already held
    49  	if !atomic.CompareAndSwapUint32(subLock, 0, 1) {
    50  		return
    51  	}
    52  
    53  	subscribe()
    54  }
    55  
    56  func (sensorCtx *SensorContext) Start(ctx context.Context) error {
    57  	log := logging.FromContext(ctx)
    58  	clusterName := fmt.Sprintf("%s-sensor-%s", sensorCtx.sensor.Namespace, sensorCtx.sensor.Name)
    59  	replicas := int(sensorCtx.sensor.Spec.GetReplicas())
    60  	leasename := fmt.Sprintf("sensor-%s", sensorCtx.sensor.Name)
    61  
    62  	// sensor for kafka eventbus can be scaled horizontally,
    63  	// therefore does not require an elector
    64  	if sensorCtx.eventBusConfig.Kafka != nil {
    65  		return sensorCtx.listenEvents(ctx)
    66  	}
    67  
    68  	elector, err := leaderelection.NewElector(ctx, *sensorCtx.eventBusConfig, clusterName, replicas, sensorCtx.sensor.Namespace, leasename, sensorCtx.hostname)
    69  	if err != nil {
    70  		log.Errorw("failed to get an elector", zap.Error(err))
    71  		return err
    72  	}
    73  
    74  	elector.RunOrDie(ctx, leaderelection.LeaderCallbacks{
    75  		OnStartedLeading: func(ctx context.Context) {
    76  			if err := sensorCtx.listenEvents(ctx); err != nil {
    77  				log.Fatalw("failed to start", zap.Error(err))
    78  			}
    79  		},
    80  		OnStoppedLeading: func() {
    81  			log.Fatalf("leader lost: %s", sensorCtx.hostname)
    82  		},
    83  	})
    84  
    85  	return nil
    86  }
    87  
    88  func initRateLimiter(trigger v1alpha1.Trigger) {
    89  	duration := time.Second
    90  	if trigger.RateLimit != nil {
    91  		switch trigger.RateLimit.Unit {
    92  		case v1alpha1.Minute:
    93  			duration = time.Minute
    94  		case v1alpha1.Hour:
    95  			duration = time.Hour
    96  		}
    97  		rateLimiters[trigger.Template.Name] = ratelimit.New(int(trigger.RateLimit.RequestsPerUnit), ratelimit.Per(duration))
    98  	} else {
    99  		rateLimiters[trigger.Template.Name] = ratelimit.NewUnlimited()
   100  	}
   101  }
   102  
   103  // listenEvents watches and handles events received from the gateway.
   104  func (sensorCtx *SensorContext) listenEvents(ctx context.Context) error {
   105  	logger := logging.FromContext(ctx)
   106  	sensor := sensorCtx.sensor
   107  
   108  	depMapping := make(map[string]v1alpha1.EventDependency)
   109  	for _, d := range sensor.Spec.Dependencies {
   110  		depMapping[d.Name] = d
   111  	}
   112  
   113  	ctx, cancel := context.WithCancel(ctx)
   114  	defer cancel()
   115  	ebDriver, err := eventbus.GetSensorDriver(logging.WithLogger(ctx, logger), *sensorCtx.eventBusConfig, sensorCtx.sensor, sensorCtx.hostname)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	err = common.DoWithRetry(&common.DefaultBackoff, func() error {
   120  		return ebDriver.Initialize()
   121  	})
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	wg := &sync.WaitGroup{}
   127  	for _, t := range sensor.Spec.Triggers {
   128  		initRateLimiter(t)
   129  		wg.Add(1)
   130  		go func(trigger v1alpha1.Trigger) {
   131  			triggerLogger := logger.With(logging.LabelTriggerName, trigger.Template.Name)
   132  
   133  			defer wg.Done()
   134  			depExpression, err := sensorCtx.getDependencyExpression(ctx, trigger)
   135  			if err != nil {
   136  				triggerLogger.Errorw("failed to get dependency expression", zap.Error(err))
   137  				return
   138  			}
   139  			// Calculate dependencies of each of the triggers.
   140  			de := strings.ReplaceAll(depExpression, "-", "\\-")
   141  			expr, err := govaluate.NewEvaluableExpression(de)
   142  			if err != nil {
   143  				triggerLogger.Errorw("failed to get new evaluable expression", zap.Error(err))
   144  				return
   145  			}
   146  			depNames := unique(expr.Vars())
   147  			deps := []eventbuscommon.Dependency{}
   148  			for _, depName := range depNames {
   149  				dep, ok := depMapping[depName]
   150  				if !ok {
   151  					triggerLogger.Errorf("Dependency expression and dependency list do not match, %s is not found", depName)
   152  					return
   153  				}
   154  				d := eventbuscommon.Dependency{
   155  					Name:            dep.Name,
   156  					EventSourceName: dep.EventSourceName,
   157  					EventName:       dep.EventName,
   158  				}
   159  				deps = append(deps, d)
   160  			}
   161  
   162  			var conn eventbuscommon.TriggerConnection
   163  			err = common.DoWithRetry(&common.DefaultBackoff, func() error {
   164  				var err error
   165  				conn, err = ebDriver.Connect(ctx, trigger.Template.Name, depExpression, deps, trigger.AtLeastOnce)
   166  				triggerLogger.Debugf("just created connection %v, %+v", &conn, conn)
   167  				return err
   168  			})
   169  			if err != nil {
   170  				triggerLogger.Fatalw("failed to connect to event bus", zap.Error(err))
   171  				return
   172  			}
   173  			defer conn.Close()
   174  
   175  			transformFunc := func(depName string, event cloudevents.Event) (*cloudevents.Event, error) {
   176  				dep, ok := depMapping[depName]
   177  				if !ok {
   178  					return nil, fmt.Errorf("dependency %s not found", dep.Name)
   179  				}
   180  				if dep.Transform == nil {
   181  					return &event, nil
   182  				}
   183  				return sensordependencies.ApplyTransform(&event, dep.Transform)
   184  			}
   185  
   186  			filterFunc := func(depName string, cloudEvent cloudevents.Event) bool {
   187  				dep, ok := depMapping[depName]
   188  				if !ok {
   189  					return false
   190  				}
   191  				if dep.Filters == nil {
   192  					return true
   193  				}
   194  				argoEvent := convertEvent(cloudEvent)
   195  
   196  				result, err := sensordependencies.Filter(argoEvent, dep.Filters, dep.FiltersLogicalOperator)
   197  				if err != nil {
   198  					if !result {
   199  						triggerLogger.Warnf("Event [%s] discarded due to filtering error: %s",
   200  							eventToString(argoEvent), err.Error())
   201  					} else {
   202  						triggerLogger.Warnf("Event [%s] passed but with filtering error: %s",
   203  							eventToString(argoEvent), err.Error())
   204  					}
   205  				} else {
   206  					if !result {
   207  						triggerLogger.Debugf("Event [%s] discarded due to filtering", eventToString(argoEvent))
   208  					}
   209  				}
   210  				return result
   211  			}
   212  
   213  			actionFunc := func(events map[string]cloudevents.Event) {
   214  				retryStrategy := trigger.RetryStrategy
   215  				if retryStrategy == nil {
   216  					retryStrategy = &apicommon.Backoff{Steps: 1}
   217  				}
   218  				err := common.DoWithRetry(retryStrategy, func() error {
   219  					return sensorCtx.triggerActions(ctx, sensor, events, trigger)
   220  				})
   221  				if err != nil {
   222  					triggerLogger.Warnf("failed to trigger actions, %v", err)
   223  				}
   224  			}
   225  
   226  			var subLock uint32
   227  			wg1 := &sync.WaitGroup{}
   228  			closeSubCh := make(chan struct{})
   229  
   230  			resetConditionsCh := make(chan struct{})
   231  			var lastResetTime time.Time
   232  			if len(trigger.Template.ConditionsReset) > 0 {
   233  				for _, c := range trigger.Template.ConditionsReset {
   234  					if c.ByTime == nil {
   235  						continue
   236  					}
   237  					cronParser := cronlib.NewParser(cronlib.Minute | cronlib.Hour | cronlib.Dom | cronlib.Month | cronlib.Dow)
   238  					opts := []cronlib.Option{
   239  						cronlib.WithParser(cronParser),
   240  						cronlib.WithChain(cronlib.Recover(cronlib.DefaultLogger)),
   241  					}
   242  					nowTime := time.Now()
   243  					if c.ByTime.Timezone != "" {
   244  						location, err := time.LoadLocation(c.ByTime.Timezone)
   245  						if err != nil {
   246  							triggerLogger.Errorw("failed to load timezone", zap.Error(err))
   247  							continue
   248  						}
   249  						opts = append(opts, cronlib.WithLocation(location))
   250  						nowTime = nowTime.In(location)
   251  					}
   252  					cr := cronlib.New(opts...)
   253  					_, err = cr.AddFunc(c.ByTime.Cron, func() {
   254  						resetConditionsCh <- struct{}{}
   255  					})
   256  					if err != nil {
   257  						triggerLogger.Errorw("failed to add cron schedule", zap.Error(err))
   258  						continue
   259  					}
   260  					cr.Start()
   261  
   262  					triggerLogger.Debugf("just started cron job; entries=%v", cr.Entries())
   263  
   264  					// set lastResetTime (the last time this would've been triggered)
   265  					if len(cr.Entries()) > 0 {
   266  						prevTriggerTime, err := common.PrevCronTime(c.ByTime.Cron, cronParser, nowTime)
   267  						if err != nil {
   268  							triggerLogger.Errorw("couldn't get previous cron trigger time", zap.Error(err))
   269  							continue
   270  						}
   271  						triggerLogger.Infof("previous trigger time: %v", prevTriggerTime)
   272  						if prevTriggerTime.After(lastResetTime) {
   273  							lastResetTime = prevTriggerTime
   274  						}
   275  					}
   276  				}
   277  			}
   278  
   279  			subscribeFunc := func() {
   280  				wg1.Add(1)
   281  				go func() {
   282  					defer wg1.Done()
   283  					// release the lock when goroutine exits
   284  					defer atomic.StoreUint32(&subLock, 0)
   285  
   286  					triggerLogger.Infof("started subscribing to events for trigger %s with client connection %s", trigger.Template.Name, conn)
   287  
   288  					subject := &sensorCtx.eventBusSubject
   289  					err = conn.Subscribe(ctx, closeSubCh, resetConditionsCh, lastResetTime, transformFunc, filterFunc, actionFunc, subject)
   290  					if err != nil {
   291  						triggerLogger.Errorw("failed to subscribe to eventbus", zap.Any("connection", conn), zap.Error(err))
   292  						return
   293  					}
   294  					triggerLogger.Debugf("exiting subscribe goroutine, conn=%+v", conn)
   295  				}()
   296  			}
   297  
   298  			subscribeOnce(&subLock, subscribeFunc)
   299  
   300  			triggerLogger.Infof("starting eventbus connection daemon for client %s...", conn)
   301  			ticker := time.NewTicker(5 * time.Second)
   302  			defer ticker.Stop()
   303  			for {
   304  				select {
   305  				case <-ctx.Done():
   306  					triggerLogger.Infof("exiting eventbus connection daemon for client %s...", conn)
   307  					wg1.Wait()
   308  					return
   309  				case <-ticker.C:
   310  					if conn == nil || conn.IsClosed() {
   311  						triggerLogger.Info("EventBus connection lost, reconnecting...")
   312  						conn, err = ebDriver.Connect(ctx, trigger.Template.Name, depExpression, deps, trigger.AtLeastOnce)
   313  						if err != nil {
   314  							triggerLogger.Errorw("failed to reconnect to eventbus", zap.Any("connection", conn), zap.Error(err))
   315  							continue
   316  						}
   317  						triggerLogger.Infow("reconnected to EventBus.", zap.Any("connection", conn))
   318  
   319  						if atomic.LoadUint32(&subLock) == 1 {
   320  							triggerLogger.Debug("acquired sublock, instructing trigger to shutdown subscription")
   321  							closeSubCh <- struct{}{}
   322  							// give subscription time to close
   323  							time.Sleep(2 * time.Second)
   324  						} else {
   325  							triggerLogger.Debug("sublock not acquired")
   326  						}
   327  					}
   328  
   329  					// create subscription if conn is alive and no subscription is currently held
   330  					if conn != nil && !conn.IsClosed() {
   331  						subscribeOnce(&subLock, subscribeFunc)
   332  					}
   333  				}
   334  			}
   335  		}(t)
   336  	}
   337  	logger.Info("Sensor started.")
   338  	<-ctx.Done()
   339  	logger.Info("Shutting down...")
   340  	cancel()
   341  	wg.Wait()
   342  	return nil
   343  }
   344  
   345  func (sensorCtx *SensorContext) triggerActions(ctx context.Context, sensor *v1alpha1.Sensor, events map[string]cloudevents.Event, trigger v1alpha1.Trigger) error {
   346  	eventsMapping := make(map[string]*v1alpha1.Event)
   347  	depNames := make([]string, 0, len(events))
   348  	eventIDs := make([]string, 0, len(events))
   349  	for k, v := range events {
   350  		eventsMapping[k] = convertEvent(v)
   351  		depNames = append(depNames, k)
   352  		eventIDs = append(eventIDs, v.ID())
   353  	}
   354  	if trigger.AtLeastOnce {
   355  		// By making this a blocking call, wait to Ack the message
   356  		// until this trigger is executed.
   357  		return sensorCtx.triggerWithRateLimit(ctx, sensor, trigger, eventsMapping, depNames, eventIDs)
   358  	} else {
   359  		go func() {
   360  			err := sensorCtx.triggerWithRateLimit(ctx, sensor, trigger, eventsMapping, depNames, eventIDs)
   361  			if err != nil {
   362  				// Log the error, and let it continue
   363  				logger := logging.FromContext(ctx)
   364  				logger.Errorw("Failed to execute a trigger", zap.Error(err), zap.String(logging.LabelTriggerName, trigger.Template.Name))
   365  			}
   366  		}()
   367  		return nil
   368  	}
   369  }
   370  
   371  func (sensorCtx *SensorContext) triggerWithRateLimit(ctx context.Context, sensor *v1alpha1.Sensor, trigger v1alpha1.Trigger, eventsMapping map[string]*v1alpha1.Event, depNames, eventIDs []string) error {
   372  	if rl, ok := rateLimiters[trigger.Template.Name]; ok {
   373  		rl.Take()
   374  	}
   375  
   376  	log := logging.FromContext(ctx)
   377  	if err := sensorCtx.triggerOne(ctx, sensor, trigger, eventsMapping, depNames, eventIDs, log); err != nil {
   378  		// Log the error, and let it continue
   379  		log.Errorw("Failed to execute a trigger", zap.Error(err), zap.String(logging.LabelTriggerName, trigger.Template.Name),
   380  			zap.Any("triggeredBy", depNames), zap.Any("triggeredByEvents", eventIDs))
   381  		sensorCtx.metrics.ActionFailed(sensor.Name, trigger.Template.Name)
   382  		return err
   383  	}
   384  	sensorCtx.metrics.ActionTriggered(sensor.Name, trigger.Template.Name)
   385  	return nil
   386  }
   387  
   388  func (sensorCtx *SensorContext) triggerOne(ctx context.Context, sensor *v1alpha1.Sensor, trigger v1alpha1.Trigger, eventsMapping map[string]*v1alpha1.Event, depNames, eventIDs []string, log *zap.SugaredLogger) error {
   389  	defer func(start time.Time) {
   390  		sensorCtx.metrics.ActionDuration(sensor.Name, trigger.Template.Name, float64(time.Since(start)/time.Millisecond))
   391  	}(time.Now())
   392  
   393  	if err := sensortriggers.ApplyTemplateParameters(eventsMapping, &trigger); err != nil {
   394  		log.Errorf("failed to apply template parameters, %v", err)
   395  		return err
   396  	}
   397  
   398  	logger := log.With(logging.LabelTriggerName, trigger.Template.Name)
   399  
   400  	logger.Debugw("resolving the trigger implementation")
   401  	triggerImpl := sensorCtx.GetTrigger(ctx, &trigger)
   402  	if triggerImpl == nil {
   403  		return fmt.Errorf("invalid trigger %s, could not find an implementation", trigger.Template.Name)
   404  	}
   405  
   406  	logger = logger.With(logging.LabelTriggerType, triggerImpl.GetTriggerType())
   407  	log.Debug("fetching trigger resource if any")
   408  	obj, err := triggerImpl.FetchResource(ctx)
   409  	if err != nil {
   410  		return err
   411  	}
   412  	if obj == nil {
   413  		return fmt.Errorf("invalid trigger %s, could not fetch the trigger resource", trigger.Template.Name)
   414  	}
   415  
   416  	logger.Debug("applying resource parameters if any")
   417  	updatedObj, err := triggerImpl.ApplyResourceParameters(eventsMapping, obj)
   418  	if err != nil {
   419  		return err
   420  	}
   421  
   422  	logger.Debug("executing the trigger resource")
   423  	newObj, err := triggerImpl.Execute(ctx, eventsMapping, updatedObj)
   424  	if err != nil {
   425  		return fmt.Errorf("failed to execute trigger, %w", err)
   426  	}
   427  	logger.Debug("trigger resource successfully executed")
   428  
   429  	logger.Debug("applying trigger policy")
   430  	if err := triggerImpl.ApplyPolicy(ctx, newObj); err != nil {
   431  		return err
   432  	}
   433  	logger.Infow(fmt.Sprintf("Successfully processed trigger '%s'", trigger.Template.Name),
   434  		zap.Any("triggeredBy", depNames), zap.Any("triggeredByEvents", eventIDs))
   435  	return nil
   436  }
   437  
   438  func (sensorCtx *SensorContext) getDependencyExpression(ctx context.Context, trigger v1alpha1.Trigger) (string, error) {
   439  	logger := logging.FromContext(ctx)
   440  
   441  	// Translate original expression which might contain group names
   442  	// to an expression only contains dependency names
   443  	translate := func(originalExpr string, parameters map[string]string) (string, error) {
   444  		originalExpr = strings.ReplaceAll(originalExpr, "&&", " + \"&&\" + ")
   445  		originalExpr = strings.ReplaceAll(originalExpr, "||", " + \"||\" + ")
   446  		originalExpr = strings.ReplaceAll(originalExpr, "-", "_")
   447  		originalExpr = strings.ReplaceAll(originalExpr, "(", "\"(\"+")
   448  		originalExpr = strings.ReplaceAll(originalExpr, ")", "+\")\"")
   449  
   450  		program, err := expr.Compile(originalExpr, expr.Env(parameters))
   451  		if err != nil {
   452  			logger.Errorw("Failed to compile original dependency expression", zap.Error(err))
   453  			return "", err
   454  		}
   455  		result, err := expr.Run(program, parameters)
   456  		if err != nil {
   457  			logger.Errorw("Failed to parse original dependency expression", zap.Error(err))
   458  			return "", err
   459  		}
   460  		newExpr := fmt.Sprintf("%v", result)
   461  		newExpr = strings.ReplaceAll(newExpr, "\"(\"", "(")
   462  		newExpr = strings.ReplaceAll(newExpr, "\")\"", ")")
   463  		return newExpr, nil
   464  	}
   465  
   466  	sensor := sensorCtx.sensor
   467  	var depExpression string
   468  	var err error
   469  	switch {
   470  	case trigger.Template.Conditions != "":
   471  		conditions := trigger.Template.Conditions
   472  		// Add all the dependency and dependency group to the parameter mappings
   473  		depGroupMapping := make(map[string]string)
   474  		for _, dep := range sensor.Spec.Dependencies {
   475  			key := strings.ReplaceAll(dep.Name, "-", "_")
   476  			depGroupMapping[key] = dep.Name
   477  		}
   478  		depExpression, err = translate(conditions, depGroupMapping)
   479  		if err != nil {
   480  			return "", err
   481  		}
   482  	default:
   483  		deps := []string{}
   484  		for _, dep := range sensor.Spec.Dependencies {
   485  			deps = append(deps, dep.Name)
   486  		}
   487  		depExpression = strings.Join(deps, "&&")
   488  	}
   489  	logger.Infof("Dependency expression for trigger %s: %s", trigger.Template.Name, depExpression)
   490  	return depExpression, nil
   491  }
   492  
   493  func eventToString(event *v1alpha1.Event) string {
   494  	return fmt.Sprintf("ID '%s', Source '%s', Time '%s', Data '%s'",
   495  		event.Context.ID, event.Context.Source, event.Context.Time.Time.Format(time.RFC3339), string(event.Data))
   496  }
   497  
   498  func convertEvent(event cloudevents.Event) *v1alpha1.Event {
   499  	return &v1alpha1.Event{
   500  		Context: &v1alpha1.EventContext{
   501  			DataContentType: event.Context.GetDataContentType(),
   502  			Source:          event.Context.GetSource(),
   503  			SpecVersion:     event.Context.GetSpecVersion(),
   504  			Type:            event.Context.GetType(),
   505  			Time:            metav1.Time{Time: event.Context.GetTime()},
   506  			ID:              event.Context.GetID(),
   507  			Subject:         event.Context.GetSubject(),
   508  		},
   509  		Data: event.Data(),
   510  	}
   511  }
   512  
   513  func unique(stringSlice []string) []string {
   514  	if len(stringSlice) == 0 {
   515  		return stringSlice
   516  	}
   517  	keys := make(map[string]bool)
   518  	list := []string{}
   519  	for _, entry := range stringSlice {
   520  		if _, value := keys[entry]; !value {
   521  			keys[entry] = true
   522  			list = append(list, entry)
   523  		}
   524  	}
   525  	return list
   526  }