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 }