inet.af/netstack@v0.0.0-20220214151720-7585b01ddccf/tcpip/timer.go (about)

     1  // Copyright 2020 The gVisor 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 tcpip
    16  
    17  import (
    18  	"time"
    19  
    20  	"inet.af/netstack/sync"
    21  )
    22  
    23  // jobInstance is a specific instance of Job.
    24  //
    25  // Different instances are created each time Job is scheduled so each timer has
    26  // its own earlyReturn signal. This is to address a bug when a Job is stopped
    27  // and reset in quick succession resulting in a timer instance's earlyReturn
    28  // signal being affected or seen by another timer instance.
    29  //
    30  // Consider the following sceneario where timer instances share a common
    31  // earlyReturn signal (T1 creates, stops and resets a Cancellable timer under a
    32  // lock L; T2, T3, T4 and T5 are goroutines that handle the first (A), second
    33  // (B), third (C), and fourth (D) instance of the timer firing, respectively):
    34  //   T1: Obtain L
    35  //   T1: Create a new Job w/ lock L (create instance A)
    36  //   T2: instance A fires, blocked trying to obtain L.
    37  //   T1: Attempt to stop instance A (set earlyReturn = true)
    38  //   T1: Schedule timer (create instance B)
    39  //   T3: instance B fires, blocked trying to obtain L.
    40  //   T1: Attempt to stop instance B (set earlyReturn = true)
    41  //   T1: Schedule timer (create instance C)
    42  //   T4: instance C fires, blocked trying to obtain L.
    43  //   T1: Attempt to stop instance C (set earlyReturn = true)
    44  //   T1: Schedule timer (create instance D)
    45  //   T5: instance D fires, blocked trying to obtain L.
    46  //   T1: Release L
    47  //
    48  // Now that T1 has released L, any of the 4 timer instances can take L and
    49  // check earlyReturn. If the timers simply check earlyReturn and then do
    50  // nothing further, then instance D will never early return even though it was
    51  // not requested to stop. If the timers reset earlyReturn before early
    52  // returning, then all but one of the timers will do work when only one was
    53  // expected to. If Job resets earlyReturn when resetting, then all the timers
    54  // will fire (again, when only one was expected to).
    55  //
    56  // To address the above concerns the simplest solution was to give each timer
    57  // its own earlyReturn signal.
    58  type jobInstance struct {
    59  	timer Timer
    60  
    61  	// Used to inform the timer to early return when it gets stopped while the
    62  	// lock the timer tries to obtain when fired is held (T1 is a goroutine that
    63  	// tries to cancel the timer and T2 is the goroutine that handles the timer
    64  	// firing):
    65  	//   T1: Obtain the lock, then call Cancel()
    66  	//   T2: timer fires, and gets blocked on obtaining the lock
    67  	//   T1: Releases lock
    68  	//   T2: Obtains lock does unintended work
    69  	//
    70  	// To resolve this, T1 will check to see if the timer already fired, and
    71  	// inform the timer using earlyReturn to return early so that once T2 obtains
    72  	// the lock, it will see that it is set to true and do nothing further.
    73  	earlyReturn *bool
    74  }
    75  
    76  // stop stops the job instance j from firing if it hasn't fired already. If it
    77  // has fired and is blocked at obtaining the lock, earlyReturn will be set to
    78  // true so that it will early return when it obtains the lock.
    79  func (j *jobInstance) stop() {
    80  	if j.timer != nil {
    81  		j.timer.Stop()
    82  		*j.earlyReturn = true
    83  	}
    84  }
    85  
    86  // Job represents some work that can be scheduled for execution. The work can
    87  // be safely cancelled when it fires at the same time some "related work" is
    88  // being done.
    89  //
    90  // The term "related work" is defined as some work that needs to be done while
    91  // holding some lock that the timer must also hold while doing some work.
    92  //
    93  // Note, it is not safe to copy a Job as its timer instance creates
    94  // a closure over the address of the Job.
    95  type Job struct {
    96  	_ sync.NoCopy
    97  
    98  	// The clock used to schedule the backing timer
    99  	clock Clock
   100  
   101  	// The active instance of a cancellable timer.
   102  	instance jobInstance
   103  
   104  	// locker is the lock taken by the timer immediately after it fires and must
   105  	// be held when attempting to stop the timer.
   106  	//
   107  	// Must never change after being assigned.
   108  	locker sync.Locker
   109  
   110  	// fn is the function that will be called when a timer fires and has not been
   111  	// signaled to early return.
   112  	//
   113  	// fn MUST NOT attempt to lock locker.
   114  	//
   115  	// Must never change after being assigned.
   116  	fn func()
   117  }
   118  
   119  // Cancel prevents the Job from executing if it has not executed already.
   120  //
   121  // Cancel requires appropriate locking to be in place for any resources managed
   122  // by the Job. If the Job is blocked on obtaining the lock when Cancel is
   123  // called, it will early return.
   124  //
   125  // Note, t will be modified.
   126  //
   127  // j.locker MUST be locked.
   128  func (j *Job) Cancel() {
   129  	j.instance.stop()
   130  
   131  	// Nothing to do with the stopped instance anymore.
   132  	j.instance = jobInstance{}
   133  }
   134  
   135  // Schedule schedules the Job for execution after duration d. This can be
   136  // called on cancelled or completed Jobs to schedule them again.
   137  //
   138  // Schedule should be invoked only on unscheduled, cancelled, or completed
   139  // Jobs. To be safe, callers should always call Cancel before calling Schedule.
   140  //
   141  // Note, j will be modified.
   142  func (j *Job) Schedule(d time.Duration) {
   143  	// Create a new instance.
   144  	earlyReturn := false
   145  
   146  	// Capture the locker so that updating the timer does not cause a data race
   147  	// when a timer fires and tries to obtain the lock (read the timer's locker).
   148  	locker := j.locker
   149  	j.instance = jobInstance{
   150  		timer: j.clock.AfterFunc(d, func() {
   151  			locker.Lock()
   152  			defer locker.Unlock()
   153  
   154  			if earlyReturn {
   155  				// If we reach this point, it means that the timer fired while another
   156  				// goroutine called Cancel while it had the lock. Simply return here
   157  				// and do nothing further.
   158  				earlyReturn = false
   159  				return
   160  			}
   161  
   162  			j.fn()
   163  		}),
   164  		earlyReturn: &earlyReturn,
   165  	}
   166  }
   167  
   168  // NewJob returns a new Job that can be used to schedule f to run in its own
   169  // gorountine. l will be locked before calling f then unlocked after f returns.
   170  //
   171  //  var clock tcpip.StdClock
   172  //  var mu sync.Mutex
   173  //  message := "foo"
   174  //  job := tcpip.NewJob(&clock, &mu, func() {
   175  //    fmt.Println(message)
   176  //  })
   177  //  job.Schedule(time.Second)
   178  //
   179  //  mu.Lock()
   180  //  message = "bar"
   181  //  mu.Unlock()
   182  //
   183  //  // Output: bar
   184  //
   185  // f MUST NOT attempt to lock l.
   186  //
   187  // l MUST be locked prior to calling the returned job's Cancel().
   188  //
   189  //  var clock tcpip.StdClock
   190  //  var mu sync.Mutex
   191  //  message := "foo"
   192  //  job := tcpip.NewJob(&clock, &mu, func() {
   193  //    fmt.Println(message)
   194  //  })
   195  //  job.Schedule(time.Second)
   196  //
   197  //  mu.Lock()
   198  //  job.Cancel()
   199  //  mu.Unlock()
   200  func NewJob(c Clock, l sync.Locker, f func()) *Job {
   201  	return &Job{
   202  		clock:  c,
   203  		locker: l,
   204  		fn:     f,
   205  	}
   206  }