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