mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 00:01:40 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			695 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			695 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) Tailscale Inc & AUTHORS
 | |
| // SPDX-License-Identifier: BSD-3-Clause
 | |
| 
 | |
| package tstest
 | |
| 
 | |
| import (
 | |
| 	"container/heap"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"tailscale.com/tstime"
 | |
| 	"tailscale.com/util/mak"
 | |
| )
 | |
| 
 | |
| // ClockOpts is used to configure the initial settings for a Clock. Once the
 | |
| // settings are configured as desired, call NewClock to get the resulting Clock.
 | |
| type ClockOpts struct {
 | |
| 	// Start is the starting time for the Clock. When FollowRealTime is false,
 | |
| 	// Start is also the value that will be returned by the first call
 | |
| 	// to Clock.Now.
 | |
| 	Start time.Time
 | |
| 	// Step is the amount of time the Clock will advance whenever Clock.Now is
 | |
| 	// called. If set to zero, the Clock will only advance when Clock.Advance is
 | |
| 	// called and/or if FollowRealTime is true.
 | |
| 	//
 | |
| 	// FollowRealTime and Step cannot be enabled at the same time.
 | |
| 	Step time.Duration
 | |
| 
 | |
| 	// TimerChannelSize configures the maximum buffered ticks that are
 | |
| 	// permitted in the channel of any Timer and Ticker created by this Clock.
 | |
| 	// The special value 0 means to use the default of 1. The buffer may need to
 | |
| 	// be increased if time is advanced by more than a single tick and proper
 | |
| 	// functioning of the test requires that the ticks are not lost.
 | |
| 	TimerChannelSize int
 | |
| 
 | |
| 	// FollowRealTime makes the simulated time increment along with real time.
 | |
| 	// It is a compromise between determinism and the difficulty of explicitly
 | |
| 	// managing the simulated time via Step or Clock.Advance. When
 | |
| 	// FollowRealTime is set, calls to Now() and PeekNow() will add the
 | |
| 	// elapsed real-world time to the simulated time.
 | |
| 	//
 | |
| 	// FollowRealTime and Step cannot be enabled at the same time.
 | |
| 	FollowRealTime bool
 | |
| }
 | |
| 
 | |
| // NewClock creates a Clock with the specified settings. To create a
 | |
| // Clock with only the default settings, new(Clock) is equivalent, except that
 | |
| // the start time will not be computed until one of the receivers is called.
 | |
| func NewClock(co ClockOpts) *Clock {
 | |
| 	if co.FollowRealTime && co.Step != 0 {
 | |
| 		panic("only one of FollowRealTime and Step are allowed in NewClock")
 | |
| 	}
 | |
| 
 | |
| 	return newClockInternal(co, nil)
 | |
| }
 | |
| 
 | |
| // newClockInternal creates a Clock with the specified settings and allows
 | |
| // specifying a non-standard realTimeClock.
 | |
| func newClockInternal(co ClockOpts, rtClock tstime.Clock) *Clock {
 | |
| 	if !co.FollowRealTime && rtClock != nil {
 | |
| 		panic("rtClock can only be set with FollowRealTime enabled")
 | |
| 	}
 | |
| 
 | |
| 	if co.FollowRealTime && rtClock == nil {
 | |
| 		rtClock = new(tstime.StdClock)
 | |
| 	}
 | |
| 
 | |
| 	c := &Clock{
 | |
| 		start:            co.Start,
 | |
| 		realTimeClock:    rtClock,
 | |
| 		step:             co.Step,
 | |
| 		timerChannelSize: co.TimerChannelSize,
 | |
| 	}
 | |
| 	c.init() // init now to capture the current time when co.Start.IsZero()
 | |
| 	return c
 | |
| }
 | |
| 
 | |
| // Clock is a testing clock that advances every time its Now method is
 | |
| // called, beginning at its start time. If no start time is specified using
 | |
| // ClockBuilder, an arbitrary start time will be selected when the Clock is
 | |
| // created and can be retrieved by calling Clock.Start().
 | |
