| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694 |
- // 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
- }
|