github.com/dolthub/go-mysql-server@v0.18.0/eventscheduler/event_executor.go (about) 1 // Copyright 2023 Dolthub, Inc. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package eventscheduler 16 17 import ( 18 "context" 19 "fmt" 20 "sync/atomic" 21 "time" 22 23 "github.com/sirupsen/logrus" 24 25 "github.com/dolthub/go-mysql-server/sql" 26 ) 27 28 // eventExecutor handles execution of each enabled events and any events related queries 29 // including CREATE/ALTER/DROP EVENT and DROP DATABASE. These queries notify the EventScheduler 30 // to update the enabled events list in the eventExecutor. It also handles updating the event 31 // metadata in the database or dropping it from the database after its execution. 32 type eventExecutor struct { 33 bThreads *sql.BackgroundThreads 34 list *enabledEventsList 35 runningEventsStatus *runningEventsStatus 36 ctxGetterFunc func() (*sql.Context, func() error, error) 37 queryRunFunc func(ctx *sql.Context, dbName, query, username, address string) error 38 stop atomic.Bool 39 catalog sql.Catalog 40 tokenTracker *tokenTracker 41 period int 42 } 43 44 // newEventExecutor returns a new eventExecutor instance with an empty enabled events list. 45 // The enabled events list is loaded only when the EventScheduler status is ENABLED. 46 func newEventExecutor(bgt *sql.BackgroundThreads, ctxFunc func() (*sql.Context, func() error, error), runQueryFunc func(ctx *sql.Context, dbName, query, username, address string) error, period int) *eventExecutor { 47 return &eventExecutor{ 48 bThreads: bgt, 49 list: newEnabledEventsList([]*enabledEvent{}), 50 runningEventsStatus: newRunningEventsStatus(), 51 ctxGetterFunc: ctxFunc, 52 queryRunFunc: runQueryFunc, 53 stop: atomic.Bool{}, 54 tokenTracker: newTokenTracker(), 55 period: period, 56 } 57 } 58 59 // start starts the eventExecutor. It checks and executes 60 // enabled events and updates necessary events' metadata. 61 func (ee *eventExecutor) start() { 62 ee.stop.Store(false) 63 logrus.Trace("Starting eventExecutor") 64 65 // TODO: Currently, we execute events by sorting the enabled events by their execution time, then 66 // waking up at a regular period and seeing if any events are ready to be executed. This 67 // could be more efficient if we used time.Timer to schedule events to be run instead 68 // of having our own loop here. It would also allow us to support any time granularity for 69 // recurring events. 70 pollingDuration := 30 * time.Second 71 if ee.period > 0 { 72 pollingDuration = time.Duration(ee.period) * time.Second 73 } 74 75 for { 76 time.Sleep(pollingDuration) 77 78 ctx, _, err := ee.ctxGetterFunc() 79 if err != nil { 80 logrus.Errorf("unable to create context for event executor: %s", err) 81 continue 82 } 83 84 needsToReloadEvents, err := ee.needsToReloadEvents(ctx) 85 if err != nil { 86 ctx.GetLogger().Errorf("unable to determine if events need to be reloaded: %s", err) 87 } 88 if needsToReloadEvents { 89 err := ee.loadAllEvents(ctx) 90 if err != nil { 91 ctx.GetLogger().Errorf("unable to reload events: %s", err) 92 } 93 } 94 95 timeNow := time.Now() 96 if ee.stop.Load() { 97 logrus.Trace("Stopping eventExecutor") 98 return 99 } else if ee.list.len() == 0 { 100 continue 101 } 102 103 // safeguard list entry getting removed while in check 104 nextAt, ok := ee.list.getNextExecutionTime() 105 if !ok { 106 continue 107 } 108 109 secondsUntilExecution := nextAt.Sub(timeNow).Seconds() 110 if secondsUntilExecution <= -1*pollingDuration.Seconds() { 111 // in case the execution time is past, re-evaluate it ( TODO: should not happen ) 112 curEvent := ee.list.pop() 113 if curEvent != nil { 114 logrus.Warnf("Reevaluating event %s, seconds until execution: %f", curEvent.name(), secondsUntilExecution) 115 ee.reevaluateEvent(curEvent.edb, curEvent.event) 116 } 117 } else if secondsUntilExecution <= 0.0000001 { 118 curEvent := ee.list.pop() 119 if curEvent != nil { 120 ctx.GetLogger().Debugf("Executing event %s, seconds until execution: %f", curEvent.name(), secondsUntilExecution) 121 ctx, commit, err := ee.ctxGetterFunc() 122 if err != nil { 123 ctx.GetLogger().Errorf("Received error '%s' getting ctx in event scheduler", err) 124 } 125 err = ee.executeEventAndUpdateList(ctx, curEvent, timeNow) 126 if err != nil { 127 ctx.GetLogger().Errorf("Received error '%s' executing event: %s", err, curEvent.event.Name) 128 } 129 err = commit() 130 if err != nil { 131 ctx.GetLogger().Errorf("Received error '%s' executing event: %s", err, curEvent.event.Name) 132 } 133 } 134 } else { 135 ctx.GetLogger().Tracef("Not executing event %s yet, seconds until execution: %f", ee.list.peek().name(), secondsUntilExecution) 136 } 137 } 138 } 139 140 // needsToReloadEvents returns true if any of the EventDatabases known to this event executor 141 func (ee *eventExecutor) needsToReloadEvents(ctx *sql.Context) (bool, error) { 142 // TODO: We currently reload all events across all databases if any of the EventDatabases indicate they 143 // need to be reloaded. In the future, we could optimize this more by ony reloading events from the 144 // EventDatabases that indicated they need to be reloaded. This just requires more detailed handling 145 // of the enabledEvent list to merge together existing events and reloaded events. 146 for _, database := range ee.catalog.AllDatabases(ctx) { 147 edb, ok := database.(sql.EventDatabase) 148 if !ok { 149 // Skip any non-EventDatabases 150 continue 151 } 152 153 token := ee.tokenTracker.GetTrackedToken(edb.Name()) 154 needsToReload, err := edb.NeedsToReloadEvents(ctx, token) 155 if err != nil { 156 return false, err 157 } 158 if needsToReload { 159 ctx.GetLogger().Debugf("Event reload needed for database %s", edb.Name()) 160 return true, nil 161 } 162 } 163 return false, nil 164 } 165 166 // loadAllEvents reloads all events from all known EventDatabases. This is necessary when out-of-band 167 // changes have modified event definitions without going through the CREATE EVENT, ALTER EVENT code paths. 168 func (ee *eventExecutor) loadAllEvents(ctx *sql.Context) error { 169 ctx.GetLogger().Debug("Loading events") 170 171 enabledEvents := make([]*enabledEvent, 0) 172 allDatabases := ee.catalog.AllDatabases(ctx) 173 for _, database := range allDatabases { 174 edb, ok := database.(sql.EventDatabase) 175 if !ok { 176 // Skip any non-EventDatabases 177 continue 178 } 179 180 // TODO: We currently need to set the current database to get the parsed plan, 181 // but this feels like it shouldn't be necessary; would be good to clean up 182 ctx.SetCurrentDatabase(edb.Name()) 183 events, token, err := edb.GetEvents(ctx) 184 if err != nil { 185 return err 186 } 187 188 ee.tokenTracker.UpdateTrackedToken(edb.Name(), token) 189 for _, eDef := range events { 190 newEnabledEvent, created, err := newEnabledEvent(ctx, edb, eDef, time.Now()) 191 if err != nil { 192 ctx.GetLogger().Errorf("unable to reload event: %s", err) 193 } else if created { 194 enabledEvents = append(enabledEvents, newEnabledEvent) 195 } 196 } 197 } 198 199 ee.list = newEnabledEventsList(enabledEvents) 200 return nil 201 } 202 203 // shutdown stops the eventExecutor. 204 func (ee *eventExecutor) shutdown() { 205 ee.stop.Store(true) 206 ee.list.clear() 207 ee.runningEventsStatus.clear() 208 } 209 210 // executeEventAndUpdateList executes the given event and updates the event's last executed time in the database. 211 // If the event is not ended, then it updates the enabled event and re-adds it back to the list. 212 func (ee *eventExecutor) executeEventAndUpdateList(ctx *sql.Context, event *enabledEvent, executionTime time.Time) error { 213 reAdd, err := ee.executeEvent(event) 214 if err != nil { 215 return err 216 } 217 218 ended, err := event.updateEventAfterExecution(ctx, event.edb, executionTime) 219 if err != nil { 220 return err 221 } else if !reAdd { 222 return nil 223 } else if !ended { 224 ee.list.add(event) 225 } 226 return nil 227 } 228 229 // executeEvent executes given event by adding a thread to background threads to run the given event's definition. 230 // This function returns whether the event needs to be added back into the enabled events list, as well as any 231 // error if a background thread was not able to be started up to execute the event. 232 func (ee *eventExecutor) executeEvent(event *enabledEvent) (bool, error) { 233 ee.runningEventsStatus.update(event.name(), true, true) 234 defer ee.runningEventsStatus.remove(event.name()) 235 236 reAdd, ok := ee.runningEventsStatus.getReAdd(event.name()) 237 if !ok { 238 // should not happen, but sanity check 239 reAdd = false 240 } 241 // if event is ONE TIME, then do not re-add 242 if event.event.HasExecuteAt { 243 reAdd = false 244 } 245 246 taskName := fmt.Sprintf("executing %s", event.name()) 247 addThreadErr := ee.bThreads.Add(taskName, func(ctx context.Context) { 248 logrus.Trace(taskName) 249 select { 250 case <-ctx.Done(): 251 logrus.Tracef("stopping background thread (%s)", taskName) 252 ee.stop.Store(true) 253 return 254 default: 255 // get a new session sql.Context for each event definition execution 256 sqlCtx, commit, err := ee.ctxGetterFunc() 257 if err != nil { 258 logrus.WithField("query", event.event.EventBody).Errorf("unable to get context for executed query: %v", err) 259 return 260 } 261 262 // Note that we pass in the full CREATE EVENT statement so that the engine can parse it 263 // and pull out the plan nodes for the event body, since the event body doesn't always 264 // parse as a valid SQL statement on its own (e.g. when using a BEGIN/END block). 265 logrus.WithField("query", event.event.EventBody).Debugf("executing event %s", event.name()) 266 err = ee.queryRunFunc(sqlCtx, event.edb.Name(), event.event.CreateEventStatement(), event.username, event.address) 267 if err != nil { 268 logrus.WithField("query", event.event.EventBody).Errorf("unable to execute query: %v", err) 269 return 270 } 271 272 // must commit after done using the sql.Context 273 err = commit() 274 if err != nil { 275 logrus.WithField("query", event.event.EventBody).Errorf("unable to commit transaction: %v", err) 276 return 277 } 278 } 279 }) 280 281 return reAdd, addThreadErr 282 } 283 284 // reevaluateEvent evaluates an event from enabled events list, but its execution time passed the current time. 285 // It creates new enabledEvent if the event being created is at ENABLE status with valid schedule. 286 // This function is used when the event misses the execution time check of the event. 287 func (ee *eventExecutor) reevaluateEvent(edb sql.EventDatabase, event sql.EventDefinition) { 288 // if the updated event status is not ENABLE, do not add it to the list. 289 if event.Status != sql.EventStatus_Enable.String() { 290 return 291 } 292 293 ctx, commit, err := ee.ctxGetterFunc() 294 if err != nil { 295 ctx.GetLogger().Errorf("Received error '%s' getting ctx in event scheduler", err) 296 } 297 298 newEvent, created, err := newEnabledEvent(ctx, edb, event, time.Now()) 299 if err != nil { 300 ctx.GetLogger().Errorf("Received error '%s' re-evaluating event to scheduler: %s", err, event.Name) 301 } else if created { 302 ee.list.add(newEvent) 303 } 304 305 err = commit() 306 if err != nil { 307 ctx.GetLogger().Errorf("Received error '%s' re-evaluating event to scheduler: %s", err, event.Name) 308 } 309 } 310 311 // addEvent creates new enabledEvent if the event being created is at ENABLE status with valid schedule. 312 // If the updated event's schedule is starting at the same time as created time, it executes immediately. 313 func (ee *eventExecutor) addEvent(ctx *sql.Context, edb sql.EventDatabase, event sql.EventDefinition) { 314 // if the updated event status is not ENABLE, do not add it to the list. 315 if event.Status != sql.EventStatus_Enable.String() { 316 return 317 } 318 319 enabledEvent, created, err := newEnabledEvent(ctx, edb, event, event.CreatedAt) 320 if err != nil { 321 ctx.GetLogger().Errorf("Received error '%s' executing event: %s", err, event.Name) 322 } else if created { 323 newEvent := enabledEvent.event 324 // if STARTS is set to current_timestamp or not set, 325 // then executeEvent the event once and update lastExecuted. 326 var firstExecutionTime time.Time 327 if newEvent.HasExecuteAt { 328 firstExecutionTime = newEvent.ExecuteAt 329 } else { 330 firstExecutionTime = newEvent.Starts 331 } 332 if firstExecutionTime.Sub(newEvent.CreatedAt).Abs().Seconds() <= 1 { 333 // after execution, the event is added to the list if applicable (if the event is not ended) 334 err = ee.executeEventAndUpdateList(ctx, enabledEvent, newEvent.CreatedAt) 335 if err != nil { 336 ctx.GetLogger().Errorf("Received error '%s' executing event: %s", err, event.Name) 337 return 338 } 339 } else { 340 ee.list.add(enabledEvent) 341 } 342 } 343 return 344 } 345 346 // updateEvent removes the event from enabled events list if it exists and adds new enabledEvent if the event status 347 // is ENABLE and event schedule is not expired. If the new event's schedule is starting at the same time as 348 // last altered time, it executes immediately. 349 func (ee *eventExecutor) updateEvent(ctx *sql.Context, edb sql.EventDatabase, origEventName string, event sql.EventDefinition) { 350 var origEventKeyName = fmt.Sprintf("%s.%s", edb.Name(), origEventName) 351 // remove the original event if exists. 352 ee.list.remove(origEventKeyName) 353 354 // if the updated event status is not ENABLE, do not add it to the list. 355 if event.Status != sql.EventStatus_Enable.String() { 356 return 357 } 358 359 // add the updated event as new event 360 newUpdatedEvent, created, err := newEnabledEvent(ctx, edb, event, event.LastAltered) 361 if err != nil { 362 return 363 } else if created { 364 newDetails := newUpdatedEvent.event 365 // if the event being updated is currently running, 366 // then do not re-add the event to the list after execution 367 if s, ok := ee.runningEventsStatus.getStatus(origEventKeyName); ok && s { 368 ee.runningEventsStatus.update(origEventKeyName, s, false) 369 } 370 371 if newDetails.Starts.Sub(newDetails.LastAltered).Abs().Seconds() <= 1 { 372 err = ee.executeEventAndUpdateList(ctx, newUpdatedEvent, newDetails.LastAltered) 373 if err != nil { 374 ctx.GetLogger().Errorf("Received error '%s' executing event: %s", err, newDetails.Name) 375 return 376 } 377 } else { 378 ee.list.add(newUpdatedEvent) 379 } 380 } 381 return 382 } 383 384 // removeEvent removes the event if it exists in the enabled events list. 385 // If the event is currently executing, it will not be in the list, 386 // so it updates the running events status to not re-add this event 387 // after its execution. 388 func (ee *eventExecutor) removeEvent(eventName string) { 389 ee.list.remove(eventName) 390 // if not found, it might have been removed as it's currently executing 391 if s, ok := ee.runningEventsStatus.getStatus(eventName); ok && s { 392 ee.runningEventsStatus.update(eventName, s, false) 393 } 394 } 395 396 // removeSchemaEvents removes all events from a given database if any exist 397 // in the enabled events list. If any events of this database 398 // are currently executing, they will not be in the list, 399 // so it updates the running events status to not re-add those 400 // events after their execution. 401 func (ee *eventExecutor) removeSchemaEvents(dbName string) { 402 ee.list.removeSchemaEvents(dbName) 403 // if not found, it might be currently executing 404 ee.runningEventsStatus.cancelEventsForDatabase(dbName) 405 } 406 407 // tokenTracker tracks the opaque tokens returned by EventDatabase.GetEvents, which are later used with 408 // EventDatabase.NeedsToReloadEvents so that integrators can signal if out-of-band event changes have 409 // occurred, in which case GMS will call EventDatabase.GetEvents to get the updated event definitions. 410 type tokenTracker struct { 411 // trackedTokenMap is a map of event database name to the last opaque reload token returned by GetEvents. 412 trackedTokenMap map[string]interface{} 413 } 414 415 // newTokenTracker creates a new, empty tokenTracker. 416 func newTokenTracker() *tokenTracker { 417 return &tokenTracker{ 418 trackedTokenMap: make(map[string]interface{}), 419 } 420 } 421 422 // Equal returns true if the last tracked token for the EventDatabase named |databaseName| is equal 423 // to the |other| opaque token. Equality is tested by an "==" check. 424 func (ht *tokenTracker) Equal(databaseName string, other interface{}) bool { 425 return ht.trackedTokenMap[databaseName] == other 426 } 427 428 // UpdateTrackedToken updates the tracked token for the EventDatabase named |databaseName| to 429 // the value in |token|. 430 func (ht *tokenTracker) UpdateTrackedToken(databaseName string, token interface{}) { 431 ht.trackedTokenMap[databaseName] = token 432 } 433 434 // GetTrackedToken returns the tracked token for the EventDatabase named |databaseName|. 435 func (ht *tokenTracker) GetTrackedToken(databaseName string) interface{} { 436 return ht.trackedTokenMap[databaseName] 437 }