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 }