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 }