From 0773d52244a9fd865ff892eb653cefb7714b4d23 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Sat, 25 Oct 2025 20:40:30 +0300 Subject: [PATCH] perf(pool): add fast path for Get/Put to match master performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/pool/conn_state.go | 14 ++++++++++++++ internal/pool/pool.go | 36 +++++++++++++++++++++--------------- 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/internal/pool/conn_state.go b/internal/pool/conn_state.go index fce0fb47..b0142320 100644 --- a/internal/pool/conn_state.go +++ b/internal/pool/conn_state.go @@ -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. diff --git a/internal/pool/pool.go b/internal/pool/pool.go index dd8bb764..598d0096 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -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) }