| type Clock struct {
 | |
| 	// start is the first value returned by Now. It must not be modified after
 | |
| 	// init is called.
 | |
| 	start time.Time
 | |
| 
 | |
| 	// realTimeClock, if not nil, indicates that the Clock shall move forward
 | |
| 	// according to realTimeClock + the accumulated calls to Advance. This can
 | |
| 	// make writing tests easier that require some control over the clock but do
 | |
| 	// not need exact control over the clock. While step can also be used for
 | |
| 	// this purpose, it is harder to control how quickly time moves using step.
 | |
| 	realTimeClock tstime.Clock
 | |
| 
 | |
| 	initOnce sync.Once
 | |
| 	mu       sync.Mutex
 | |
| 
 | |
| 	// step is how much to advance with each Now call.
 | |
| 	step time.Duration
 | |
| 	// present is the last value returned by Now (and will be returned again by
 | |
| 	// PeekNow).
 | |
| 	present time.Time
 | |
| 	// realTime is the time from realTimeClock corresponding to the current
 | |
| 	// value of present.
 | |
| 	realTime time.Time
 | |
| 	// skipStep indicates that the next call to Now should not add step to
 | |
| 	// present. This occurs after initialization and after Advance.
 | |
| 	skipStep bool
 | |
| 	// timerChannelSize is the buffer size to use for channels created by
 | |
| 	// NewTimer and NewTicker.
 | |
| 	timerChannelSize int
 | |
| 
 | |
| 	events eventManager
 | |
| }
 | |
| 
 | |
