github.com/weedge/lib@v0.0.0-20230424045628-a36dcc1d90e4/timingwheel/timingwheel.go (about)

     1  package timingwheel
     2  
     3  import (
     4  	"errors"
     5  	"sync/atomic"
     6  	"time"
     7  	"unsafe"
     8  
     9  	"github.com/weedge/lib/container/queue"
    10  )
    11  
    12  // TimingWheel is an implementation of Hierarchical Timing Wheels.
    13  type TimingWheel struct {
    14  	tick      int64 // in milliseconds
    15  	wheelSize int64
    16  
    17  	interval    int64 // in milliseconds
    18  	currentTime int64 // in milliseconds
    19  	buckets     []*bucket
    20  	queue       *queue.DelayQueue
    21  
    22  	// The higher-level overflow wheel.
    23  	//
    24  	// NOTE: This field may be updated and read concurrently, through Add().
    25  	overflowWheel unsafe.Pointer // type: *TimingWheel
    26  
    27  	exitC     chan struct{}
    28  	waitGroup waitGroupWrapper
    29  }
    30  
    31  // NewTimingWheel creates an instance of TimingWheel with the given tick and wheelSize.
    32  func NewTimingWheel(tick time.Duration, wheelSize int64) *TimingWheel {
    33  	tickMs := int64(tick / time.Millisecond)
    34  	if tickMs <= 0 {
    35  		panic(errors.New("tick must be greater than or equal to 1ms"))
    36  	}
    37  
    38  	startMs := timeToMs(time.Now().UTC())
    39  
    40  	return newTimingWheel(
    41  		tickMs,
    42  		wheelSize,
    43  		startMs,
    44  		queue.NewDelayQueue(int(wheelSize)),
    45  	)
    46  }
    47  
    48  // newTimingWheel is an internal helper function that really creates an instance of TimingWheel.
    49  func newTimingWheel(tickMs int64, wheelSize int64, startMs int64, queue *queue.DelayQueue) *TimingWheel {
    50  	buckets := make([]*bucket, wheelSize)
    51  	for i := range buckets {
    52  		buckets[i] = newBucket()
    53  	}
    54  	return &TimingWheel{
    55  		tick:        tickMs,
    56  		wheelSize:   wheelSize,
    57  		currentTime: truncate(startMs, tickMs),
    58  		interval:    tickMs * wheelSize,
    59  		buckets:     buckets,
    60  		queue:       queue,
    61  		exitC:       make(chan struct{}),
    62  	}
    63  }
    64  
    65  // add inserts the timer t into the current timing wheel.
    66  func (tw *TimingWheel) add(t *Timer) bool {
    67  	currentTime := atomic.LoadInt64(&tw.currentTime)
    68  	if t.expiration < currentTime+tw.tick {
    69  		// Already expired
    70  		return false
    71  	} else if t.expiration < currentTime+tw.interval {
    72  		// Put it into its own bucket
    73  		virtualID := t.expiration / tw.tick
    74  		b := tw.buckets[virtualID%tw.wheelSize]
    75  		b.Add(t)
    76  
    77  		// Set the bucket expiration time
    78  		if b.SetExpiration(virtualID * tw.tick) {
    79  			// The bucket needs to be enqueued since it was an expired bucket.
    80  			// We only need to enqueue the bucket when its expiration time has changed,
    81  			// i.e. the wheel has advanced and this bucket get reused with a new expiration.
    82  			// Any further calls to set the expiration within the same wheel cycle will
    83  			// pass in the same value and hence return false, thus the bucket with the
    84  			// same expiration will not be enqueued multiple times.
    85  			tw.queue.Offer(b, b.Expiration())
    86  		}
    87  
    88  		return true
    89  	} else {
    90  		// Out of the interval. Put it into the overflow wheel
    91  		overflowWheel := atomic.LoadPointer(&tw.overflowWheel)
    92  		if overflowWheel == nil {
    93  			atomic.CompareAndSwapPointer(
    94  				&tw.overflowWheel,
    95  				nil,
    96  				unsafe.Pointer(newTimingWheel(
    97  					tw.interval,
    98  					tw.wheelSize,
    99  					currentTime,
   100  					tw.queue,
   101  				)),
   102  			)
   103  			overflowWheel = atomic.LoadPointer(&tw.overflowWheel)
   104  		}
   105  		return (*TimingWheel)(overflowWheel).add(t)
   106  	}
   107  }
   108  
   109  // addOrRun inserts the timer t into the current timing wheel, or run the
   110  // timer's task if it has already expired.
   111  func (tw *TimingWheel) addOrRun(t *Timer) {
   112  	if !tw.add(t) {
   113  		// Already expired
   114  
   115  		// Like the standard time.AfterFunc (https://golang.org/pkg/time/#AfterFunc),
   116  		// always execute the timer's task in its own goroutine.
   117  		go t.task()
   118  	}
   119  }
   120  
   121  func (tw *TimingWheel) advanceClock(expiration int64) {
   122  	currentTime := atomic.LoadInt64(&tw.currentTime)
   123  	if expiration >= currentTime+tw.tick {
   124  		currentTime = truncate(expiration, tw.tick)
   125  		atomic.StoreInt64(&tw.currentTime, currentTime)
   126  
   127  		// Try to advance the clock of the overflow wheel if present
   128  		overflowWheel := atomic.LoadPointer(&tw.overflowWheel)
   129  		if overflowWheel != nil {
   130  			(*TimingWheel)(overflowWheel).advanceClock(currentTime)
   131  		}
   132  	}
   133  }
   134  
   135  // Start starts the current timing wheel.
   136  func (tw *TimingWheel) Start() {
   137  	tw.waitGroup.Wrap(func() {
   138  		tw.queue.Poll(tw.exitC, func() int64 {
   139  			return timeToMs(time.Now().UTC())
   140  		})
   141  	})
   142  
   143  	tw.waitGroup.Wrap(func() {
   144  		tw.queue.Do(tw.exitC, func(elem interface{}) {
   145  			b := elem.(*bucket)
   146  			tw.advanceClock(b.Expiration())
   147  			b.Flush(tw.addOrRun)
   148  		})
   149  	})
   150  }
   151  
   152  // Stop stops the current timing wheel.
   153  //
   154  // If there is any timer's task being running in its own goroutine, Stop does
   155  // not wait for the task to complete before returning. If the caller needs to
   156  // know whether the task is completed, it must coordinate with the task explicitly.
   157  func (tw *TimingWheel) Stop() {
   158  	close(tw.exitC)
   159  	tw.waitGroup.Wait()
   160  }
   161  
   162  // AfterFunc waits for the duration to elapse and then calls f in its own goroutine.
   163  // It returns a Timer that can be used to cancel the call using its Stop method.
   164  func (tw *TimingWheel) AfterFunc(d time.Duration, f func()) *Timer {
   165  	t := &Timer{
   166  		expiration: timeToMs(time.Now().UTC().Add(d)),
   167  		task:       f,
   168  	}
   169  	tw.addOrRun(t)
   170  	return t
   171  }
   172  
   173  // Scheduler determines the execution plan of a task.
   174  type Scheduler interface {
   175  	// Next returns the next execution time after the given (previous) time.
   176  	// It will return a zero time if no next time is scheduled.
   177  	//
   178  	// All times must be UTC.
   179  	Next(time.Time) time.Time
   180  }
   181  
   182  // ScheduleFunc calls f (in its own goroutine) according to the execution
   183  // plan scheduled by s. It returns a Timer that can be used to cancel the
   184  // call using its Stop method.
   185  //
   186  // If the caller want to terminate the execution plan halfway, it must
   187  // stop the timer and ensure that the timer is stopped actually, since in
   188  // the current implementation, there is a gap between the expiring and the
   189  // restarting of the timer. The wait time for ensuring is short since the
   190  // gap is very small.
   191  //
   192  // Internally, ScheduleFunc will ask the first execution time (by calling
   193  // s.Next()) initially, and create a timer if the execution time is non-zero.
   194  // Afterwards, it will ask the next execution time each time f is about to
   195  // be executed, and f will be called at the next execution time if the time
   196  // is non-zero.
   197  func (tw *TimingWheel) ScheduleFunc(s Scheduler, f func()) (t *Timer) {
   198  	expiration := s.Next(time.Now().UTC())
   199  	if expiration.IsZero() {
   200  		// No time is scheduled, return nil.
   201  		return
   202  	}
   203  
   204  	t = &Timer{
   205  		expiration: timeToMs(expiration),
   206  		task: func() {
   207  			// Schedule the task to execute at the next time if possible.
   208  			expiration := s.Next(msToTime(t.expiration))
   209  			if !expiration.IsZero() {
   210  				t.expiration = timeToMs(expiration)
   211  				tw.addOrRun(t)
   212  			}
   213  
   214  			// Actually execute the task.
   215  			f()
   216  		},
   217  	}
   218  	tw.addOrRun(t)
   219  
   220  	return
   221  }