VictoriaMetrics/vendor/google.golang.org/grpc/internal/idle/idle.go

279 lines
9.5 KiB
Go
Raw Normal View History

2023-07-07 09:04:32 +02:00
/*
*
* Copyright 2023 gRPC 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.
*
*/
2023-09-07 12:34:14 +02:00
// Package idle contains a component for managing idleness (entering and exiting)
// based on RPC activity.
package idle
2023-07-07 09:04:32 +02:00
import (
"fmt"
"math"
"sync"
"sync/atomic"
"time"
)
// For overriding in unit tests.
var timeAfterFunc = func(d time.Duration, f func()) *time.Timer {
return time.AfterFunc(d, f)
}
2023-09-07 12:34:14 +02:00
// Enforcer is the functionality provided by grpc.ClientConn to enter
2023-07-07 09:04:32 +02:00
// and exit from idle mode.
2023-09-07 12:34:14 +02:00
type Enforcer interface {
ExitIdleMode() error
2023-12-12 23:35:31 +01:00
EnterIdleMode()
2023-07-07 09:04:32 +02:00
}
2023-12-12 23:35:31 +01:00
// Manager implements idleness detection and calls the configured Enforcer to
// enter/exit idle mode when appropriate. Must be created by NewManager.
type Manager struct {
2023-07-07 09:04:32 +02:00
// State accessed atomically.
lastCallEndTime int64 // Unix timestamp in nanos; time when the most recent RPC completed.
activeCallsCount int32 // Count of active RPCs; -math.MaxInt32 means channel is idle or is trying to get there.
activeSinceLastTimerCheck int32 // Boolean; True if there was an RPC since the last timer callback.
closed int32 // Boolean; True when the manager is closed.
// Can be accessed without atomics or mutex since these are set at creation
// time and read-only after that.
2023-09-07 12:34:14 +02:00
enforcer Enforcer // Functionality provided by grpc.ClientConn.
2023-12-12 23:35:31 +01:00
timeout time.Duration
2023-07-07 09:04:32 +02:00
// idleMu is used to guarantee mutual exclusion in two scenarios:
// - Opposing intentions:
// - a: Idle timeout has fired and handleIdleTimeout() is trying to put
// the channel in idle mode because the channel has been inactive.
2023-09-07 12:34:14 +02:00
// - b: At the same time an RPC is made on the channel, and OnCallBegin()
2023-07-07 09:04:32 +02:00
// is trying to prevent the channel from going idle.
// - Competing intentions:
// - The channel is in idle mode and there are multiple RPCs starting at
// the same time, all trying to move the channel out of idle. Only one
// of them should succeed in doing so, while the other RPCs should
// piggyback on the first one and be successfully handled.
idleMu sync.RWMutex
actuallyIdle bool
timer *time.Timer
}
2023-12-12 23:35:31 +01:00
// NewManager creates a new idleness manager implementation for the
// given idle timeout. It begins in idle mode.
func NewManager(enforcer Enforcer, timeout time.Duration) *Manager {
return &Manager{
enforcer: enforcer,
timeout: timeout,
actuallyIdle: true,
activeCallsCount: -math.MaxInt32,
}
2023-09-07 12:34:14 +02:00
}
2023-12-12 23:35:31 +01:00
// resetIdleTimerLocked resets the idle timer to the given duration. Called
// when exiting idle mode or when the timer fires and we need to reset it.
func (m *Manager) resetIdleTimerLocked(d time.Duration) {
if m.isClosed() || m.timeout == 0 || m.actuallyIdle {
return
2023-07-07 09:04:32 +02:00
}
2023-12-12 23:35:31 +01:00
// It is safe to ignore the return value from Reset() because this method is
// only ever called from the timer callback or when exiting idle mode.
if m.timer != nil {
m.timer.Stop()
2023-07-07 09:04:32 +02:00
}
2023-12-12 23:35:31 +01:00
m.timer = timeAfterFunc(d, m.handleIdleTimeout)
2023-07-07 09:04:32 +02:00
}
2023-12-12 23:35:31 +01:00
func (m *Manager) resetIdleTimer(d time.Duration) {
2023-09-07 12:34:14 +02:00
m.idleMu.Lock()
defer m.idleMu.Unlock()
2023-12-12 23:35:31 +01:00
m.resetIdleTimerLocked(d)
2023-07-07 09:04:32 +02:00
}
// handleIdleTimeout is the timer callback that is invoked upon expiry of the
// configured idle timeout. The channel is considered inactive if there are no
// ongoing calls and no RPC activity since the last time the timer fired.
2023-12-12 23:35:31 +01:00
func (m *Manager) handleIdleTimeout() {
2023-09-07 12:34:14 +02:00
if m.isClosed() {
2023-07-07 09:04:32 +02:00
return
}
2023-09-07 12:34:14 +02:00
if atomic.LoadInt32(&m.activeCallsCount) > 0 {
2023-12-12 23:35:31 +01:00
m.resetIdleTimer(m.timeout)
2023-07-07 09:04:32 +02:00
return
}
// There has been activity on the channel since we last got here. Reset the
// timer and return.
2023-09-07 12:34:14 +02:00
if atomic.LoadInt32(&m.activeSinceLastTimerCheck) == 1 {
2023-07-07 09:04:32 +02:00
// Set the timer to fire after a duration of idle timeout, calculated
// from the time the most recent RPC completed.
2023-09-07 12:34:14 +02:00
atomic.StoreInt32(&m.activeSinceLastTimerCheck, 0)
2023-12-12 23:35:31 +01:00
m.resetIdleTimer(time.Duration(atomic.LoadInt64(&m.lastCallEndTime)-time.Now().UnixNano()) + m.timeout)
2023-07-07 09:04:32 +02:00
return
}
2023-12-12 23:35:31 +01:00
// Now that we've checked that there has been no activity, attempt to enter
// idle mode, which is very likely to succeed.
2023-09-07 12:34:14 +02:00
if m.tryEnterIdleMode() {
2023-07-07 09:04:32 +02:00
// Successfully entered idle mode. No timer needed until we exit idle.
return
}
// Failed to enter idle mode due to a concurrent RPC that kept the channel
// active, or because of an error from the channel. Undo the attempt to
// enter idle, and reset the timer to try again later.
2023-12-12 23:35:31 +01:00
m.resetIdleTimer(m.timeout)
2023-07-07 09:04:32 +02:00
}
// tryEnterIdleMode instructs the channel to enter idle mode. But before
// that, it performs a last minute check to ensure that no new RPC has come in,
// making the channel active.
//
// Return value indicates whether or not the channel moved to idle mode.
//
// Holds idleMu which ensures mutual exclusion with exitIdleMode.
2023-12-12 23:35:31 +01:00
func (m *Manager) tryEnterIdleMode() bool {
// Setting the activeCallsCount to -math.MaxInt32 indicates to OnCallBegin()
// that the channel is either in idle mode or is trying to get there.
if !atomic.CompareAndSwapInt32(&m.activeCallsCount, 0, -math.MaxInt32) {
// This CAS operation can fail if an RPC started after we checked for
// activity in the timer handler, or one was ongoing from before the
// last time the timer fired, or if a test is attempting to enter idle
// mode without checking. In all cases, abort going into idle mode.
return false
}
// N.B. if we fail to enter idle mode after this, we must re-add
// math.MaxInt32 to m.activeCallsCount.
2023-09-07 12:34:14 +02:00
m.idleMu.Lock()
defer m.idleMu.Unlock()
2023-07-07 09:04:32 +02:00
2023-09-07 12:34:14 +02:00
if atomic.LoadInt32(&m.activeCallsCount) != -math.MaxInt32 {
2023-07-07 09:04:32 +02:00
// We raced and lost to a new RPC. Very rare, but stop entering idle.
2023-12-12 23:35:31 +01:00
atomic.AddInt32(&m.activeCallsCount, math.MaxInt32)
2023-07-07 09:04:32 +02:00
return false
}
2023-09-07 12:34:14 +02:00
if atomic.LoadInt32(&m.activeSinceLastTimerCheck) == 1 {
2023-12-12 23:35:31 +01:00
// A very short RPC could have come in (and also finished) after we
2023-07-07 09:04:32 +02:00
// checked for calls count and activity in handleIdleTimeout(), but
// before the CAS operation. So, we need to check for activity again.
2023-12-12 23:35:31 +01:00
atomic.AddInt32(&m.activeCallsCount, math.MaxInt32)
2023-07-07 09:04:32 +02:00
return false
}
2023-12-12 23:35:31 +01:00
// No new RPCs have come in since we set the active calls count value to
// -math.MaxInt32. And since we have the lock, it is safe to enter idle mode
// unconditionally now.
m.enforcer.EnterIdleMode()
2023-09-07 12:34:14 +02:00
m.actuallyIdle = true
2023-07-07 09:04:32 +02:00
return true
}
2023-12-12 23:35:31 +01:00
func (m *Manager) EnterIdleModeForTesting() {
m.tryEnterIdleMode()
}
2023-09-07 12:34:14 +02:00
// OnCallBegin is invoked at the start of every RPC.
2023-12-12 23:35:31 +01:00
func (m *Manager) OnCallBegin() error {
2023-09-07 12:34:14 +02:00
if m.isClosed() {
2023-07-07 09:04:32 +02:00
return nil
}
2023-09-07 12:34:14 +02:00
if atomic.AddInt32(&m.activeCallsCount, 1) > 0 {
2023-07-07 09:04:32 +02:00
// Channel is not idle now. Set the activity bit and allow the call.
2023-09-07 12:34:14 +02:00
atomic.StoreInt32(&m.activeSinceLastTimerCheck, 1)
2023-07-07 09:04:32 +02:00
return nil
}
// Channel is either in idle mode or is in the process of moving to idle
// mode. Attempt to exit idle mode to allow this RPC.
2023-12-12 23:35:31 +01:00
if err := m.ExitIdleMode(); err != nil {
2023-07-07 09:04:32 +02:00
// Undo the increment to calls count, and return an error causing the
// RPC to fail.
2023-09-07 12:34:14 +02:00
atomic.AddInt32(&m.activeCallsCount, -1)
2023-07-07 09:04:32 +02:00
return err
}
2023-09-07 12:34:14 +02:00
atomic.StoreInt32(&m.activeSinceLastTimerCheck, 1)
2023-07-07 09:04:32 +02:00
return nil
}
2023-12-12 23:35:31 +01:00
// ExitIdleMode instructs m to call the enforcer's ExitIdleMode and update m's
// internal state.
func (m *Manager) ExitIdleMode() error {
// Holds idleMu which ensures mutual exclusion with tryEnterIdleMode.
2023-09-07 12:34:14 +02:00
m.idleMu.Lock()
defer m.idleMu.Unlock()
2023-07-07 09:04:32 +02:00
2023-12-12 23:35:31 +01:00
if m.isClosed() || !m.actuallyIdle {
// This can happen in three scenarios:
2023-07-07 09:04:32 +02:00
// - handleIdleTimeout() set the calls count to -math.MaxInt32 and called
// tryEnterIdleMode(). But before the latter could grab the lock, an RPC
2023-09-07 12:34:14 +02:00
// came in and OnCallBegin() noticed that the calls count is negative.
2023-07-07 09:04:32 +02:00
// - Channel is in idle mode, and multiple new RPCs come in at the same
2023-09-07 12:34:14 +02:00
// time, all of them notice a negative calls count in OnCallBegin and get
2023-07-07 09:04:32 +02:00
// here. The first one to get the lock would got the channel to exit idle.
2023-12-12 23:35:31 +01:00
// - Channel is not in idle mode, and the user calls Connect which calls
// m.ExitIdleMode.
2023-07-07 09:04:32 +02:00
//
2023-12-12 23:35:31 +01:00
// In any case, there is nothing to do here.
2023-07-07 09:04:32 +02:00
return nil
}
2023-09-07 12:34:14 +02:00
if err := m.enforcer.ExitIdleMode(); err != nil {
2023-12-12 23:35:31 +01:00
return fmt.Errorf("failed to exit idle mode: %w", err)
2023-07-07 09:04:32 +02:00
}
// Undo the idle entry process. This also respects any new RPC attempts.
2023-09-07 12:34:14 +02:00
atomic.AddInt32(&m.activeCallsCount, math.MaxInt32)
m.actuallyIdle = false
2023-07-07 09:04:32 +02:00
// Start a new timer to fire after the configured idle timeout.
2023-12-12 23:35:31 +01:00
m.resetIdleTimerLocked(m.timeout)
2023-07-07 09:04:32 +02:00
return nil
}
2023-09-07 12:34:14 +02:00
// OnCallEnd is invoked at the end of every RPC.
2023-12-12 23:35:31 +01:00
func (m *Manager) OnCallEnd() {
2023-09-07 12:34:14 +02:00
if m.isClosed() {
2023-07-07 09:04:32 +02:00
return
}
// Record the time at which the most recent call finished.
2023-09-07 12:34:14 +02:00
atomic.StoreInt64(&m.lastCallEndTime, time.Now().UnixNano())
2023-07-07 09:04:32 +02:00
// Decrement the active calls count. This count can temporarily go negative
// when the timer callback is in the process of moving the channel to idle
// mode, but one or more RPCs come in and complete before the timer callback
// can get done with the process of moving to idle mode.
2023-09-07 12:34:14 +02:00
atomic.AddInt32(&m.activeCallsCount, -1)
2023-07-07 09:04:32 +02:00
}
2023-12-12 23:35:31 +01:00
func (m *Manager) isClosed() bool {
2023-09-07 12:34:14 +02:00
return atomic.LoadInt32(&m.closed) == 1
2023-07-07 09:04:32 +02:00
}
2023-12-12 23:35:31 +01:00
func (m *Manager) Close() {
2023-09-07 12:34:14 +02:00
atomic.StoreInt32(&m.closed, 1)
2023-07-07 09:04:32 +02:00
2023-09-07 12:34:14 +02:00
m.idleMu.Lock()
2023-12-12 23:35:31 +01:00
if m.timer != nil {
m.timer.Stop()
m.timer = nil
}
2023-09-07 12:34:14 +02:00
m.idleMu.Unlock()
2023-07-07 09:04:32 +02:00
}