mirror of
https://github.com/redis/go-redis.git
synced 2025-12-02 06:22:31 +03:00
perf(pool): add fast path for Get/Put to match master performance
Introduced TryTransitionFast() for the hot path (Get/Put operations): - Single CAS operation (same as master's atomic bool) - No waiter notification overhead - No loop through valid states - No error allocation Hot path flow: 1. popIdle(): Try IDLE → IN_USE (fast), fallback to CREATED → IN_USE 2. putConn(): Try IN_USE → IDLE (fast) This matches master's performance while preserving state machine for: - Background operations (handoff/reauth use UNUSABLE state) - State validation (TryTransition still available) - Waiter notification (AwaitAndTransition for blocking) Performance comparison per Get/Put cycle: - Master: 2 atomic CAS operations - State machine (before): 5 atomic operations (2.5x slower) - State machine (after): 2 atomic CAS operations (same as master!) Expected improvement: Restore to baseline ~11,373 ops/sec
This commit is contained in:
@@ -121,6 +121,20 @@ func (sm *ConnStateMachine) GetState() ConnState {
|
||||
return ConnState(sm.state.Load())
|
||||
}
|
||||
|
||||
// TryTransitionFast is an optimized version for the hot path (Get/Put operations).
|
||||
// It only handles simple IDLE ⇄ IN_USE transitions without waiter notification.
|
||||
// This is safe because:
|
||||
// 1. Get/Put don't need to wait for state changes
|
||||
// 2. Background operations (handoff/reauth) use UNUSABLE state, which this won't match
|
||||
// 3. If a background operation is in progress (state is UNUSABLE), this fails fast
|
||||
//
|
||||
// Returns true if transition succeeded, false otherwise.
|
||||
// Use this for performance-critical paths where you don't need error details.
|
||||
func (sm *ConnStateMachine) TryTransitionFast(fromState, targetState ConnState) bool {
|
||||
// Single CAS operation - as fast as the old atomic bool!
|
||||
return sm.state.CompareAndSwap(uint32(fromState), uint32(targetState))
|
||||
}
|
||||
|
||||
// TryTransition attempts an immediate state transition without waiting.
|
||||
// Returns the current state after the transition attempt and an error if the transition failed.
|
||||
// The returned state is the CURRENT state (after the attempt), not the previous state.
|
||||
|
||||
@@ -599,16 +599,25 @@ func (p *ConnPool) popIdle() (*Conn, error) {
|
||||
}
|
||||
attempts++
|
||||
|
||||
// Try to atomically transition to IN_USE using state machine
|
||||
// Accept both CREATED (uninitialized) and IDLE (initialized) states
|
||||
// Use predefined slice to avoid allocation
|
||||
_, err := cn.GetStateMachine().TryTransition(validFromCreatedOrIdle, StateInUse)
|
||||
if err == nil {
|
||||
// Successfully acquired the connection
|
||||
// Hot path optimization: try fast IDLE → IN_USE transition first
|
||||
// This is a single CAS operation, as fast as the old atomic bool
|
||||
sm := cn.GetStateMachine()
|
||||
if sm.TryTransitionFast(StateIdle, StateInUse) {
|
||||
// Successfully acquired the connection (common case)
|
||||
p.idleConnsLen.Add(-1)
|
||||
break
|
||||
}
|
||||
|
||||
// Fast path failed - connection might be CREATED (uninitialized) or UNUSABLE (handoff/reauth)
|
||||
// Try CREATED → IN_USE for new connections
|
||||
if sm.TryTransitionFast(StateCreated, StateInUse) {
|
||||
// Successfully acquired uninitialized connection
|
||||
p.idleConnsLen.Add(-1)
|
||||
break
|
||||
}
|
||||
|
||||
// Connection is in UNUSABLE, INITIALIZING, or other state - skip it
|
||||
|
||||
// Connection is not in a valid state (might be UNUSABLE for handoff/re-auth, INITIALIZING, etc.)
|
||||
// Put it back in the pool and try the next one
|
||||
if p.cfg.PoolFIFO {
|
||||
@@ -691,16 +700,13 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {
|
||||
var shouldCloseConn bool
|
||||
|
||||
if p.cfg.MaxIdleConns == 0 || p.idleConnsLen.Load() < p.cfg.MaxIdleConns {
|
||||
// Try to transition to IDLE state BEFORE adding to pool
|
||||
// Only transition if connection is still IN_USE (hooks might have changed state)
|
||||
// This prevents:
|
||||
// 1. Race condition where another goroutine could acquire a connection that's still in IN_USE state
|
||||
// 2. Overwriting state changes made by hooks (e.g., IN_USE → UNUSABLE for handoff)
|
||||
// Use predefined slice to avoid allocation
|
||||
currentState, err := cn.GetStateMachine().TryTransition(validFromInUse, StateIdle)
|
||||
if err != nil {
|
||||
// Hook changed the state (e.g., to UNUSABLE for handoff)
|
||||
// Hot path optimization: try fast IN_USE → IDLE transition
|
||||
// This is a single CAS operation, as fast as the old atomic bool
|
||||
sm := cn.GetStateMachine()
|
||||
if !sm.TryTransitionFast(StateInUse, StateIdle) {
|
||||
// Fast path failed - hook might have changed state (e.g., to UNUSABLE for handoff)
|
||||
// Keep the state set by the hook and pool the connection anyway
|
||||
currentState := sm.GetState()
|
||||
internal.Logger.Printf(ctx, "Connection state changed by hook to %v, pooling as-is", currentState)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user