207 lines
6.8 KiB
Go
207 lines
6.8 KiB
Go
// Copyright 2020 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package tcpip
|
|
|
|
import (
|
|
"time"
|
|
|
|
"gvisor.dev/gvisor/pkg/sync"
|
|
)
|
|
|
|
// jobInstance is a specific instance of Job.
|
|
//
|
|
// Different instances are created each time Job is scheduled so each timer has
|
|
// its own earlyReturn signal. This is to address a bug when a Job is stopped
|
|
// and reset in quick succession resulting in a timer instance's earlyReturn
|
|
// signal being affected or seen by another timer instance.
|
|
//
|
|
// Consider the following sceneario where timer instances share a common
|
|
// earlyReturn signal (T1 creates, stops and resets a Cancellable timer under a
|
|
// lock L; T2, T3, T4 and T5 are goroutines that handle the first (A), second
|
|
// (B), third (C), and fourth (D) instance of the timer firing, respectively):
|
|
// T1: Obtain L
|
|
// T1: Create a new Job w/ lock L (create instance A)
|
|
// T2: instance A fires, blocked trying to obtain L.
|
|
// T1: Attempt to stop instance A (set earlyReturn = true)
|
|
// T1: Schedule timer (create instance B)
|
|
// T3: instance B fires, blocked trying to obtain L.
|
|
// T1: Attempt to stop instance B (set earlyReturn = true)
|
|
// T1: Schedule timer (create instance C)
|
|
// T4: instance C fires, blocked trying to obtain L.
|
|
// T1: Attempt to stop instance C (set earlyReturn = true)
|
|
// T1: Schedule timer (create instance D)
|
|
// T5: instance D fires, blocked trying to obtain L.
|
|
// T1: Release L
|
|
//
|
|
// Now that T1 has released L, any of the 4 timer instances can take L and
|
|
// check earlyReturn. If the timers simply check earlyReturn and then do
|
|
// nothing further, then instance D will never early return even though it was
|
|
// not requested to stop. If the timers reset earlyReturn before early
|
|
// returning, then all but one of the timers will do work when only one was
|
|
// expected to. If Job resets earlyReturn when resetting, then all the timers
|
|
// will fire (again, when only one was expected to).
|
|
//
|
|
// To address the above concerns the simplest solution was to give each timer
|
|
// its own earlyReturn signal.
|
|
type jobInstance struct {
|
|
timer Timer
|
|
|
|
// Used to inform the timer to early return when it gets stopped while the
|
|
// lock the timer tries to obtain when fired is held (T1 is a goroutine that
|
|
// tries to cancel the timer and T2 is the goroutine that handles the timer
|
|
// firing):
|
|
// T1: Obtain the lock, then call Cancel()
|
|
// T2: timer fires, and gets blocked on obtaining the lock
|
|
// T1: Releases lock
|
|
// T2: Obtains lock does unintended work
|
|
//
|
|
// To resolve this, T1 will check to see if the timer already fired, and
|
|
// inform the timer using earlyReturn to return early so that once T2 obtains
|
|
// the lock, it will see that it is set to true and do nothing further.
|
|
earlyReturn *bool
|
|
}
|
|
|
|
// stop stops the job instance j from firing if it hasn't fired already. If it
|
|
// has fired and is blocked at obtaining the lock, earlyReturn will be set to
|
|
// true so that it will early return when it obtains the lock.
|
|
func (j *jobInstance) stop() {
|
|
if j.timer != nil {
|
|
j.timer.Stop()
|
|
*j.earlyReturn = true
|
|
}
|
|
}
|
|
|
|
// Job represents some work that can be scheduled for execution. The work can
|
|
// be safely cancelled when it fires at the same time some "related work" is
|
|
// being done.
|
|
//
|
|
// The term "related work" is defined as some work that needs to be done while
|
|
// holding some lock that the timer must also hold while doing some work.
|
|
//
|
|
// Note, it is not safe to copy a Job as its timer instance creates
|
|
// a closure over the address of the Job.
|
|
type Job struct {
|
|
_ sync.NoCopy
|
|
|
|
// The clock used to schedule the backing timer
|
|
clock Clock
|
|
|
|
// The active instance of a cancellable timer.
|
|
instance jobInstance
|
|
|
|
// locker is the lock taken by the timer immediately after it fires and must
|
|
// be held when attempting to stop the timer.
|
|
//
|
|
// Must never change after being assigned.
|
|
locker sync.Locker
|
|
|
|
// fn is the function that will be called when a timer fires and has not been
|
|
// signaled to early return.
|
|
//
|
|
// fn MUST NOT attempt to lock locker.
|
|
//
|
|
// Must never change after being assigned.
|
|
fn func()
|
|
}
|
|
|
|
// Cancel prevents the Job from executing if it has not executed already.
|
|
//
|
|
// Cancel requires appropriate locking to be in place for any resources managed
|
|
// by the Job. If the Job is blocked on obtaining the lock when Cancel is
|
|
// called, it will early return.
|
|
//
|
|
// Note, t will be modified.
|
|
//
|
|
// j.locker MUST be locked.
|
|
func (j *Job) Cancel() {
|
|
j.instance.stop()
|
|
|
|
// Nothing to do with the stopped instance anymore.
|
|
j.instance = jobInstance{}
|
|
}
|
|
|
|
// Schedule schedules the Job for execution after duration d. This can be
|
|
// called on cancelled or completed Jobs to schedule them again.
|
|
//
|
|
// Schedule should be invoked only on unscheduled, cancelled, or completed
|
|
// Jobs. To be safe, callers should always call Cancel before calling Schedule.
|
|
//
|
|
// Note, j will be modified.
|
|
func (j *Job) Schedule(d time.Duration) {
|
|
// Create a new instance.
|
|
earlyReturn := false
|
|
|
|
// Capture the locker so that updating the timer does not cause a data race
|
|
// when a timer fires and tries to obtain the lock (read the timer's locker).
|
|
locker := j.locker
|
|
j.instance = jobInstance{
|
|
timer: j.clock.AfterFunc(d, func() {
|
|
locker.Lock()
|
|
defer locker.Unlock()
|
|
|
|
if earlyReturn {
|
|
// If we reach this point, it means that the timer fired while another
|
|
// goroutine called Cancel while it had the lock. Simply return here
|
|
// and do nothing further.
|
|
earlyReturn = false
|
|
return
|
|
}
|
|
|
|
j.fn()
|
|
}),
|
|
earlyReturn: &earlyReturn,
|
|
}
|
|
}
|
|
|
|
// NewJob returns a new Job that can be used to schedule f to run in its own
|
|
// gorountine. l will be locked before calling f then unlocked after f returns.
|
|
//
|
|
// var clock tcpip.StdClock
|
|
// var mu sync.Mutex
|
|
// message := "foo"
|
|
// job := tcpip.NewJob(&clock, &mu, func() {
|
|
// fmt.Println(message)
|
|
// })
|
|
// job.Schedule(time.Second)
|
|
//
|
|
// mu.Lock()
|
|
// message = "bar"
|
|
// mu.Unlock()
|
|
//
|
|
// // Output: bar
|
|
//
|
|
// f MUST NOT attempt to lock l.
|
|
//
|
|
// l MUST be locked prior to calling the returned job's Cancel().
|
|
//
|
|
// var clock tcpip.StdClock
|
|
// var mu sync.Mutex
|
|
// message := "foo"
|
|
// job := tcpip.NewJob(&clock, &mu, func() {
|
|
// fmt.Println(message)
|
|
// })
|
|
// job.Schedule(time.Second)
|
|
//
|
|
// mu.Lock()
|
|
// job.Cancel()
|
|
// mu.Unlock()
|
|
func NewJob(c Clock, l sync.Locker, f func()) *Job {
|
|
return &Job{
|
|
clock: c,
|
|
locker: l,
|
|
fn: f,
|
|
}
|
|
}
|