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())
|
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.
|
// TryTransition attempts an immediate state transition without waiting.
|
||||||
// Returns the current state after the transition attempt and an error if the transition failed.
|
// 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.
|
// 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++
|
attempts++
|
||||||
|
|
||||||
// Try to atomically transition to IN_USE using state machine
|
// Hot path optimization: try fast IDLE → IN_USE transition first
|
||||||
// Accept both CREATED (uninitialized) and IDLE (initialized) states
|
// This is a single CAS operation, as fast as the old atomic bool
|
||||||
// Use predefined slice to avoid allocation
|
sm := cn.GetStateMachine()
|
||||||
_, err := cn.GetStateMachine().TryTransition(validFromCreatedOrIdle, StateInUse)
|
if sm.TryTransitionFast(StateIdle, StateInUse) {
|
||||||
if err == nil {
|
// Successfully acquired the connection (common case)
|
||||||
// Successfully acquired the connection
|
|
||||||
p.idleConnsLen.Add(-1)
|
p.idleConnsLen.Add(-1)
|
||||||
break
|
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.)
|
// 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
|
// Put it back in the pool and try the next one
|
||||||
if p.cfg.PoolFIFO {
|
if p.cfg.PoolFIFO {
|
||||||
@@ -691,16 +700,13 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {
|
|||||||
var shouldCloseConn bool
|
var shouldCloseConn bool
|
||||||
|
|
||||||
if p.cfg.MaxIdleConns == 0 || p.idleConnsLen.Load() < p.cfg.MaxIdleConns {
|
if p.cfg.MaxIdleConns == 0 || p.idleConnsLen.Load() < p.cfg.MaxIdleConns {
|
||||||
// Try to transition to IDLE state BEFORE adding to pool
|
// Hot path optimization: try fast IN_USE → IDLE transition
|
||||||
// Only transition if connection is still IN_USE (hooks might have changed state)
|
// This is a single CAS operation, as fast as the old atomic bool
|
||||||
// This prevents:
|
sm := cn.GetStateMachine()
|
||||||
// 1. Race condition where another goroutine could acquire a connection that's still in IN_USE state
|
if !sm.TryTransitionFast(StateInUse, StateIdle) {
|
||||||
// 2. Overwriting state changes made by hooks (e.g., IN_USE → UNUSABLE for handoff)
|
// Fast path failed - hook might have changed state (e.g., to 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)
|
|
||||||
// Keep the state set by the hook and pool the connection anyway
|
// 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)
|
internal.Logger.Printf(ctx, "Connection state changed by hook to %v, pooling as-is", currentState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user