| func (c *Clock) init() {
 | |
| 	c.initOnce.Do(func() {
 | |
| 		if c.realTimeClock != nil {
 | |
| 			c.realTime = c.realTimeClock.Now()
 | |
| 		}
 | |
| 		if c.start.IsZero() {
 | |
| 			if c.realTime.IsZero() {
 | |
| 				c.start = time.Now()
 | |
| 			} else {
 | |
| 				c.start = c.realTime
 | |
| 			}
 | |
| 		}
 | |
| 		if c.timerChannelSize == 0 {
 | |
| 			c.timerChannelSize = 1
 | |
| 		}
 | |
| 		c.present = c.start
 | |
| 		c.skipStep = true
 | |
| 		c.events.AdvanceTo(c.present)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| // Now returns the virtual clock's current time, and advances it
 | |
| // according to its step configuration.
 | |
| func (c *Clock) Now() time.Time {
 | |
| 	c.init()
 | |
| 	rt := c.maybeGetRealTime()
 | |
| 
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 
 | |
| 	step := c.step
 | |
| 	if c.skipStep {
 | |
| 		step = 0
 | |
| 		c.skipStep = false
 | |
| 	}
 | |
| 	c.advanceLocked(rt, step)
 | |
| 
 | |
| 	return c.present
 | |
| }
 | |
| 
 | |
| func (c *Clock) maybeGetRealTime() time.Time {
 | |
| 	if c.realTimeClock == nil {
 | |
| 		return time.Time{}
 | |
| 	}
 | |
| 	return c.realTimeClock.Now()
 | |
| }
 | |
| 
 | |
| func (c *Clock) advanceLocked(now time.Time, add time.Duration) {
 | |
| 	if !now.IsZero() {
 | |
| 		add += now.Sub(c.realTime)
 | |
| 		c.realTime = now
 | |
| 	}
 | |
| 	if add == 0 {
 | |
| 		return
 | |
| 	}
 | |
| 	c.present = c.present.Add(add)
 | |
| 	c.events.AdvanceTo(c.present)
 | |
| }
 | |
| 
 | |
| // PeekNow returns the last time reported by Now. If Now has never been called,
 | |
| // PeekNow returns the same value as GetStart.
 | |
| func (c *Clock) PeekNow() time.Time {
 | |
| 	c.init()
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	return c.present
 | |
| }
 | |
| 
 | |
| // Advance moves simulated time forward or backwards by a relative amount. Any
 | |
| // Timer or Ticker that is waiting will fire at the requested point in simulated
 | |
| // time. Advance returns the new simulated time. If this Clock follows real time
 | |
| // then the next call to Now will equal the return value of Advance + the
 | |
| // elapsed time since calling Advance. Otherwise, the next call to Now will
 | |
| // equal the return value of Advance, regardless of the current step.
 | |
| func (c *Clock) Advance(d time.Duration) time.Time {
 | |
| 	c.init()
 | |
| 	rt := c.maybeGetRealTime()
 | |
| 
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	c.skipStep = true
 | |
| 
 | |
| 	c.advanceLocked(rt, d)
 | |
| 	return c.present
 | |
| }
 | |
| 
 | |
| // AdvanceTo moves simulated time to a new absolute value. Any Timer or Ticker
 | |
| // that is waiting will fire at the requested point in simulated time. If this
 | |
| // Clock follows real time then the next call to Now will equal t + the elapsed
 | |
| // time since calling Advance. Otherwise, the next call to Now will equal t,
 | |
| // regardless of the configured step.
 | |
| func (c *Clock) AdvanceTo(t time.Time) {
 | |
| 	c.init()
 | |
| 	rt := c.maybeGetRealTime()
 | |
| 
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	c.skipStep = true
 | |
| 	c.realTime = rt
 | |
| 	c.present = t
 | |
| 	c.events.AdvanceTo(c.present)
 | |
| }
 | |
| 
 | |
| // GetStart returns the initial simulated time when this Clock was created.
 | |
| func (c *Clock) GetStart() time.Time {
 | |
| 	c.init()
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	return c.start
 | |
| }
 | |
| 
 | |
| // GetStep returns the amount that simulated time advances on every call to Now.
 | |
| func (c *Clock) GetStep() time.Duration {
 | |
| 	c.init()
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	return c.step
 | |
| }
 | |
| 
 | |
| // SetStep updates the amount that simulated time advances on every call to Now.
 | |
| func (c *Clock) SetStep(d time.Duration) {
 | |
| 	c.init()
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	c.step = d
 | |
| }
 | |
| 
 | |
| // SetTimerChannelSize changes the channel size for any Timer or Ticker created
 | |
| // in the future. It does not affect those that were already created.
 | |
| func (c *Clock) SetTimerChannelSize(n int) {
 | |
| 	c.init()
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 	c.timerChannelSize = n
 | |
| }
 | |
| 
 | |
| // NewTicker returns a Ticker that uses this Clock for accessing the current
 | |
| // time.
 | |
| func (c *Clock) NewTicker(d time.Duration) (tstime.TickerController, <-chan time.Time) {
 | |
| 	c.init()
 | |
| 	rt := c.maybeGetRealTime()
 | |
| 
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 
 | |
| 	c.advanceLocked(rt, 0)
 | |
| 	t := &Ticker{
 | |
| 		nextTrigger: c.present.Add(d),
 | |
| 		period:      d,
 | |
| 		em:          &c.events,
 | |
| 	}
 | |
| 	t.init(c.timerChannelSize)
 | |
| 	return t, t.C
 | |
| }
 | |
| 
 | |
| // NewTimer returns a Timer that uses this Clock for accessing the current
 | |
| // time.
 | |
| func (c *Clock) NewTimer(d time.Duration) (tstime.TimerController, <-chan time.Time) {
 | |
| 	c.init()
 | |
| 	rt := c.maybeGetRealTime()
 | |
| 
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 
 | |
| 	c.advanceLocked(rt, 0)
 | |
| 	t := &Timer{
 | |
| 		nextTrigger: c.present.Add(d),
 | |
| 		em:          &c.events,
 | |
| 	}
 | |
| 	t.init(c.timerChannelSize, nil)
 | |
| 	return t, t.C
 | |
| }
 | |
| 
 | |
| // AfterFunc returns a Timer that calls f when it fires, using this Clock for
 | |
| // accessing the current time.
 | |
| func (c *Clock) AfterFunc(d time.Duration, f func()) tstime.TimerController {
 | |
| 	c.init()
 | |
| 	rt := c.maybeGetRealTime()
 | |
| 
 | |
| 	c.mu.Lock()
 | |
| 	defer c.mu.Unlock()
 | |
| 
 | |
| 	c.advanceLocked(rt, 0)
 | |
| 	t := &Timer{
 | |
| 		nextTrigger: c.present.Add(d),
 | |
| 		em:          &c.events,
 | |
| 	}
 | |
| 	t.init(c.timerChannelSize, f)
 | |
| 	return t
 | |
| }
 | |
| 
 | |
| // Since subtracts specified duration from Now().
 | |
| func (c *Clock) Since(t time.Time) time.Duration {
 | |
| 	return c.Now().Sub(t)
 | |
| }
 | |
| 
 | |
| // eventHandler offers a common interface for Timer and Ticker events to avoid
 | |
| // code duplication in eventManager.
 | |
| type eventHandler interface {
 | |
| 	// Fire signals the event. The provided time is written to the event's
 | |
| 	// channel as the current time. The return value is the next time this event
 | |
| 	// should fire, otherwise if it is zero then the event will be removed from
 | |
| 	// the eventManager.
 | |
| 	Fire(time.Time) time.Time
 | |
| }
 | |
| 
 | |
| // event tracks details about an upcoming Timer or Ticker firing.
 | |
| type event struct {
 | |
| 	position int       // The current index in the heap, needed for heap.Fix and heap.Remove.
 | |
| 	when     time.Time // A cache of the next time the event triggers to avoid locking issues if we were to get it from eh.
 | |
| 	eh       eventHandler
 | |
| }
 | |
| 
 | |
| // eventManager tracks pending events created by Timer and Ticker. eventManager
 | |
| // implements heap.Interface for efficient lookups of the next event.
 | |
| type eventManager struct {
 | |
| 	// clock is a real time clock for scheduling events with. When clock is nil,
 | |
| 	// events only fire when AdvanceTo is called by the simulated clock that
 | |
| 	// this eventManager belongs to. When clock is not nil, events may fire when
 | |
| 	// timer triggers.
 | |
| 	clock tstime.Clock
 | |
| 
 | |
| 	mu            sync.Mutex
 | |
| 	now           time.Time
 | |
| 	heap          []*event
 | |
| 	reverseLookup map[eventHandler]*event
 | |
| 
 | |
| 	// timer is an AfterFunc that triggers at heap[0].when.Sub(now) relative to
 | |
| 	// the time represented by clock. In other words, if clock is real world
 | |
| 	// time, then if an event is scheduled 1 second into the future in the
 | |
| 	// simulated time, then the event will trigger after 1 second of actual test
 | |
| 	// execution time (unless the test advances simulated time, in which case
 | |
| 	// the timer is updated accordingly). This makes tests easier to write in
 | |
| 	// situations where the simulated time only needs to be partially
 | |
| 	// controlled, and the test writer wishes for simulated time to pass with an
 | |
| 	// offset but still synchronized with the real world.
 | |
| 	//
 | |
| 	// In the future, this could be extended to allow simulated time to run at a
 | |
| 	// multiple of real world time.
 | |
| 	timer tstime.TimerController
 | |
| }
 | |
| 
 | |
| func (em *eventManager) handleTimer() {
 | |
| 	rt := em.clock.Now()
 | |
| 	em.AdvanceTo(rt)
 | |
| }
 | |
| 
 | |
| // Push implements heap.Interface.Push and must only be called by heap funcs
 | |
| // with em.mu already held.
 | |
| func (em *eventManager) Push(x any) {
 | |
| 	e, ok := x.(*event)
 | |
| 	if !ok {
 | |
| 		panic("incorrect event type")
 | |
| 	}
 | |
| 	if e == nil {
 | |
| 		panic("nil event")
 | |
| 	}
 | |
| 
 | |
| 	mak.Set(&em.reverseLookup, e.eh, e)
 | |
| 	e.position = len(em.heap)
 | |
| 	em.heap = append(em.heap, e)
 | |
| }
 | |
| 
 | |
| // Pop implements heap.Interface.Pop and must only be called by heap funcs with
 | |
| // em.mu already held.
 | |
| func (em *eventManager) Pop() any {
 | |
| 	e := em.heap[len(em.heap)-1]
 | |
| 	em.heap = em.heap[:len(em.heap)-1]
 | |
| 	delete(em.reverseLookup, e.eh)
 | |
| 	return e
 | |
| }
 | |
| 
 | |
| // Len implements sort.Interface.Len and must only be called by heap funcs with
 | |
| // em.mu already held.
 | |
| func (em *eventManager) Len() int {
 | |
| 	return len(em.heap)
 | |
| }
 | |
| 
 | |
| // Less implements sort.Interface.Less and must only be called by heap funcs
 | |
| // with em.mu already held.
 | |
| func (em *eventManager) Less(i, j int) bool {
 | |
| 	return em.heap[i].when.Before(em.heap[j].when)
 | |
| }
 | |
| 
 | |
| // Swap implements sort.Interface.Swap and must only be called by heap funcs
 | |
| // with em.mu already held.
 | |
| func (em *eventManager) Swap(i, j int) {
 | |
| 	em.heap[i], em.heap[j] = em.heap[j], em.heap[i]
 | |
| 	em.heap[i].position = i
 | |
| 	em.heap[j].position = j
 | |
| }
 | |
| 
 | |
| // Reschedule adds/updates/deletes an event in the heap, whichever
 | |
| // operation is applicable (use a zero time to delete).
 | |
| func (em *eventManager) Reschedule(eh eventHandler, t time.Time) {
 | |
| 	em.mu.Lock()
 | |
| 	defer em.mu.Unlock()
 | |
| 	defer em.updateTimerLocked()
 | |
| 
 | |
| 	e, ok := em.reverseLookup[eh]
 | |
| 	if !ok {
 | |
| 		if t.IsZero() {
 | |
| 			// eh is not scheduled and also not active, so do nothing.
 | |
| 			return
 | |
| 		}
 | |
| 		// eh is not scheduled but is active, so add it.
 | |
| 		heap.Push(em, &event{
 | |
| 			when: t,
 | |
| 			eh:   eh,
 | |
| 		})
 | |
| 		em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now).
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if t.IsZero() {
 | |
| 		// e is scheduled but not active, so remove it.
 | |
| 		heap.Remove(em, e.position)
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// e is scheduled and active, so update it.
 | |
| 	e.when = t
 | |
| 	heap.Fix(em, e.position)
 | |
| 	em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now).
 | |
| }
 | |
| 
 | |
| // AdvanceTo updates the current time to tm and fires all events scheduled
 | |
| // before or equal to tm. When an event fires, it may request rescheduling and
 | |
| // the rescheduled events will be combined with the other existing events that
 | |
| // are waiting, and will be run in the unified ordering. A poorly behaved event
 | |
| // may theoretically prevent this from ever completing, but both Timer and
 | |
| // Ticker require positive steps into the future.
 | |
| func (em *eventManager) AdvanceTo(tm time.Time) {
 | |
| 	em.mu.Lock()
 | |
| 	defer em.mu.Unlock()
 | |
| 	defer em.updateTimerLocked()
 | |
| 
 | |
| 	em.processEventsLocked(tm)
 | |
| 	em.now = tm
 | |
| }
 | |
| 
 | |
| // Now returns the cached current time. It is intended for use by a Timer or
 | |
| // Ticker that needs to convert a relative time to an absolute time.
 | |
| func (em *eventManager) Now() time.Time {
 | |
| 	em.mu.Lock()
 | |
| 	defer em.mu.Unlock()
 | |
| 	return em.now
 | |
| }
 | |
| 
 | |
| func (em *eventManager) processEventsLocked(tm time.Time) {
 | |
| 	for len(em.heap) > 0 && !em.heap[0].when.After(tm) {
 | |
| 		// Ideally some jitter would be added here but it's difficult to do so
 | |
| 		// in a deterministic fashion.
 | |
| 		em.now = em.heap[0].when
 | |
| 
 | |
| 		if nextFire := em.heap[0].eh.Fire(em.now); !nextFire.IsZero() {
 | |
| 			em.heap[0].when = nextFire
 | |
| 			heap.Fix(em, 0)
 | |
| 		} else {
 | |
| 			heap.Pop(em)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (em *eventManager) updateTimerLocked() {
 | |
| 	if em.clock == nil {
 | |
| 		return
 | |
| 	}
 | |
| 	if len(em.heap) == 0 {
 | |
| 		if em.timer != nil {
 | |
| 			em.timer.Stop()
 | |
| 		}
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	timeToEvent := em.heap[0].when.Sub(em.now)
 | |
| 	if em.timer == nil {
 | |
| 		em.timer = em.clock.AfterFunc(timeToEvent, em.handleTimer)
 | |
| 		return
 | |
| 	}
 | |
| 	em.timer.Reset(timeToEvent)
 | |
| }
 | |
| 
 | |
| // Ticker is a time.Ticker lookalike for use in tests that need to control when
 | |
| // events fire. Ticker could be made standalone in future but for now is
 | |
| // expected to be paired with a Clock and created by Clock.NewTicker.
 | |
| type Ticker struct {
 | |
| 	C <-chan time.Time // The channel on which ticks are delivered.
 | |
| 
 | |
| 	// em is the eventManager to be notified when nextTrigger changes.
 | |
| 	// eventManager has its own mutex, and the pointer is immutable, therefore
 | |
| 	// em can be accessed without holding mu.
 | |
| 	em *eventManager
 | |
| 
 | |
| 	c chan<- time.Time // The writer side of C.
 | |
| 
 | |
| 	mu sync.Mutex
 | |
| 
 | |
| 	// nextTrigger is the time of the ticker's next scheduled activation. When
 | |
| 	// Fire activates the ticker, nextTrigger is the timestamp written to the
 | |
| 	// channel.
 | |
| 	nextTrigger time.Time
 | |
| 
 | |
| 	// period is the duration that is added to nextTrigger when the ticker
 | |
| 	// fires.
 | |
| 	period time.Duration
 | |
| }
 | |
| 
 | |
| func (t *Ticker) init(channelSize int) {
 | |
| 	if channelSize <= 0 {
 | |
| 		panic("ticker channel size must be non-negative")
 | |
| 	}
 | |
| 	c := make(chan time.Time, channelSize)
 | |
| 	t.c = c
 | |
| 	t.C = c
 | |
| 	t.em.Reschedule(t, t.nextTrigger)
 | |
| }
 | |
| 
 | |
| // Fire triggers the ticker. curTime is the timestamp to write to the channel.
 | |
| // The next trigger time for the ticker is updated to the last computed trigger
 | |
| // time + the ticker period (set at creation or using Reset). The next trigger
 | |
| // time is computed this way to match standard time.Ticker behavior, which
 | |
| // prevents accumulation of long term drift caused by delays in event execution.
 | |
| func (t *Ticker) Fire(curTime time.Time) time.Time {
 | |
| 	t.mu.Lock()
 | |
| 	defer t.mu.Unlock()
 | |
| 
 | |
| 	if t.nextTrigger.IsZero() {
 | |
| 		return time.Time{}
 | |
| 	}
 | |
| 	select {
 | |
| 	case t.c <- curTime:
 | |
| 	default:
 | |
| 	}
 | |
| 	t.nextTrigger = t.nextTrigger.Add(t.period)
 | |
| 
 | |
| 	return t.nextTrigger
 | |
| }
 | |
| 
 | |
| // Reset adjusts the Ticker's period to d and reschedules the next fire time to
 | |
| // the current simulated time + d.
 | |
| func (t *Ticker) Reset(d time.Duration) {
 | |
| 	if d <= 0 {
 | |
| 		// The standard time.Ticker requires a positive period.
 | |
| 		panic("non-positive period for Ticker.Reset")
 | |
| 	}
 | |
| 
 | |
| 	now := t.em.Now()
 | |
| 
 | |
| 	t.mu.Lock()
 | |
| 	t.resetLocked(now.Add(d), d)
 | |
| 	t.mu.Unlock()
 | |
| 
 | |
| 	t.em.Reschedule(t, t.nextTrigger)
 | |
| }
 | |
| 
 | |
| // ResetAbsolute adjusts the Ticker's period to d and reschedules the next fire
 | |
| // time to nextTrigger.
 | |
| func (t *Ticker) ResetAbsolute(nextTrigger time.Time, d time.Duration) {
 | |
| 	if nextTrigger.IsZero() {
 | |
| 		panic("zero nextTrigger time for ResetAbsolute")
 | |
| 	}
 | |
| 	if d <= 0 {
 | |
| 		panic("non-positive period for ResetAbsolute")
 | |
| 	}
 | |
| 
 | |
| 	t.mu.Lock()
 | |
| 	t.resetLocked(nextTrigger, d)
 | |
| 	t.mu.Unlock()
 | |
| 
 | |
| 	t.em.Reschedule(t, t.nextTrigger)
 | |
| }
 | |
| 
 | |
| func (t *Ticker) resetLocked(nextTrigger time.Time, d time.Duration) {
 | |
| 	t.nextTrigger = nextTrigger
 | |
| 	t.period = d
 | |
| }
 | |
| 
 | |
| // Stop deactivates the Ticker.
 | |
| func (t *Ticker) Stop() {
 | |
| 	t.mu.Lock()
 | |
| 	t.nextTrigger = time.Time{}
 | |
| 	t.mu.Unlock()
 | |
| 
 | |
| 	t.em.Reschedule(t, t.nextTrigger)
 | |
| }
 | |
| 
 | |
| // Timer is a time.Timer lookalike for use in tests that need to control when
 | |
| // events fire. Timer could be made standalone in future but for now must be
 | |
| // paired with a Clock and created by Clock.NewTimer.
 | |
| type Timer struct {
 | |
| 	C <-chan time.Time // The channel on which ticks are delivered.
 | |
| 
 | |
| 	// em is the eventManager to be notified when nextTrigger changes.
 | |
| 	// eventManager has its own mutex, and the pointer is immutable, therefore
 | |
| 	// em can be accessed without holding mu.
 | |
| 	em *eventManager
 | |
| 
 | |
| 	f func(time.Time) // The function to call when the timer expires.
 | |
| 
 | |
| 	mu sync.Mutex
 | |
| 
 | |
| 	// nextTrigger is the time of the ticker's next scheduled activation. When
 | |
| 	// Fire activates the ticker, nextTrigger is the timestamp written to the
 | |
| 	// channel.
 | |
| 	nextTrigger time.Time
 | |
| }
 | |
| 
 | |
| func (t *Timer) init(channelSize int, afterFunc func()) {
 | |
| 	if channelSize <= 0 {
 | |
| 		panic("ticker channel size must be non-negative")
 | |
| 	}
 | |
| 	c := make(chan time.Time, channelSize)
 | |
| 	t.C = c
 | |
| 	if afterFunc == nil {
 | |
| 		t.f = func(curTime time.Time) {
 | |
| 			select {
 | |
| 			case c <- curTime:
 | |
| 			default:
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		t.f = func(_ time.Time) { afterFunc() }
 | |
| 	}
 | |
| 	t.em.Reschedule(t, t.nextTrigger)
 | |
| }
 | |
| 
 | |
| // Fire triggers the ticker. curTime is the timestamp to write to the channel.
 | |
| // The next trigger time for the ticker is updated to the last computed trigger
 | |
| // time + the ticker period (set at creation or using Reset). The next trigger
 | |
| // time is computed this way to match standard time.Ticker behavior, which
 | |
| // prevents accumulation of long term drift caused by delays in event execution.
 | |
| func (t *Timer) Fire(curTime time.Time) time.Time {
 | |
| 	t.mu.Lock()
 | |
| 	defer t.mu.Unlock()
 | |
| 
 | |
| 	if t.nextTrigger.IsZero() {
 | |
| 		return time.Time{}
 | |
| 	}
 | |
| 	t.nextTrigger = time.Time{}
 | |
| 	t.f(curTime)
 | |
| 	return time.Time{}
 | |
| }
 | |
| 
 | |
| // Reset reschedules the next fire time to the current simulated time + d.
 | |
| // Reset reports whether the timer was still active before the reset.
 | |
| func (t *Timer) Reset(d time.Duration) bool {
 | |
| 	if d <= 0 {
 | |
| 		// The standard time.Timer requires a positive delay.
 | |
| 		panic("non-positive delay for Timer.Reset")
 | |
| 	}
 | |
| 
 | |
| 	return t.reset(t.em.Now().Add(d))
 | |
| }
 | |
| 
 | |
| // ResetAbsolute reschedules the next fire time to nextTrigger.
 | |
| // ResetAbsolute reports whether the timer was still active before the reset.
 | |
| func (t *Timer) ResetAbsolute(nextTrigger time.Time) bool {
 | |
| 	if nextTrigger.IsZero() {
 | |
| 		panic("zero nextTrigger time for ResetAbsolute")
 | |
| 	}
 | |
| 
 | |
| 	return t.reset(nextTrigger)
 | |
| }
 | |
| 
 | |
| // Stop deactivates the Timer. Stop reports whether the timer was active before
 | |
| // stopping.
 | |
| func (t *Timer) Stop() bool {
 | |
| 	return t.reset(time.Time{})
 | |
| }
 | |
| 
 | |
| func (t *Timer) reset(nextTrigger time.Time) bool {
 | |
| 	t.mu.Lock()
 | |
| 	wasActive := !t.nextTrigger.IsZero()
 | |
| 	t.nextTrigger = nextTrigger
 | |
| 	t.mu.Unlock()
 | |
| 
 | |
| 	t.em.Reschedule(t, t.nextTrigger)
 | |
| 	return wasActive
 | |
| }
 |