github.com/mailgun/holster/v4@v4.20.0/clock/frozen.go (about)

     1  package clock
     2  
     3  import (
     4  	"sync"
     5  	"time"
     6  
     7  	"github.com/mailgun/holster/v4/errors"
     8  )
     9  
    10  type frozenTime struct {
    11  	mu       sync.Mutex
    12  	frozenAt time.Time
    13  	now      time.Time
    14  	timers   []*frozenTimer
    15  	waiter   *waiter
    16  }
    17  
    18  type waiter struct {
    19  	count    int
    20  	signalCh chan struct{}
    21  }
    22  
    23  func (ft *frozenTime) Now() time.Time {
    24  	ft.mu.Lock()
    25  	defer ft.mu.Unlock()
    26  	return ft.now
    27  }
    28  
    29  func (ft *frozenTime) Sleep(d time.Duration) {
    30  	<-ft.NewTimer(d).C()
    31  }
    32  
    33  func (ft *frozenTime) After(d time.Duration) <-chan time.Time {
    34  	return ft.NewTimer(d).C()
    35  }
    36  
    37  func (ft *frozenTime) NewTimer(d time.Duration) Timer {
    38  	return ft.AfterFunc(d, nil)
    39  }
    40  
    41  func (ft *frozenTime) AfterFunc(d time.Duration, f func()) Timer {
    42  	t := &frozenTimer{
    43  		ft:   ft,
    44  		when: ft.Now().Add(d),
    45  		f:    f,
    46  	}
    47  	if f == nil {
    48  		t.c = make(chan time.Time, 1)
    49  	}
    50  	ft.startTimer(t)
    51  	return t
    52  }
    53  
    54  func (ft *frozenTime) advance(d time.Duration) {
    55  	ft.mu.Lock()
    56  	defer ft.mu.Unlock()
    57  
    58  	ft.now = ft.now.Add(d)
    59  	for t := ft.nextExpired(); t != nil; t = ft.nextExpired() {
    60  		// Send the timer expiration time to the timer channel if it is
    61  		// defined. But make sure not to block on the send if the channel is
    62  		// full. This behavior will make a ticker skip beats if it readers are
    63  		// not fast enough.
    64  		if t.c != nil {
    65  			select {
    66  			case t.c <- t.when:
    67  			default:
    68  			}
    69  		}
    70  		// If it is a ticking timer then schedule next tick, otherwise mark it
    71  		// as stopped.
    72  		if t.interval != 0 {
    73  			t.when = t.when.Add(t.interval)
    74  			t.stopped = false
    75  			ft.unlockedStartTimer(t)
    76  		}
    77  		// If a function is associated with the timer then call it, but make
    78  		// sure to release the lock for the time of call it is necessary
    79  		// because the lock is not re-entrant but the function may need to
    80  		// start another timer or ticker.
    81  		if t.f != nil {
    82  			func() {
    83  				ft.mu.Unlock()
    84  				defer ft.mu.Lock()
    85  				t.f()
    86  			}()
    87  		}
    88  	}
    89  }
    90  
    91  func (ft *frozenTime) stopTimer(t *frozenTimer) bool {
    92  	ft.mu.Lock()
    93  	defer ft.mu.Unlock()
    94  
    95  	if t.stopped {
    96  		return false
    97  	}
    98  	for i, curr := range ft.timers {
    99  		if curr != t {
   100  			continue
   101  		}
   102  
   103  		t.stopped = true
   104  		copy(ft.timers[i:], ft.timers[i+1:])
   105  		lastIdx := len(ft.timers) - 1
   106  		ft.timers[lastIdx] = nil
   107  		ft.timers = ft.timers[:lastIdx]
   108  		return true
   109  	}
   110  	return false
   111  }
   112  
   113  func (ft *frozenTime) nextExpired() *frozenTimer {
   114  	if len(ft.timers) == 0 {
   115  		return nil
   116  	}
   117  	t := ft.timers[0]
   118  	if ft.now.Before(t.when) {
   119  		return nil
   120  	}
   121  	copy(ft.timers, ft.timers[1:])
   122  	lastIdx := len(ft.timers) - 1
   123  	ft.timers[lastIdx] = nil
   124  	ft.timers = ft.timers[:lastIdx]
   125  	t.stopped = true
   126  	return t
   127  }
   128  
   129  func (ft *frozenTime) startTimer(t *frozenTimer) {
   130  	ft.mu.Lock()
   131  	defer ft.mu.Unlock()
   132  
   133  	ft.unlockedStartTimer(t)
   134  
   135  	if ft.waiter == nil {
   136  		return
   137  	}
   138  	if len(ft.timers) >= ft.waiter.count {
   139  		close(ft.waiter.signalCh)
   140  	}
   141  }
   142  
   143  func (ft *frozenTime) unlockedStartTimer(t *frozenTimer) {
   144  	pos := 0
   145  	for _, curr := range ft.timers {
   146  		if t.when.Before(curr.when) {
   147  			break
   148  		}
   149  		pos++
   150  	}
   151  	ft.timers = append(ft.timers, nil)
   152  	copy(ft.timers[pos+1:], ft.timers[pos:])
   153  	ft.timers[pos] = t
   154  }
   155  
   156  type frozenTimer struct {
   157  	ft       *frozenTime
   158  	when     time.Time
   159  	interval time.Duration
   160  	stopped  bool
   161  	c        chan time.Time
   162  	f        func()
   163  }
   164  
   165  func (t *frozenTimer) C() <-chan time.Time {
   166  	return t.c
   167  }
   168  
   169  func (t *frozenTimer) Stop() bool {
   170  	return t.ft.stopTimer(t)
   171  }
   172  
   173  func (t *frozenTimer) Reset(d time.Duration) bool {
   174  	active := t.ft.stopTimer(t)
   175  	t.when = t.ft.Now().Add(d)
   176  	t.ft.startTimer(t)
   177  	return active
   178  }
   179  
   180  type frozenTicker struct {
   181  	t *frozenTimer
   182  }
   183  
   184  func (t *frozenTicker) C() <-chan time.Time {
   185  	return t.t.C()
   186  }
   187  
   188  func (t *frozenTicker) Stop() {
   189  	t.t.Stop()
   190  }
   191  
   192  func (ft *frozenTime) NewTicker(d time.Duration) Ticker {
   193  	if d <= 0 {
   194  		panic(errors.New("non-positive interval for NewTicker"))
   195  	}
   196  	t := &frozenTimer{
   197  		ft:       ft,
   198  		when:     ft.Now().Add(d),
   199  		interval: d,
   200  		c:        make(chan time.Time, 1),
   201  	}
   202  	ft.startTimer(t)
   203  	return &frozenTicker{t}
   204  }
   205  
   206  func (ft *frozenTime) Tick(d time.Duration) <-chan time.Time {
   207  	if d <= 0 {
   208  		return nil
   209  	}
   210  	return ft.NewTicker(d).C()
   211  }
   212  
   213  func (ft *frozenTime) Wait4Scheduled(count int, timeout time.Duration) bool {
   214  	ft.mu.Lock()
   215  	if len(ft.timers) >= count {
   216  		ft.mu.Unlock()
   217  		return true
   218  	}
   219  	if ft.waiter != nil {
   220  		panic("Concurrent call")
   221  	}
   222  	ft.waiter = &waiter{count, make(chan struct{})}
   223  	ft.mu.Unlock()
   224  
   225  	success := false
   226  	select {
   227  	case <-ft.waiter.signalCh:
   228  		success = true
   229  	case <-time.After(timeout):
   230  	}
   231  	ft.mu.Lock()
   232  	ft.waiter = nil
   233  	ft.mu.Unlock()
   234  	return success
   235  }