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

     1  package modules
     2  
     3  import (
     4  	"context"
     5  	"runtime"
     6  	"sync/atomic"
     7  	"time"
     8  
     9  	"github.com/tevino/abool"
    10  
    11  	"github.com/safing/portbase/log"
    12  )
    13  
    14  // TODO: getting some errors when in nanosecond precision for tests:
    15  // (1) panic: sync: WaitGroup is reused before previous Wait has returned - should theoretically not happen
    16  // (2) sometimes there seems to some kind of race condition stuff, the test hangs and does not complete
    17  // NOTE: These might be resolved by the switch to clearance queues.
    18  
    19  var (
    20  	microTasks           *int32
    21  	microTasksThreshhold *int32
    22  	microTaskFinished    = make(chan struct{}, 1)
    23  )
    24  
    25  const (
    26  	defaultMediumPriorityMaxDelay = 1 * time.Second
    27  	defaultLowPriorityMaxDelay    = 3 * time.Second
    28  )
    29  
    30  func init() {
    31  	var microTasksVal int32
    32  	microTasks = &microTasksVal
    33  
    34  	microTasksThreshholdVal := int32(runtime.GOMAXPROCS(0) * 2)
    35  	microTasksThreshhold = &microTasksThreshholdVal
    36  }
    37  
    38  // SetMaxConcurrentMicroTasks sets the maximum number of microtasks that should
    39  // be run concurrently. The modules system initializes it with GOMAXPROCS.
    40  // The minimum is 2.
    41  func SetMaxConcurrentMicroTasks(n int) {
    42  	if n < 2 {
    43  		atomic.StoreInt32(microTasksThreshhold, 2)
    44  	} else {
    45  		atomic.StoreInt32(microTasksThreshhold, int32(n))
    46  	}
    47  }
    48  
    49  // StartHighPriorityMicroTask starts a new MicroTask with high priority.
    50  // It will start immediately.
    51  // The call starts a new goroutine and returns immediately.
    52  // The given function will be executed and panics caught.
    53  func (m *Module) StartHighPriorityMicroTask(name string, fn func(context.Context) error) {
    54  	go func() {
    55  		err := m.RunHighPriorityMicroTask(name, fn)
    56  		if err != nil {
    57  			log.Warningf("%s: microtask %s failed: %s", m.Name, name, err)
    58  		}
    59  	}()
    60  }
    61  
    62  // StartMicroTask starts a new MicroTask with medium priority.
    63  // The call starts a new goroutine and returns immediately.
    64  // It will wait until a slot becomes available while respecting maxDelay.
    65  // You can also set maxDelay to 0 to use the default value of 1 second.
    66  // The given function will be executed and panics caught.
    67  func (m *Module) StartMicroTask(name string, maxDelay time.Duration, fn func(context.Context) error) {
    68  	go func() {
    69  		err := m.RunMicroTask(name, maxDelay, fn)
    70  		if err != nil {
    71  			log.Warningf("%s: microtask %s failed: %s", m.Name, name, err)
    72  		}
    73  	}()
    74  }
    75  
    76  // StartLowPriorityMicroTask starts a new MicroTask with low priority.
    77  // The call starts a new goroutine and returns immediately.
    78  // It will wait until a slot becomes available while respecting maxDelay.
    79  // You can also set maxDelay to 0 to use the default value of 3 seconds.
    80  // The given function will be executed and panics caught.
    81  func (m *Module) StartLowPriorityMicroTask(name string, maxDelay time.Duration, fn func(context.Context) error) {
    82  	go func() {
    83  		err := m.RunLowPriorityMicroTask(name, maxDelay, fn)
    84  		if err != nil {
    85  			log.Warningf("%s: microtask %s failed: %s", m.Name, name, err)
    86  		}
    87  	}()
    88  }
    89  
    90  // RunHighPriorityMicroTask starts a new MicroTask with high priority.
    91  // The given function will be executed and panics caught.
    92  // The call blocks until the given function finishes.
    93  func (m *Module) RunHighPriorityMicroTask(name string, fn func(context.Context) error) error {
    94  	if m == nil {
    95  		log.Errorf(`modules: cannot start microtask "%s" with nil module`, name)
    96  		return errNoModule
    97  	}
    98  
    99  	// Increase global counter here, as high priority tasks do not wait for clearance.
   100  	atomic.AddInt32(microTasks, 1)
   101  	return m.runMicroTask(name, fn)
   102  }
   103  
   104  // RunMicroTask starts a new MicroTask with medium priority.
   105  // It will wait until a slot becomes available while respecting maxDelay.
   106  // You can also set maxDelay to 0 to use the default value of 1 second.
   107  // The given function will be executed and panics caught.
   108  // The call blocks until the given function finishes.
   109  func (m *Module) RunMicroTask(name string, maxDelay time.Duration, fn func(context.Context) error) error {
   110  	if m == nil {
   111  		log.Errorf(`modules: cannot start microtask "%s" with nil module`, name)
   112  		return errNoModule
   113  	}
   114  
   115  	// Set default max delay, if not defined.
   116  	if maxDelay <= 0 {
   117  		maxDelay = defaultMediumPriorityMaxDelay
   118  	}
   119  
   120  	getMediumPriorityClearance(maxDelay)
   121  	return m.runMicroTask(name, fn)
   122  }
   123  
   124  // RunLowPriorityMicroTask starts a new MicroTask with low priority.
   125  // It will wait until a slot becomes available while respecting maxDelay.
   126  // You can also set maxDelay to 0 to use the default value of 3 seconds.
   127  // The given function will be executed and panics caught.
   128  // The call blocks until the given function finishes.
   129  func (m *Module) RunLowPriorityMicroTask(name string, maxDelay time.Duration, fn func(context.Context) error) error {
   130  	if m == nil {
   131  		log.Errorf(`modules: cannot start microtask "%s" with nil module`, name)
   132  		return errNoModule
   133  	}
   134  
   135  	// Set default max delay, if not defined.
   136  	if maxDelay <= 0 {
   137  		maxDelay = defaultLowPriorityMaxDelay
   138  	}
   139  
   140  	getLowPriorityClearance(maxDelay)
   141  	return m.runMicroTask(name, fn)
   142  }
   143  
   144  func (m *Module) runMicroTask(name string, fn func(context.Context) error) (err error) {
   145  	// start for module
   146  	// hint: only microTasks global var is important for scheduling, others can be set here
   147  	atomic.AddInt32(m.microTaskCnt, 1)
   148  
   149  	// set up recovery
   150  	defer func() {
   151  		// recover from panic
   152  		panicVal := recover()
   153  		if panicVal != nil {
   154  			me := m.NewPanicError(name, "microtask", panicVal)
   155  			me.Report()
   156  			log.Errorf("%s: microtask %s panicked: %s", m.Name, name, panicVal)
   157  			err = me
   158  		}
   159  
   160  		m.concludeMicroTask()
   161  	}()
   162  
   163  	// run
   164  	err = fn(m.Ctx)
   165  	return // Use named return val in order to change it in defer.
   166  }
   167  
   168  // SignalHighPriorityMicroTask signals the start of a new MicroTask with high priority.
   169  // The returned "done" function SHOULD be called when the task has finished
   170  // and MUST be called in any case. Failing to do so will have devastating effects.
   171  // You can safely call "done" multiple times; additional calls do nothing.
   172  func (m *Module) SignalHighPriorityMicroTask() (done func()) {
   173  	if m == nil {
   174  		log.Errorf("modules: cannot signal microtask with nil module")
   175  		return
   176  	}
   177  
   178  	// Increase global counter here, as high priority tasks do not wait for clearance.
   179  	atomic.AddInt32(microTasks, 1)
   180  	return m.signalMicroTask()
   181  }
   182  
   183  // SignalMicroTask signals the start of a new MicroTask with medium priority.
   184  // The call will wait until a slot becomes available while respecting maxDelay.
   185  // You can also set maxDelay to 0 to use the default value of 1 second.
   186  // The returned "done" function SHOULD be called when the task has finished
   187  // and MUST be called in any case. Failing to do so will have devastating effects.
   188  // You can safely call "done" multiple times; additional calls do nothing.
   189  func (m *Module) SignalMicroTask(maxDelay time.Duration) (done func()) {
   190  	if m == nil {
   191  		log.Errorf("modules: cannot signal microtask with nil module")
   192  		return
   193  	}
   194  
   195  	getMediumPriorityClearance(maxDelay)
   196  	return m.signalMicroTask()
   197  }
   198  
   199  // SignalLowPriorityMicroTask signals the start of a new MicroTask with low priority.
   200  // The call will wait until a slot becomes available while respecting maxDelay.
   201  // You can also set maxDelay to 0 to use the default value of 1 second.
   202  // The returned "done" function SHOULD be called when the task has finished
   203  // and MUST be called in any case. Failing to do so will have devastating effects.
   204  // You can safely call "done" multiple times; additional calls do nothing.
   205  func (m *Module) SignalLowPriorityMicroTask(maxDelay time.Duration) (done func()) {
   206  	if m == nil {
   207  		log.Errorf("modules: cannot signal microtask with nil module")
   208  		return
   209  	}
   210  
   211  	getLowPriorityClearance(maxDelay)
   212  	return m.signalMicroTask()
   213  }
   214  
   215  func (m *Module) signalMicroTask() (done func()) {
   216  	// Start microtask for module.
   217  	// Global counter is set earlier as required for scheduling.
   218  	atomic.AddInt32(m.microTaskCnt, 1)
   219  
   220  	doneCalled := abool.New()
   221  	return func() {
   222  		if doneCalled.SetToIf(false, true) {
   223  			m.concludeMicroTask()
   224  		}
   225  	}
   226  }
   227  
   228  func (m *Module) concludeMicroTask() {
   229  	// Finish for module.
   230  	atomic.AddInt32(m.microTaskCnt, -1)
   231  	m.checkIfStopComplete()
   232  
   233  	// Finish and possibly trigger next task.
   234  	atomic.AddInt32(microTasks, -1)
   235  	select {
   236  	case microTaskFinished <- struct{}{}:
   237  	default:
   238  	}
   239  }
   240  
   241  var (
   242  	clearanceQueueBaseSize  = 100
   243  	clearanceQueueSize      = runtime.GOMAXPROCS(0) * clearanceQueueBaseSize
   244  	mediumPriorityClearance = make(chan chan struct{}, clearanceQueueSize)
   245  	lowPriorityClearance    = make(chan chan struct{}, clearanceQueueSize)
   246  
   247  	triggerLogWriting = log.TriggerWriterChannel()
   248  
   249  	microTaskSchedulerStarted = abool.NewBool(false)
   250  )
   251  
   252  func microTaskScheduler() {
   253  	var clearanceSignal chan struct{}
   254  
   255  	// Create ticker for max delay for checking clearances.
   256  	recheck := time.NewTicker(1 * time.Second)
   257  	defer recheck.Stop()
   258  
   259  	// only ever start once
   260  	if !microTaskSchedulerStarted.SetToIf(false, true) {
   261  		return
   262  	}
   263  
   264  	// Debugging: Print current amount of microtasks.
   265  	// go func() {
   266  	// 	for {
   267  	// 		time.Sleep(1 * time.Second)
   268  	// 		log.Debugf("modules: microtasks: %d", atomic.LoadInt32(microTasks))
   269  	// 	}
   270  	// }()
   271  
   272  	for {
   273  		if shutdownFlag.IsSet() {
   274  			go microTaskShutdownScheduler()
   275  			return
   276  		}
   277  
   278  		// Check if there is space for one more microtask.
   279  		if atomic.LoadInt32(microTasks) < atomic.LoadInt32(microTasksThreshhold) { // space left for firing task
   280  			// Give Medium clearance.
   281  			select {
   282  			case clearanceSignal = <-mediumPriorityClearance:
   283  			default:
   284  
   285  				// Give Medium and Low clearance.
   286  				select {
   287  				case clearanceSignal = <-mediumPriorityClearance:
   288  				case clearanceSignal = <-lowPriorityClearance:
   289  				default:
   290  
   291  					// Give Medium, Low and other clearancee.
   292  					select {
   293  					case clearanceSignal = <-mediumPriorityClearance:
   294  					case clearanceSignal = <-lowPriorityClearance:
   295  					case taskTimeslot <- struct{}{}:
   296  					case triggerLogWriting <- struct{}{}:
   297  					}
   298  				}
   299  			}
   300  
   301  			// Send clearance signal and increase task counter.
   302  			if clearanceSignal != nil {
   303  				close(clearanceSignal)
   304  				atomic.AddInt32(microTasks, 1)
   305  			}
   306  			clearanceSignal = nil
   307  		} else {
   308  			// wait for signal that a task was completed
   309  			select {
   310  			case <-microTaskFinished:
   311  			case <-recheck.C:
   312  			}
   313  		}
   314  
   315  	}
   316  }
   317  
   318  func microTaskShutdownScheduler() {
   319  	var clearanceSignal chan struct{}
   320  
   321  	for {
   322  		// During shutdown, always give clearances immediately.
   323  		select {
   324  		case clearanceSignal = <-mediumPriorityClearance:
   325  		case clearanceSignal = <-lowPriorityClearance:
   326  		case taskTimeslot <- struct{}{}:
   327  		case triggerLogWriting <- struct{}{}:
   328  		}
   329  
   330  		// Give clearance if requested.
   331  		if clearanceSignal != nil {
   332  			close(clearanceSignal)
   333  			atomic.AddInt32(microTasks, 1)
   334  		}
   335  		clearanceSignal = nil
   336  	}
   337  }
   338  
   339  func getMediumPriorityClearance(maxDelay time.Duration) {
   340  	// Submit signal to scheduler.
   341  	signal := make(chan struct{})
   342  	select {
   343  	case mediumPriorityClearance <- signal:
   344  	default:
   345  		select {
   346  		case mediumPriorityClearance <- signal:
   347  		case <-time.After(maxDelay):
   348  			// Start without clearance and increase microtask counter.
   349  			atomic.AddInt32(microTasks, 1)
   350  			return
   351  		}
   352  	}
   353  	// Wait for signal to start.
   354  	select {
   355  	case <-signal:
   356  	default:
   357  		select {
   358  		case <-signal:
   359  		case <-time.After(maxDelay):
   360  			// Don't keep waiting for signal forever.
   361  			// Don't increase microtask counter, as the signal was already submitted
   362  			// and the counter will be increased by the scheduler.
   363  		}
   364  	}
   365  }
   366  
   367  func getLowPriorityClearance(maxDelay time.Duration) {
   368  	// Submit signal to scheduler.
   369  	signal := make(chan struct{})
   370  	select {
   371  	case lowPriorityClearance <- signal:
   372  	default:
   373  		select {
   374  		case lowPriorityClearance <- signal:
   375  		case <-time.After(maxDelay):
   376  			// Start without clearance and increase microtask counter.
   377  			atomic.AddInt32(microTasks, 1)
   378  			return
   379  		}
   380  	}
   381  	// Wait for signal to start.
   382  	select {
   383  	case <-signal:
   384  	default:
   385  		select {
   386  		case <-signal:
   387  		case <-time.After(maxDelay):
   388  			// Don't keep waiting for signal forever.
   389  			// Don't increase microtask counter, as the signal was already submitted
   390  			// and the counter will be increased by the scheduler.
   391  		}
   392  	}
   393  }