github.com/juju/clock@v1.0.3/testclock/dilated.go (about)

     1  // Copyright 2022 Canonical Ltd.
     2  // Licensed under the LGPLv3, see LICENCE file for details.
     3  
     4  package testclock
     5  
     6  import (
     7  	"sync"
     8  	"sync/atomic"
     9  	"time"
    10  
    11  	"github.com/juju/clock"
    12  )
    13  
    14  // NewDilatedWallClock returns a clock that can be sped up or slowed down.
    15  // realSecondDuration is the real duration of a second.
    16  func NewDilatedWallClock(realSecondDuration time.Duration) AdvanceableClock {
    17  	dc := &dilationClock{
    18  		epoch:              time.Now(),
    19  		realSecondDuration: realSecondDuration,
    20  		offsetChanged:      make(chan any),
    21  	}
    22  	dc.offsetChangedCond = sync.NewCond(dc.offsetChangedMutex.RLocker())
    23  	return dc
    24  }
    25  
    26  type dilationClock struct {
    27  	epoch              time.Time
    28  	realSecondDuration time.Duration
    29  
    30  	// offsetAtomic is the current dilated offset to allow for time jumps/advances.
    31  	offsetAtomic int64
    32  	// offsetChanged is a channel that is closed when timers need to be signaled
    33  	// that there is a offset change coming.
    34  	offsetChanged chan any
    35  	// offsetChangedMutex is a mutex protecting the offsetChanged and is used by
    36  	// the offsetChangedCond.
    37  	offsetChangedMutex sync.RWMutex
    38  	// offsetChangedCond is used to signal timers that they may try to pull the new
    39  	// offset.
    40  	offsetChangedCond *sync.Cond
    41  }
    42  
    43  // Now is part of the Clock interface.
    44  func (dc *dilationClock) Now() time.Time {
    45  	dt, _ := dc.nowWithOffset()
    46  	return dt
    47  }
    48  
    49  func (dc *dilationClock) nowWithOffset() (time.Time, time.Duration) {
    50  	offset := time.Duration(atomic.LoadInt64(&dc.offsetAtomic))
    51  	realNow := time.Now()
    52  	dt := dilateTime(dc.epoch, realNow, dc.realSecondDuration, offset)
    53  	return dt, offset
    54  }
    55  
    56  // After implements Clock.After
    57  func (dc *dilationClock) After(d time.Duration) <-chan time.Time {
    58  	t := newDilatedWallTimer(dc, d, nil)
    59  	return t.c
    60  }
    61  
    62  // AfterFunc implements Clock.AfterFunc
    63  func (dc *dilationClock) AfterFunc(d time.Duration, f func()) clock.Timer {
    64  	return newDilatedWallTimer(dc, d, f)
    65  }
    66  
    67  // NewTimer implements Clock.NewTimer
    68  func (dc *dilationClock) NewTimer(d time.Duration) clock.Timer {
    69  	return newDilatedWallTimer(dc, d, nil)
    70  }
    71  
    72  // Advance implements AdvanceableClock.Advance
    73  func (dc *dilationClock) Advance(d time.Duration) {
    74  	close(dc.offsetChanged)
    75  	dc.offsetChangedMutex.Lock()
    76  	dc.offsetChanged = make(chan any)
    77  	atomic.AddInt64(&dc.offsetAtomic, int64(d))
    78  	dc.offsetChangedCond.Broadcast()
    79  	dc.offsetChangedMutex.Unlock()
    80  }
    81  
    82  // dilatedWallTimer implements the Timer interface.
    83  type dilatedWallTimer struct {
    84  	timer      *time.Timer
    85  	dc         *dilationClock
    86  	c          chan time.Time
    87  	target     time.Time
    88  	offset     time.Duration
    89  	after      func()
    90  	done       chan any
    91  	resetChan  chan resetReq
    92  	resetMutex sync.Mutex
    93  	stopChan   chan chan bool
    94  }
    95  
    96  type resetReq struct {
    97  	d time.Duration
    98  	r chan bool
    99  }
   100  
   101  func newDilatedWallTimer(dc *dilationClock, d time.Duration, after func()) *dilatedWallTimer {
   102  	t := &dilatedWallTimer{
   103  		dc:        dc,
   104  		c:         make(chan time.Time),
   105  		resetChan: make(chan resetReq),
   106  		stopChan:  make(chan chan bool),
   107  	}
   108  	t.start(d, after)
   109  	return t
   110  }
   111  
   112  func (t *dilatedWallTimer) start(d time.Duration, after func()) {
   113  	t.dc.offsetChangedMutex.RLock()
   114  	dialatedNow, offset := t.dc.nowWithOffset()
   115  	realDuration := time.Duration(float64(d) * t.dc.realSecondDuration.Seconds())
   116  	t.target = dialatedNow.Add(d)
   117  	t.timer = time.NewTimer(realDuration)
   118  	t.offset = offset
   119  	t.after = after
   120  	t.done = make(chan any)
   121  	go t.run()
   122  }
   123  
   124  func (t *dilatedWallTimer) run() {
   125  	defer t.dc.offsetChangedMutex.RUnlock()
   126  	defer close(t.done)
   127  	var sendChan chan time.Time
   128  	var sendTime time.Time
   129  	for {
   130  		select {
   131  		case reset := <-t.resetChan:
   132  			realNow := time.Now()
   133  			dialatedNow := dilateTime(t.dc.epoch, realNow, t.dc.realSecondDuration, t.offset)
   134  			realDuration := time.Duration(float64(reset.d) * t.dc.realSecondDuration.Seconds())
   135  			t.target = dialatedNow.Add(reset.d)
   136  			sendChan = nil
   137  			sendTime = time.Time{}
   138  			reset.r <- t.timer.Reset(realDuration)
   139  		case stop := <-t.stopChan:
   140  			stop <- t.timer.Stop()
   141  			return
   142  		case tt := <-t.timer.C:
   143  			if t.after != nil {
   144  				t.after()
   145  				return
   146  			}
   147  			if sendChan != nil {
   148  				panic("reset should have been called")
   149  			}
   150  			sendChan = t.c
   151  			sendTime = dilateTime(t.dc.epoch, tt, t.dc.realSecondDuration, t.offset)
   152  		case sendChan <- sendTime:
   153  			sendChan = nil
   154  			sendTime = time.Time{}
   155  			return
   156  		case <-t.dc.offsetChanged:
   157  			t.dc.offsetChangedCond.Wait()
   158  			newOffset := time.Duration(atomic.LoadInt64(&t.dc.offsetAtomic))
   159  			if newOffset == t.offset {
   160  				continue
   161  			}
   162  			t.offset = newOffset
   163  			stopped := t.timer.Stop()
   164  			if !stopped {
   165  				continue
   166  			}
   167  			realNow := time.Now()
   168  			dialatedNow := dilateTime(t.dc.epoch, realNow, t.dc.realSecondDuration, t.offset)
   169  			dialatedDuration := t.target.Sub(dialatedNow)
   170  			if dialatedDuration <= 0 {
   171  				sendChan = t.c
   172  				sendTime = dialatedNow
   173  				continue
   174  			}
   175  			realDuration := time.Duration(float64(dialatedDuration) * t.dc.realSecondDuration.Seconds())
   176  			t.timer.Reset(realDuration)
   177  		}
   178  	}
   179  }
   180  
   181  // Chan implements Timer.Chan
   182  func (t *dilatedWallTimer) Chan() <-chan time.Time {
   183  	return t.c
   184  }
   185  
   186  // Chan implements Timer.Reset
   187  func (t *dilatedWallTimer) Reset(d time.Duration) bool {
   188  	t.resetMutex.Lock()
   189  	defer t.resetMutex.Unlock()
   190  	reset := resetReq{
   191  		d: d,
   192  		r: make(chan bool),
   193  	}
   194  	select {
   195  	case <-t.done:
   196  		t.start(d, nil)
   197  		return true
   198  	case t.resetChan <- reset:
   199  		return <-reset.r
   200  	}
   201  }
   202  
   203  // Chan implements Timer.Stop
   204  func (t *dilatedWallTimer) Stop() bool {
   205  	stop := make(chan bool)
   206  	select {
   207  	case <-t.done:
   208  		return false
   209  	case t.stopChan <- stop:
   210  		return <-stop
   211  	}
   212  }
   213  
   214  func dilateTime(epoch, realNow time.Time,
   215  	realSecondDuration, dilatedOffset time.Duration) time.Time {
   216  	delta := realNow.Sub(epoch)
   217  	if delta < 0 {
   218  		delta = time.Duration(0)
   219  	}
   220  	return epoch.Add(dilatedOffset).Add(time.Duration(float64(delta) / realSecondDuration.Seconds()))
   221  }