github.com/argoproj/argo-events@v1.9.1/eventsources/sources/calendar/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 calendar
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"os"
    24  	"strings"
    25  	"time"
    26  
    27  	cronlib "github.com/robfig/cron/v3"
    28  	"go.uber.org/zap"
    29  	"k8s.io/client-go/kubernetes"
    30  
    31  	"github.com/argoproj/argo-events/common"
    32  	"github.com/argoproj/argo-events/common/logging"
    33  	eventsourcecommon "github.com/argoproj/argo-events/eventsources/common"
    34  	"github.com/argoproj/argo-events/eventsources/persist"
    35  	metrics "github.com/argoproj/argo-events/metrics"
    36  	apicommon "github.com/argoproj/argo-events/pkg/apis/common"
    37  	"github.com/argoproj/argo-events/pkg/apis/events"
    38  	"github.com/argoproj/argo-events/pkg/apis/eventsource/v1alpha1"
    39  )
    40  
    41  // EventListener implements Eventing for calendar based events
    42  type EventListener struct {
    43  	EventSourceName     string
    44  	EventName           string
    45  	Namespace           string
    46  	CalendarEventSource v1alpha1.CalendarEventSource
    47  	Metrics             *metrics.Metrics
    48  
    49  	log              *zap.SugaredLogger
    50  	eventPersistence persist.EventPersist
    51  }
    52  
    53  // GetEventSourceName returns name of event source
    54  func (el *EventListener) GetEventSourceName() string {
    55  	return el.EventSourceName
    56  }
    57  
    58  // GetEventName returns name of event
    59  func (el *EventListener) GetEventName() string {
    60  	return el.EventName
    61  }
    62  
    63  // GetEventSourceType return type of event server
    64  func (el *EventListener) GetEventSourceType() apicommon.EventSourceType {
    65  	return apicommon.CalendarEvent
    66  }
    67  
    68  // initializePersistence initialize the persistence object.
    69  // This func can move to eventing.go once we start supporting persistence for all sources.
    70  func (el *EventListener) initializePersistence(ctx context.Context, persistence *v1alpha1.EventPersistence) error {
    71  	el.log.Info("Initializing Persistence")
    72  	if persistence.ConfigMap != nil {
    73  		kubeConfig, _ := os.LookupEnv(common.EnvVarKubeConfig)
    74  
    75  		restConfig, err := common.GetClientConfig(kubeConfig)
    76  		if err != nil {
    77  			return fmt.Errorf("failed to get a K8s rest config for the event source %s, %w", el.GetEventName(), err)
    78  		}
    79  		kubeClientset, err := kubernetes.NewForConfig(restConfig)
    80  		if err != nil {
    81  			return fmt.Errorf("failed to set up a K8s client for the event source %s, %w", el.GetEventName(), err)
    82  		}
    83  
    84  		el.eventPersistence, err = persist.NewConfigMapPersist(ctx, kubeClientset, persistence.ConfigMap, el.Namespace)
    85  		if err != nil {
    86  			return err
    87  		}
    88  	}
    89  	return nil
    90  }
    91  
    92  func (el *EventListener) getPersistenceKey() string {
    93  	return fmt.Sprintf("%s.%s", el.EventSourceName, el.EventName)
    94  }
    95  
    96  // getExecutionTime return starting schedule time for execution
    97  func (el *EventListener) getExecutionTime() (time.Time, error) {
    98  	lastT := time.Now()
    99  	if el.eventPersistence.IsEnabled() && el.CalendarEventSource.Persistence.IsCatchUpEnabled() {
   100  		lastEvent, err := el.eventPersistence.Get(el.getPersistenceKey())
   101  		if err != nil {
   102  			el.log.Errorw("failed to get last persisted event.", zap.Error(err))
   103  			return lastT, fmt.Errorf("failed to get last persisted event, , %w", err)
   104  		}
   105  		if lastEvent != nil && lastEvent.EventPayload != "" {
   106  			var eventData events.CalendarEventData
   107  			err := json.Unmarshal([]byte(lastEvent.EventPayload), &eventData)
   108  			if err != nil {
   109  				el.log.Errorw("failed to marshal last persisted event.", zap.Error(err))
   110  				return lastT, fmt.Errorf("failed to marshal last persisted event, , %w", err)
   111  			}
   112  			eventTime := strings.Split(eventData.EventTime, " m=")
   113  			lastT, err = time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", eventTime[0])
   114  			if err != nil {
   115  				el.log.Errorw("failed to parse the persisted last event timestamp", zap.Error(err))
   116  				return lastT, fmt.Errorf("failed to parse the persisted last event timestamp, %w", err)
   117  			}
   118  		}
   119  
   120  		if el.CalendarEventSource.Persistence.Catchup.MaxDuration != "" {
   121  			duration, err := time.ParseDuration(el.CalendarEventSource.Persistence.Catchup.MaxDuration)
   122  			if err != nil {
   123  				return lastT, err
   124  			}
   125  
   126  			// Set maxCatchupDuration in execution time if last persisted event time is greater than maxCatchupDuration
   127  			if duration < time.Since(lastT) {
   128  				el.log.Infow("set execution time", zap.Any("maxDuration", el.CalendarEventSource.Persistence.Catchup.MaxDuration))
   129  				lastT = time.Now().Add(-duration)
   130  			}
   131  		}
   132  	}
   133  	return lastT, nil
   134  }
   135  
   136  // StartListening starts listening events
   137  func (el *EventListener) StartListening(ctx context.Context, dispatch func([]byte, ...eventsourcecommon.Option) error) error {
   138  	el.log = logging.FromContext(ctx).
   139  		With(logging.LabelEventSourceType, el.GetEventSourceType(), logging.LabelEventName, el.GetEventName())
   140  	el.log.Info("started processing the calendar event source...")
   141  
   142  	calendarEventSource := &el.CalendarEventSource
   143  	el.log.Info("resolving calendar schedule...")
   144  	schedule, err := resolveSchedule(calendarEventSource)
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	el.log.Info("parsing exclusion dates if any...")
   150  	exDates, err := common.ParseExclusionDates(calendarEventSource.ExclusionDates)
   151  	if err != nil {
   152  		return err
   153  	}
   154  
   155  	el.eventPersistence = &persist.NullPersistence{}
   156  	if calendarEventSource.Persistence != nil {
   157  		if err = el.initializePersistence(ctx, calendarEventSource.Persistence); err != nil {
   158  			return err
   159  		}
   160  	} else {
   161  		el.log.Info("Persistence not enabled")
   162  	}
   163  
   164  	var next Next
   165  	next = func(last time.Time) time.Time {
   166  		nextT := schedule.Next(last)
   167  		nextYear := nextT.Year()
   168  		nextMonth := nextT.Month()
   169  		nextDay := nextT.Day()
   170  		for _, exDate := range exDates {
   171  			// if exDate == nextEvent, then we need to skip this and get the next
   172  			if exDate.Year() == nextYear && exDate.Month() == nextMonth && exDate.Day() == nextDay {
   173  				return next(nextT)
   174  			}
   175  		}
   176  		return nextT
   177  	}
   178  
   179  	lastT, err := el.getExecutionTime()
   180  	if err != nil {
   181  		return err
   182  	}
   183  
   184  	var location *time.Location
   185  	if calendarEventSource.Timezone != "" {
   186  		el.log.Infow("loading location for the schedule...", zap.Any("location", calendarEventSource.Timezone))
   187  		location, err = time.LoadLocation(calendarEventSource.Timezone)
   188  		if err != nil {
   189  			return fmt.Errorf("failed to load location for event source %s / %s, , %w", el.GetEventSourceName(), el.GetEventName(), err)
   190  		}
   191  		lastT = lastT.In(location)
   192  	}
   193  	sendEventFunc := func(tx time.Time) error {
   194  		defer func(start time.Time) {
   195  			el.Metrics.EventProcessingDuration(el.GetEventSourceName(), el.GetEventName(), float64(time.Since(start)/time.Millisecond))
   196  		}(time.Now())
   197  
   198  		eventData := &events.CalendarEventData{
   199  			EventTime: tx.String(),
   200  			Metadata:  calendarEventSource.Metadata,
   201  		}
   202  		payload, err := json.Marshal(eventData)
   203  		if err != nil {
   204  			el.log.Errorw("failed to marshal the event data", zap.Error(err))
   205  			// no need to continue as further event payloads will suffer same fate as this one.
   206  			return fmt.Errorf("failed to marshal the event data for event source %s / %s, %w", el.GetEventSourceName(), el.GetEventName(), err)
   207  		}
   208  		el.log.Info("dispatching calendar event...")
   209  		err = dispatch(payload)
   210  		if err != nil {
   211  			el.log.Errorw("failed to dispatch calendar event", zap.Error(err))
   212  			return fmt.Errorf("failed to dispatch calendar event, %w", err)
   213  		}
   214  		if el.eventPersistence != nil && el.eventPersistence.IsEnabled() {
   215  			event := persist.Event{EventKey: el.getPersistenceKey(), EventPayload: string(payload)}
   216  			err = el.eventPersistence.Save(&event)
   217  			if err != nil {
   218  				el.log.Errorw("failed to persist calendar event", zap.Error(err))
   219  			}
   220  		}
   221  		return nil
   222  	}
   223  
   224  	el.log.Infow("Calendar event start time:", zap.Any("Time", lastT.Format(time.RFC822)))
   225  	for {
   226  		t := next(lastT)
   227  
   228  		// Catchup scenario
   229  		// Trigger the event immediately if the current schedule time is earlier then
   230  		if time.Now().After(t) {
   231  			el.log.Infow("triggering catchup events", zap.Any(logging.LabelTime, t.UTC().String()))
   232  			if err = sendEventFunc(t); err != nil {
   233  				el.log.Errorw("failed to dispatch calendar event", zap.Error(err))
   234  				el.Metrics.EventProcessingFailed(el.GetEventSourceName(), el.GetEventName())
   235  				if el.eventPersistence.IsEnabled() {
   236  					time.Sleep(100 * time.Millisecond)
   237  					continue
   238  				}
   239  			}
   240  			lastT = t
   241  			if location != nil {
   242  				lastT = lastT.In(location)
   243  			}
   244  			continue
   245  		}
   246  
   247  		timer := time.After(time.Until(t))
   248  		el.log.Infow("expected next calendar event", zap.Any(logging.LabelTime, t.UTC().String()))
   249  		select {
   250  		case tx := <-timer:
   251  			if err = sendEventFunc(tx); err != nil {
   252  				el.log.Errorw("failed to dispatch calendar event", zap.Error(err))
   253  				el.Metrics.EventProcessingFailed(el.GetEventSourceName(), el.GetEventName())
   254  				if el.eventPersistence.IsEnabled() {
   255  					time.Sleep(100 * time.Millisecond)
   256  					continue
   257  				}
   258  			}
   259  			lastT = tx
   260  			if location != nil {
   261  				lastT = lastT.In(location)
   262  			}
   263  		case <-ctx.Done():
   264  			el.log.Info("exiting calendar event listener...")
   265  			return nil
   266  		}
   267  	}
   268  }
   269  
   270  // Next is a function to compute the next event time from a given time
   271  type Next func(time.Time) time.Time
   272  
   273  // resolveSchedule parses the schedule and returns a valid cron schedule
   274  func resolveSchedule(cal *v1alpha1.CalendarEventSource) (cronlib.Schedule, error) {
   275  	if cal.Schedule != "" {
   276  		// standard cron expression
   277  		specParser := cronlib.NewParser(cronlib.Minute | cronlib.Hour | cronlib.Dom | cronlib.Month | cronlib.Dow)
   278  		schedule, err := specParser.Parse(cal.Schedule)
   279  		if err != nil {
   280  			return nil, fmt.Errorf("failed to parse schedule %s from calendar event. Cause: %w", cal.Schedule, err)
   281  		}
   282  		return schedule, nil
   283  	}
   284  	if cal.Interval != "" {
   285  		intervalDuration, err := time.ParseDuration(cal.Interval)
   286  		if err != nil {
   287  			return nil, fmt.Errorf("failed to parse interval %s from calendar event. Cause: %w", cal.Interval, err)
   288  		}
   289  		schedule := cronlib.ConstantDelaySchedule{Delay: intervalDuration}
   290  		return schedule, nil
   291  	}
   292  	return nil, fmt.Errorf("calendar event must contain either a schedule or interval")
   293  }