185 lines
6.8 KiB
Go
185 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 (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// cancellableTimerInstance is a specific instance of CancellableTimer.
|
|
//
|
|
// Different instances are created each time CancellableTimer is Reset so each
|
|
// timer has its own earlyReturn signal. This is to address a bug when a
|
|
// CancellableTimer 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 CancellableTimer 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: Reset timer (create instance B)
|
|
// T3: instance B fires, blocked trying to obtain L.
|
|
// T1: Attempt to stop instance B (set earlyReturn = true)
|
|
// T1: Reset timer (create instance C)
|
|
// T4: instance C fires, blocked trying to obtain L.
|
|
// T1: Attempt to stop instance C (set earlyReturn = true)
|
|
// T1: Reset 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 CancellableTimer 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 cancellableTimerInstance struct {
|
|
timer *time.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 StopLocked()
|
|
// 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 timer instance t 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 (t *cancellableTimerInstance) stop() {
|
|
if t.timer != nil {
|
|
t.timer.Stop()
|
|
*t.earlyReturn = true
|
|
}
|
|
}
|
|
|
|
// CancellableTimer is a timer that does some work and 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 CancellableTimer as its timer instance creates
|
|
// a closure over the address of the CancellableTimer.
|
|
type CancellableTimer struct {
|
|
// The active instance of a cancellable timer.
|
|
instance cancellableTimerInstance
|
|
|
|
// 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()
|
|
}
|
|
|
|
// StopLocked prevents the Timer from firing if it has not fired already.
|
|
//
|
|
// If the timer is blocked on obtaining the t.locker lock when StopLocked is
|
|
// called, it will early return instead of calling t.fn.
|
|
//
|
|
// Note, t will be modified.
|
|
//
|
|
// t.locker MUST be locked.
|
|
func (t *CancellableTimer) StopLocked() {
|
|
t.instance.stop()
|
|
|
|
// Nothing to do with the stopped instance anymore.
|
|
t.instance = cancellableTimerInstance{}
|
|
}
|
|
|
|
// Reset changes the timer to expire after duration d.
|
|
//
|
|
// Note, t will be modified.
|
|
//
|
|
// Reset should only be called on stopped or expired timers. To be safe, callers
|
|
// should always call StopLocked before calling Reset.
|
|
func (t *CancellableTimer) Reset(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 := t.locker
|
|
t.instance = cancellableTimerInstance{
|
|
timer: time.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 StopLocked while it had the lock. Simply return
|
|
// here and do nothing further.
|
|
earlyReturn = false
|
|
return
|
|
}
|
|
|
|
t.fn()
|
|
}),
|
|
earlyReturn: &earlyReturn,
|
|
}
|
|
}
|
|
|
|
// Lock is a no-op used by the copylocks checker from go vet.
|
|
//
|
|
// See CancellableTimer for details about why it shouldn't be copied.
|
|
//
|
|
// See https://github.com/golang/go/issues/8005#issuecomment-190753527 for more
|
|
// details about the copylocks checker.
|
|
func (*CancellableTimer) Lock() {}
|
|
|
|
// Unlock is a no-op used by the copylocks checker from go vet.
|
|
//
|
|
// See CancellableTimer for details about why it shouldn't be copied.
|
|
//
|
|
// See https://github.com/golang/go/issues/8005#issuecomment-190753527 for more
|
|
// details about the copylocks checker.
|
|
func (*CancellableTimer) Unlock() {}
|
|
|
|
// NewCancellableTimer returns an unscheduled CancellableTimer with the given
|
|
// locker and fn.
|
|
//
|
|
// fn MUST NOT attempt to lock locker.
|
|
//
|
|
// Callers must call Reset to schedule the timer to fire.
|
|
func NewCancellableTimer(locker sync.Locker, fn func()) *CancellableTimer {
|
|
return &CancellableTimer{locker: locker, fn: fn}
|
|
}
|