go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/clock/testclock/fastclock.go (about)

     1  // Copyright 2021 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package testclock
    16  
    17  import (
    18  	"container/heap"
    19  	"context"
    20  	"fmt"
    21  	"sync"
    22  	"time"
    23  
    24  	"go.chromium.org/luci/common/clock"
    25  )
    26  
    27  // FastClock mimics faster physical clock in tests.
    28  //
    29  // Its API is exactly the same as that of TestClock and can be used in-place.
    30  // However, unlike TestClock, the time in this clock moves forward as
    31  // a normal wall clock would, but at an arbitrarily faster rate.
    32  //
    33  // It's useful for integration tests simulating a large system over long period
    34  // of (wall clock) time where adjusting each indiviudal timeout/delay/timer via
    35  // testclock API isn't feasible.
    36  type FastClock struct {
    37  	mutex          sync.Mutex
    38  	initSysTime    time.Time
    39  	initFastTime   time.Time
    40  	fastToSysRatio int
    41  	pendingTimers  pendingTimerHeap
    42  	timerCallback  TimerCallback
    43  	// pendingTimersChanged pipes "true" to the worker
    44  	// whenever pending timers change.
    45  	//
    46  	// In Close(), this channel is closed and thus pipes "false" indefinitely.
    47  	pendingTimersChanged chan bool
    48  	systemNow            func() time.Time // mocked in tests.
    49  }
    50  
    51  // NewFastClock creates a new FastClock running faster than a system clock.
    52  //
    53  // You SHOULD call .Close() on the returned object after use to avoid leaks.
    54  func NewFastClock(now time.Time, ratio int) *FastClock {
    55  	f := &FastClock{
    56  		initFastTime:         now,
    57  		initSysTime:          time.Now(),
    58  		fastToSysRatio:       ratio,
    59  		pendingTimersChanged: make(chan bool, 1), // holds at most one "poke"
    60  		systemNow:            time.Now,
    61  	}
    62  	go f.worker()
    63  	return f
    64  }
    65  
    66  // onTimersChangedLocked wakes up the worker() func if it hasn't been poked
    67  // already.
    68  func (f *FastClock) onTimersChangedLocked() {
    69  	select {
    70  	case f.pendingTimersChanged <- true:
    71  	default:
    72  		// Already notified.
    73  	}
    74  }
    75  
    76  // periodicTimerNotify follows system (wall) clock and wakes us timers as
    77  // necessary.
    78  func (f *FastClock) worker() {
    79  	const maxSysWait = time.Hour
    80  	// Create system timer to wait on.
    81  	sysTimer := time.NewTimer(maxSysWait)
    82  	// Make the timer ready for Reset.
    83  	if !sysTimer.Stop() {
    84  		<-sysTimer.C
    85  	}
    86  
    87  	notifyAndResetSysTimer := func() {
    88  		f.mutex.Lock()
    89  		defer f.mutex.Unlock()
    90  		fNow := f.Now()
    91  		triggerTimersLocked(fNow, &f.pendingTimers)
    92  		wait := maxSysWait
    93  		if len(f.pendingTimers) > 0 {
    94  			// Due to triggerTimersLocked() before, `wait` must be >0.
    95  			wait = f.pendingTimers[0].deadline.Sub(fNow) / time.Duration(f.fastToSysRatio)
    96  		}
    97  		sysTimer.Reset(wait)
    98  	}
    99  
   100  	for {
   101  		notifyAndResetSysTimer()
   102  		select {
   103  		case <-sysTimer.C:
   104  		case changed := <-f.pendingTimersChanged:
   105  			if !sysTimer.Stop() {
   106  				<-sysTimer.C
   107  			}
   108  			if !changed {
   109  				// The pendingTimersChanged channel was closed by Close().
   110  				return
   111  			}
   112  		}
   113  	}
   114  }
   115  
   116  // Close frees clock resources.
   117  func (f *FastClock) Close() {
   118  	close(f.pendingTimersChanged)
   119  }
   120  
   121  // Now returns the current time (see time.Now).
   122  func (f *FastClock) Now() time.Time {
   123  	_, fNow := f.now()
   124  	return fNow
   125  }
   126  
   127  // now returns system (wall) clock time and this clock's time.
   128  func (f *FastClock) now() (time.Time, time.Time) {
   129  	sNow := f.systemNow()
   130  	fNow := f.initFastTime.Add(sNow.Sub(f.initSysTime) * time.Duration(f.fastToSysRatio))
   131  	return sNow, fNow
   132  }
   133  
   134  // Sleep sleeps the current goroutine (see time.Sleep).
   135  //
   136  // Sleep will return a TimerResult containing the time when it was awakened
   137  // and detailing its execution. If the sleep terminated prematurely from
   138  // cancellation, the TimerResult's Incomplete() method will return true.
   139  func (f *FastClock) Sleep(ctx context.Context, d time.Duration) clock.TimerResult {
   140  	t := f.NewTimer(ctx)
   141  	t.Reset(d)
   142  	return <-t.GetC()
   143  }
   144  
   145  // NewTimer creates a new Timer instance, bound to this Clock.
   146  //
   147  // If the supplied Context is canceled, the timer will expire immediately.
   148  func (f *FastClock) NewTimer(ctx context.Context) clock.Timer {
   149  	return newTimer(ctx, f)
   150  }
   151  
   152  // Set sets the test clock's time to at least the given time.
   153  //
   154  // Noop if Now() is already after the given time.
   155  func (f *FastClock) Set(fNew time.Time) {
   156  	f.mutex.Lock()
   157  	defer f.mutex.Unlock()
   158  
   159  	sNow, fBefore := f.now()
   160  	if fBefore.After(fNew) {
   161  		// fNew is already in the past.
   162  		return
   163  	}
   164  	f.initSysTime = sNow
   165  	f.initFastTime = fNew
   166  
   167  	triggerTimersLocked(fNew, &f.pendingTimers)
   168  	f.onTimersChangedLocked()
   169  }
   170  
   171  // Add advances the test clock's time.
   172  func (f *FastClock) Add(d time.Duration) {
   173  	if d < 0 {
   174  		panic(fmt.Errorf("cannot go backwards in time. You're not Doc Brown.\nDelta: %s", d))
   175  	}
   176  
   177  	f.mutex.Lock()
   178  	defer f.mutex.Unlock()
   179  
   180  	sNow, fBefore := f.now()
   181  	f.initSysTime = sNow
   182  	f.initFastTime = fBefore.Add(d)
   183  	triggerTimersLocked(f.initFastTime, &f.pendingTimers)
   184  	f.onTimersChangedLocked()
   185  }
   186  
   187  // SetTimerCallback is a goroutine-safe method to set an instance-wide
   188  // callback that is invoked when any timer begins.
   189  func (f *FastClock) SetTimerCallback(clbk TimerCallback) {
   190  	f.mutex.Lock()
   191  	f.timerCallback = clbk
   192  	f.mutex.Unlock()
   193  }
   194  
   195  func (f *FastClock) addPendingTimer(t *timer, d time.Duration, triggerC chan<- time.Time) {
   196  	deadline := f.Now().Add(d)
   197  	if callback := f.timerCallback; callback != nil {
   198  		callback(d, t)
   199  	}
   200  
   201  	f.mutex.Lock()
   202  	defer f.mutex.Unlock()
   203  
   204  	heap.Push(&f.pendingTimers, &pendingTimer{
   205  		timer:    t,
   206  		deadline: deadline,
   207  		triggerC: triggerC,
   208  	})
   209  	_, now := f.now()
   210  	triggerTimersLocked(now, &f.pendingTimers)
   211  	f.onTimersChangedLocked()
   212  }
   213  
   214  func (f *FastClock) clearPendingTimer(t *timer) {
   215  	f.mutex.Lock()
   216  	defer f.mutex.Unlock()
   217  
   218  	for i := 0; i < len(f.pendingTimers); {
   219  		if e := f.pendingTimers[0]; e.timer == t {
   220  			heap.Remove(&f.pendingTimers, i)
   221  			close(e.triggerC)
   222  		} else {
   223  			i++
   224  		}
   225  	}
   226  	f.onTimersChangedLocked()
   227  }