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

     1  // Copyright 2015 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  	"context"
    19  	"time"
    20  
    21  	"go.chromium.org/luci/common/clock"
    22  )
    23  
    24  // timerClock encapsulates clock implementation for use by the `timer` type.
    25  type timerClock interface {
    26  	Now() time.Time
    27  	addPendingTimer(t *timer, d time.Duration, triggerC chan<- time.Time)
    28  	clearPendingTimer(t *timer)
    29  }
    30  
    31  // timer is an implementation of clock.TestTimer that uses a channel
    32  // to signal the timer to fire.
    33  //
    34  // The channel is buffered so it can be used without requiring a separate
    35  // signalling goroutine.
    36  type timer struct {
    37  	ctx   context.Context
    38  	clock timerClock
    39  
    40  	// tags is the set of tags in the Context when this timer was created.
    41  	tags []string
    42  
    43  	// afterC will have the TimerResult of the timer's expiration written to it
    44  	// when this timer triggers or is canceled.
    45  	afterC chan clock.TimerResult
    46  
    47  	// monitorFinishedC is used by our timer monitor to signal that the timer has
    48  	// stopped.
    49  	monitorFinishedC chan bool
    50  }
    51  
    52  var _ clock.Timer = (*timer)(nil)
    53  
    54  // NewTimer returns a new, instantiated timer.
    55  func newTimer(ctx context.Context, clk timerClock) *timer {
    56  	return &timer{
    57  		ctx:    ctx,
    58  		clock:  clk,
    59  		tags:   clock.Tags(ctx),
    60  		afterC: make(chan clock.TimerResult, 1),
    61  	}
    62  }
    63  
    64  func (t *timer) GetC() <-chan clock.TimerResult {
    65  	return t.afterC
    66  }
    67  
    68  func (t *timer) Reset(d time.Duration) (active bool) {
    69  	if d < 0 {
    70  		d = 0
    71  	}
    72  
    73  	// Stop our current polling goroutine, if it's running.
    74  	active = t.Stop()
    75  
    76  	// Add ourself to our Clock's scheduler.
    77  	triggerC := make(chan time.Time, 1)
    78  	t.clock.addPendingTimer(t, d, triggerC)
    79  
    80  	// Start a timer monitor goroutine.
    81  	t.monitorFinishedC = make(chan bool, 1)
    82  	go t.monitor(triggerC, t.monitorFinishedC)
    83  	return
    84  }
    85  
    86  func (t *timer) Stop() (stopped bool) {
    87  	// If the timer is not running, we're done.
    88  	if t.monitorFinishedC == nil {
    89  		return false
    90  	}
    91  
    92  	// Shutdown and wait for our monitoring goroutine.
    93  	t.clock.clearPendingTimer(t)
    94  	stopped = <-t.monitorFinishedC
    95  
    96  	// Clear state.
    97  	t.monitorFinishedC = nil
    98  	return
    99  }
   100  
   101  // GetTags returns the tags associated with the specified timer. If the timer
   102  // has no tags, an empty slice (nil) will be returned.
   103  func GetTags(t clock.Timer) []string {
   104  	if tt, ok := t.(*timer); ok {
   105  		return clock.Tags(tt.ctx)
   106  	}
   107  	return nil
   108  }
   109  
   110  // HasTags tests if a given timer has the same tags.
   111  func HasTags(t clock.Timer, first string, tags ...string) bool {
   112  	timerTags := GetTags(t)
   113  	if len(timerTags) == 0 {
   114  		return false
   115  	}
   116  
   117  	if first != timerTags[0] {
   118  		return false
   119  	}
   120  
   121  	if len(timerTags) > 1 {
   122  		// Compare the remainder.
   123  		timerTags = timerTags[1:]
   124  		if len(timerTags) != len(tags) {
   125  			return false
   126  		}
   127  
   128  		for i, tag := range timerTags {
   129  			if tags[i] != tag {
   130  				return false
   131  			}
   132  		}
   133  	}
   134  	return true
   135  }
   136  
   137  // monitor is run in a goroutine, monitoring the timer Context for
   138  // cancellation and forcing a premature timer completion if the monitor
   139  // was canceled.
   140  func (t *timer) monitor(triggerC chan time.Time, finishedC chan bool) {
   141  	monitorRunning := false
   142  	defer func() {
   143  		finishedC <- monitorRunning
   144  		close(finishedC)
   145  	}()
   146  
   147  	select {
   148  	case now, ok := <-triggerC:
   149  		if !ok {
   150  			// Our monitor goroutine has been reaped, probably because we were
   151  			// stopped.
   152  			monitorRunning = true
   153  			break
   154  		}
   155  
   156  		// The Clock has signalled that this timer has expired.
   157  		t.afterC <- clock.TimerResult{
   158  			Time: now,
   159  			Err:  nil,
   160  		}
   161  
   162  	case <-t.ctx.Done():
   163  		// Our Context has been canceled.
   164  		t.clock.clearPendingTimer(t)
   165  		t.afterC <- clock.TimerResult{
   166  			Time: t.clock.Now(),
   167  			Err:  t.ctx.Err(),
   168  		}
   169  	}
   170  }