1
0
mirror of https://github.com/redis/go-redis.git synced 2025-12-03 18:31:14 +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:
Nedyalko Dyakov
2025-10-25 20:40:30 +03:00
parent f08338ebdf
commit 0773d52244
2 changed files with 35 additions and 15 deletions

View File

@@ -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.

View File

@@ -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)
} }