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  }