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 = µTasksVal 33 34 microTasksThreshholdVal := int32(runtime.GOMAXPROCS(0) * 2) 35 microTasksThreshhold = µTasksThreshholdVal 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 }