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 }