blob: f5f01f32f2e147bb4cd1ac788d8f538515e6df17 [file] [log] [blame]
// 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.
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
t.instance = cancellableTimerInstance{
timer: time.AfterFunc(d, func() {
t.locker.Lock()
defer t.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,
}
}
// MakeCancellableTimer 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 MakeCancellableTimer(locker sync.Locker, fn func()) CancellableTimer {
return CancellableTimer{locker: locker, fn: fn}
}