scheduler.go 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
  1. package nebula
  2. import (
  3. "context"
  4. "sync"
  5. "time"
  6. )
  7. // Scheduler is an allocation-conscious dispatch primitive for delayed work.
  8. // Pending items are handed to time.AfterFunc, and ready items land on a worker
  9. // channel for centralized dispatch in fire-time order.
  10. //
  11. // Pick a Scheduler when fire timing matters (exact deadlines, no bucketing) or when the scheduling
  12. // rate is uneven enough that idle CPU matters. Each fire is a runtime-spawned goroutine running the callback before
  13. // delivering to the worker, which is fine at sparse rates but adds up at line rate.
  14. //
  15. // Pick a TimerWheel when scheduling is high-rate and uniform: its O(1) insert, internal item cache,
  16. // and bucket-batched dispatch are cheaper at scale.
  17. // The caller drives the tick loop (Advance/Purge) and pays for fires at bucket boundaries rather than exact deadlines.
  18. type Scheduler[T any] struct {
  19. queue chan T
  20. pool sync.Pool
  21. }
  22. type schedItem[T any] struct {
  23. val T
  24. ctx context.Context
  25. s *Scheduler[T]
  26. timer *time.Timer
  27. fire func()
  28. }
  29. // NewScheduler builds a Scheduler whose worker channel is sized to queueSize.
  30. // The buffer absorbs bursts of timers firing close together without
  31. // blocking the runtime's callback goroutines on the worker.
  32. func NewScheduler[T any](queueSize int) *Scheduler[T] {
  33. s := &Scheduler[T]{
  34. queue: make(chan T, queueSize),
  35. }
  36. s.pool.New = func() any {
  37. si := &schedItem[T]{s: s}
  38. // fire is allocated exactly once per pool-resident item.
  39. // The closure captures only `si`, which stays stable for the item's lifetime.
  40. si.fire = func() {
  41. select {
  42. case si.s.queue <- si.val:
  43. case <-si.ctx.Done():
  44. }
  45. var zero T
  46. si.val = zero
  47. si.ctx = nil
  48. si.s.pool.Put(si)
  49. }
  50. return si
  51. }
  52. return s
  53. }
  54. // Schedule arranges item to be delivered to the worker after delay.
  55. // The runtime's timer heap handles the wait, so the scheduler itself burns no CPU while idle.
  56. // The callback observes ctx: if ctx is cancelled before the timer fires, the item is dropped instead of queued.
  57. func (s *Scheduler[T]) Schedule(ctx context.Context, item T, delay time.Duration) {
  58. si := s.pool.Get().(*schedItem[T])
  59. si.val = item
  60. si.ctx = ctx
  61. if si.timer == nil {
  62. si.timer = time.AfterFunc(delay, si.fire)
  63. } else {
  64. si.timer.Reset(delay)
  65. }
  66. }
  67. // Run drains the worker queue, calling fn for each item. Returns when ctx is cancelled.
  68. // Tests that want deterministic timing should drive the queue directly rather than going through Schedule + Run.
  69. func (s *Scheduler[T]) Run(ctx context.Context, fn func(T)) {
  70. for {
  71. select {
  72. case <-ctx.Done():
  73. return
  74. case item := <-s.queue:
  75. fn(item)
  76. }
  77. }
  78. }