github.com/safing/portbase@v0.19.5/modules/tasks.go (about)

     1  package modules
     2  
     3  import (
     4  	"container/list"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"sync"
     9  	"sync/atomic"
    10  	"time"
    11  
    12  	"github.com/tevino/abool"
    13  
    14  	"github.com/safing/portbase/log"
    15  )
    16  
    17  // Task is managed task bound to a module.
    18  type Task struct {
    19  	name   string
    20  	module *Module
    21  	taskFn func(context.Context, *Task) error
    22  
    23  	canceled  bool
    24  	executing bool
    25  	overtime  bool // locked by scheduleLock
    26  
    27  	// these are populated at task creation
    28  	// ctx is canceled when module is shutdown -> all tasks become canceled
    29  	ctx       context.Context
    30  	cancelCtx func()
    31  
    32  	executeAt time.Time
    33  	repeat    time.Duration
    34  	maxDelay  time.Duration
    35  
    36  	queueElement            *list.Element
    37  	prioritizedQueueElement *list.Element
    38  	scheduleListElement     *list.Element
    39  
    40  	lock sync.Mutex
    41  }
    42  
    43  var (
    44  	taskQueue            = list.New()
    45  	prioritizedTaskQueue = list.New()
    46  	queuesLock           sync.Mutex
    47  	queueWg              sync.WaitGroup
    48  
    49  	taskSchedule = list.New()
    50  	scheduleLock sync.Mutex
    51  
    52  	waitForever chan time.Time
    53  
    54  	queueIsFilled       = make(chan struct{}, 1) // kick off queue handler
    55  	notifyTaskScheduler = make(chan struct{}, 1)
    56  	taskTimeslot        = make(chan struct{})
    57  )
    58  
    59  const (
    60  	maxTimeslotWait   = 30 * time.Second
    61  	minRepeatDuration = 1 * time.Minute
    62  	maxExecutionWait  = 1 * time.Minute
    63  	defaultMaxDelay   = 1 * time.Minute
    64  )
    65  
    66  // NewTask creates a new task with a descriptive name (non-unique), a optional
    67  // deadline, and the task function to be executed. You must call one of Queue,
    68  // QueuePrioritized, StartASAP, Schedule or Repeat in order to have the Task
    69  // executed.
    70  func (m *Module) NewTask(name string, fn func(context.Context, *Task) error) *Task {
    71  	if m == nil {
    72  		log.Errorf(`modules: cannot create task "%s" with nil module`, name)
    73  		return &Task{
    74  			name:     name,
    75  			module:   &Module{Name: "[NONE]"},
    76  			canceled: true,
    77  		}
    78  	}
    79  
    80  	m.Lock()
    81  	defer m.Unlock()
    82  
    83  	return m.newTask(name, fn)
    84  }
    85  
    86  func (m *Module) newTask(name string, fn func(context.Context, *Task) error) *Task {
    87  	if m.Ctx == nil || !m.OnlineSoon() {
    88  		log.Errorf(`modules: tasks should only be started when the module is online or starting`)
    89  		return &Task{
    90  			name:     name,
    91  			module:   m,
    92  			canceled: true,
    93  		}
    94  	}
    95  
    96  	// create new task
    97  	newTask := &Task{
    98  		name:     name,
    99  		module:   m,
   100  		taskFn:   fn,
   101  		maxDelay: defaultMaxDelay,
   102  	}
   103  
   104  	// create context
   105  	newTask.ctx, newTask.cancelCtx = context.WithCancel(m.Ctx)
   106  
   107  	return newTask
   108  }
   109  
   110  func (t *Task) isActive() bool {
   111  	if t.canceled {
   112  		return false
   113  	}
   114  	return t.module.OnlineSoon()
   115  }
   116  
   117  func (t *Task) prepForQueueing() (ok bool) {
   118  	if !t.isActive() {
   119  		return false
   120  	}
   121  
   122  	if t.maxDelay != 0 {
   123  		t.executeAt = time.Now().Add(t.maxDelay)
   124  		t.addToSchedule(true)
   125  	}
   126  
   127  	return true
   128  }
   129  
   130  func notifyQueue() {
   131  	select {
   132  	case queueIsFilled <- struct{}{}:
   133  	default:
   134  	}
   135  }
   136  
   137  // Queue queues the Task for execution.
   138  func (t *Task) Queue() *Task {
   139  	t.lock.Lock()
   140  	defer t.lock.Unlock()
   141  
   142  	if !t.prepForQueueing() {
   143  		return t
   144  	}
   145  
   146  	if t.queueElement == nil {
   147  		queuesLock.Lock()
   148  		t.queueElement = taskQueue.PushBack(t)
   149  		queuesLock.Unlock()
   150  	}
   151  
   152  	notifyQueue()
   153  	return t
   154  }
   155  
   156  // QueuePrioritized queues the Task for execution in the prioritized queue.
   157  func (t *Task) QueuePrioritized() *Task {
   158  	t.lock.Lock()
   159  	defer t.lock.Unlock()
   160  
   161  	if !t.prepForQueueing() {
   162  		return t
   163  	}
   164  
   165  	if t.prioritizedQueueElement == nil {
   166  		queuesLock.Lock()
   167  		t.prioritizedQueueElement = prioritizedTaskQueue.PushBack(t)
   168  		queuesLock.Unlock()
   169  	}
   170  
   171  	notifyQueue()
   172  	return t
   173  }
   174  
   175  // StartASAP schedules the task to be executed next.
   176  func (t *Task) StartASAP() *Task {
   177  	t.lock.Lock()
   178  	defer t.lock.Unlock()
   179  
   180  	if !t.prepForQueueing() {
   181  		return t
   182  	}
   183  
   184  	queuesLock.Lock()
   185  	if t.prioritizedQueueElement == nil {
   186  		t.prioritizedQueueElement = prioritizedTaskQueue.PushFront(t)
   187  	} else {
   188  		prioritizedTaskQueue.MoveToFront(t.prioritizedQueueElement)
   189  	}
   190  	queuesLock.Unlock()
   191  
   192  	notifyQueue()
   193  	return t
   194  }
   195  
   196  // MaxDelay sets a maximum delay within the task should be executed from being queued. Scheduled tasks are queued when they are triggered. The default delay is 3 minutes.
   197  func (t *Task) MaxDelay(maxDelay time.Duration) *Task {
   198  	t.lock.Lock()
   199  	defer t.lock.Unlock()
   200  
   201  	t.maxDelay = maxDelay
   202  	return t
   203  }
   204  
   205  // Schedule schedules the task for execution at the given time. A zero time will remove cancel the scheduled execution.
   206  func (t *Task) Schedule(executeAt time.Time) *Task {
   207  	t.lock.Lock()
   208  	defer t.lock.Unlock()
   209  
   210  	t.executeAt = executeAt
   211  
   212  	if executeAt.IsZero() {
   213  		t.removeFromQueues()
   214  	} else {
   215  		t.addToSchedule(false)
   216  	}
   217  	return t
   218  }
   219  
   220  // Repeat sets the task to be executed in endless repeat at the specified interval. First execution will be after interval. Minimum repeat interval is one minute. An interval of zero will disable repeating, but won't change the current schedule.
   221  func (t *Task) Repeat(interval time.Duration) *Task {
   222  	// check minimum interval duration
   223  	if interval != 0 && interval < minRepeatDuration {
   224  		interval = minRepeatDuration
   225  	}
   226  
   227  	t.lock.Lock()
   228  	defer t.lock.Unlock()
   229  
   230  	// Check if repeating should be disabled.
   231  	if interval == 0 {
   232  		t.repeat = 0
   233  		return t
   234  	}
   235  
   236  	t.repeat = interval
   237  	t.executeAt = time.Now().Add(t.repeat)
   238  	t.addToSchedule(false)
   239  	return t
   240  }
   241  
   242  // Cancel cancels the current and any future execution of the Task. This is not reversible by any other functions.
   243  func (t *Task) Cancel() {
   244  	t.lock.Lock()
   245  	defer t.lock.Unlock()
   246  
   247  	t.canceled = true
   248  	if t.cancelCtx != nil {
   249  		t.cancelCtx()
   250  	}
   251  }
   252  
   253  func (t *Task) removeFromQueues() {
   254  	// remove from lists
   255  	if t.queueElement != nil {
   256  		queuesLock.Lock()
   257  		taskQueue.Remove(t.queueElement)
   258  		queuesLock.Unlock()
   259  		t.queueElement = nil
   260  	}
   261  	if t.prioritizedQueueElement != nil {
   262  		queuesLock.Lock()
   263  		prioritizedTaskQueue.Remove(t.prioritizedQueueElement)
   264  		queuesLock.Unlock()
   265  		t.prioritizedQueueElement = nil
   266  	}
   267  	if t.scheduleListElement != nil {
   268  		scheduleLock.Lock()
   269  		taskSchedule.Remove(t.scheduleListElement)
   270  		t.overtime = false
   271  		scheduleLock.Unlock()
   272  		t.scheduleListElement = nil
   273  	}
   274  }
   275  
   276  func (t *Task) runWithLocking() {
   277  	t.lock.Lock()
   278  
   279  	// we will not attempt execution, remove from queues
   280  	t.removeFromQueues()
   281  
   282  	// check if task is already executing
   283  	if t.executing {
   284  		t.lock.Unlock()
   285  		return
   286  	}
   287  
   288  	// check if task is active
   289  	// - has not been cancelled
   290  	// - module is online (soon)
   291  	if !t.isActive() {
   292  		t.lock.Unlock()
   293  		return
   294  	}
   295  
   296  	// check if module was stopped
   297  	select {
   298  	case <-t.ctx.Done():
   299  		t.lock.Unlock()
   300  		return
   301  	default:
   302  	}
   303  
   304  	// enter executing state
   305  	t.executing = true
   306  	t.lock.Unlock()
   307  
   308  	// wait for good timeslot regarding microtasks
   309  	select {
   310  	case <-taskTimeslot:
   311  	case <-time.After(maxTimeslotWait):
   312  	}
   313  
   314  	// wait for module start
   315  	if !t.module.Online() {
   316  		if t.module.OnlineSoon() {
   317  			// wait
   318  			<-t.module.StartCompleted()
   319  		} else {
   320  			// abort, module will not come online
   321  			t.lock.Lock()
   322  			t.executing = false
   323  			t.lock.Unlock()
   324  			return
   325  		}
   326  	}
   327  
   328  	// add to queue workgroup
   329  	queueWg.Add(1)
   330  
   331  	go t.executeWithLocking()
   332  	go func() {
   333  		select {
   334  		case <-t.ctx.Done():
   335  		case <-time.After(maxExecutionWait):
   336  		}
   337  		// complete queue worker (early) to allow next worker
   338  		queueWg.Done()
   339  	}()
   340  }
   341  
   342  func (t *Task) executeWithLocking() {
   343  	// start for module
   344  	// hint: only queueWg global var is important for scheduling, others can be set here
   345  	atomic.AddInt32(t.module.taskCnt, 1)
   346  
   347  	defer func() {
   348  		// recover from panic
   349  		panicVal := recover()
   350  		if panicVal != nil {
   351  			me := t.module.NewPanicError(t.name, "task", panicVal)
   352  			me.Report()
   353  			log.Errorf("%s: task %s panicked: %s\n%s", t.module.Name, t.name, panicVal, me.StackTrace)
   354  		}
   355  
   356  		// finish for module
   357  		atomic.AddInt32(t.module.taskCnt, -1)
   358  		t.module.checkIfStopComplete()
   359  
   360  		t.lock.Lock()
   361  
   362  		// reset state
   363  		t.executing = false
   364  
   365  		// repeat?
   366  		if t.isActive() && t.repeat != 0 && t.executeAt.IsZero() {
   367  			t.executeAt = time.Now().Add(t.repeat)
   368  			t.addToSchedule(false)
   369  		}
   370  
   371  		// notify that we finished
   372  		t.cancelCtx()
   373  		// refresh context
   374  
   375  		// RACE CONDITION with L314!
   376  		t.ctx, t.cancelCtx = context.WithCancel(t.module.Ctx)
   377  
   378  		t.lock.Unlock()
   379  	}()
   380  
   381  	// reset executeAt to detect if task set next execution itself
   382  	t.executeAt = time.Time{}
   383  
   384  	// run
   385  	err := t.taskFn(t.ctx, t)
   386  	switch {
   387  	case err == nil:
   388  		return
   389  	case errors.Is(err, context.Canceled):
   390  		log.Debugf("%s: task %s was canceled: %s", t.module.Name, t.name, err)
   391  	default:
   392  		log.Errorf("%s: task %s failed: %s", t.module.Name, t.name, err)
   393  	}
   394  }
   395  
   396  func (t *Task) addToSchedule(overtime bool) {
   397  	if !t.isActive() {
   398  		return
   399  	}
   400  
   401  	scheduleLock.Lock()
   402  	defer scheduleLock.Unlock()
   403  	// defer printTaskList(taskSchedule) // for debugging
   404  
   405  	if overtime {
   406  		// do not set to false
   407  		t.overtime = true
   408  	}
   409  
   410  	// notify scheduler
   411  	defer func() {
   412  		select {
   413  		case notifyTaskScheduler <- struct{}{}:
   414  		default:
   415  		}
   416  	}()
   417  
   418  	// insert task into schedule
   419  	for e := taskSchedule.Front(); e != nil; e = e.Next() {
   420  		// check for self
   421  		eVal := e.Value.(*Task) //nolint:forcetypeassert // Can only be *Task.
   422  		if eVal == t {
   423  			continue
   424  		}
   425  		// compare
   426  		if t.executeAt.Before(eVal.executeAt) {
   427  			// insert/move task
   428  			if t.scheduleListElement == nil {
   429  				t.scheduleListElement = taskSchedule.InsertBefore(t, e)
   430  			} else {
   431  				taskSchedule.MoveBefore(t.scheduleListElement, e)
   432  			}
   433  			return
   434  		}
   435  	}
   436  
   437  	// add/move to end
   438  	if t.scheduleListElement == nil {
   439  		t.scheduleListElement = taskSchedule.PushBack(t)
   440  	} else {
   441  		taskSchedule.MoveToBack(t.scheduleListElement)
   442  	}
   443  }
   444  
   445  func waitUntilNextScheduledTask() <-chan time.Time {
   446  	scheduleLock.Lock()
   447  	defer scheduleLock.Unlock()
   448  
   449  	if taskSchedule.Len() > 0 {
   450  		return time.After(time.Until(taskSchedule.Front().Value.(*Task).executeAt)) //nolint:forcetypeassert // Can only be *Task.
   451  	}
   452  	return waitForever
   453  }
   454  
   455  var (
   456  	taskQueueHandlerStarted    = abool.NewBool(false)
   457  	taskScheduleHandlerStarted = abool.NewBool(false)
   458  )
   459  
   460  func taskQueueHandler() {
   461  	// only ever start once
   462  	if !taskQueueHandlerStarted.SetToIf(false, true) {
   463  		return
   464  	}
   465  
   466  	for {
   467  		// wait
   468  		select {
   469  		case <-shutdownSignal:
   470  			return
   471  		case <-queueIsFilled:
   472  		}
   473  
   474  		// execute
   475  	execLoop:
   476  		for {
   477  			// wait for execution slot
   478  			queueWg.Wait()
   479  
   480  			// check for shutdown
   481  			if shutdownFlag.IsSet() {
   482  				return
   483  			}
   484  
   485  			// get next Task
   486  			queuesLock.Lock()
   487  			e := prioritizedTaskQueue.Front()
   488  			if e != nil {
   489  				prioritizedTaskQueue.Remove(e)
   490  			} else {
   491  				e = taskQueue.Front()
   492  				if e != nil {
   493  					taskQueue.Remove(e)
   494  				}
   495  			}
   496  			queuesLock.Unlock()
   497  
   498  			// lists are empty
   499  			if e == nil {
   500  				break execLoop
   501  			}
   502  
   503  			// value -> Task
   504  			t := e.Value.(*Task) //nolint:forcetypeassert // Can only be *Task.
   505  			// run
   506  			t.runWithLocking()
   507  		}
   508  	}
   509  }
   510  
   511  func taskScheduleHandler() {
   512  	// only ever start once
   513  	if !taskScheduleHandlerStarted.SetToIf(false, true) {
   514  		return
   515  	}
   516  
   517  	for {
   518  
   519  		if sleepMode.IsSet() {
   520  			select {
   521  			case <-shutdownSignal:
   522  				return
   523  			case <-notifyTaskScheduler:
   524  				continue
   525  			}
   526  		}
   527  
   528  		select {
   529  		case <-shutdownSignal:
   530  			return
   531  		case <-notifyTaskScheduler:
   532  			continue
   533  		case <-waitUntilNextScheduledTask():
   534  			scheduleLock.Lock()
   535  
   536  			// get first task in schedule
   537  			e := taskSchedule.Front()
   538  			if e == nil {
   539  				scheduleLock.Unlock()
   540  				continue
   541  			}
   542  			t := e.Value.(*Task) //nolint:forcetypeassert // Can only be *Task.
   543  
   544  			// process Task
   545  			if t.overtime {
   546  				// already queued and maxDelay reached
   547  				t.overtime = false
   548  				scheduleLock.Unlock()
   549  
   550  				t.runWithLocking()
   551  			} else {
   552  				// place in front of prioritized queue
   553  				t.overtime = true
   554  				scheduleLock.Unlock()
   555  
   556  				t.StartASAP()
   557  			}
   558  		}
   559  	}
   560  }
   561  
   562  func printTaskList(*list.List) { //nolint:unused,deadcode // for debugging, NOT production use
   563  	fmt.Println("Modules Task List:")
   564  	for e := taskSchedule.Front(); e != nil; e = e.Next() {
   565  		t, ok := e.Value.(*Task)
   566  		if ok {
   567  			fmt.Printf(
   568  				"%s:%s over=%v canc=%v exec=%v exat=%s rep=%s delay=%s\n",
   569  				t.module.Name,
   570  				t.name,
   571  				t.overtime,
   572  				t.canceled,
   573  				t.executing,
   574  				t.executeAt,
   575  				t.repeat,
   576  				t.maxDelay,
   577  			)
   578  		}
   579  	}
   580  }