// 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

	// 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,
	}
}

// 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}
}