From 55c502dde4815f064cbcc32d949a69faa9bcd1c3 Mon Sep 17 00:00:00 2001 From: Nedyalko Dyakov Date: Mon, 27 Oct 2025 07:30:09 +0200 Subject: [PATCH] try to cache time if for non-critical calculation --- internal/pool/conn.go | 39 ++++++++++++++++++++++++++++++++++++--- internal/pool/pool.go | 13 ++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/internal/pool/conn.go b/internal/pool/conn.go index 470797ff..56be7098 100644 --- a/internal/pool/conn.go +++ b/internal/pool/conn.go @@ -18,6 +18,35 @@ import ( var noDeadline = time.Time{} +// Global time cache updated every 50ms by background goroutine. +// This avoids expensive time.Now() syscalls in hot paths like getEffectiveReadTimeout. +// Max staleness: 50ms, which is acceptable for timeout deadline checks (timeouts are typically 3-30 seconds). +var globalTimeCache struct { + nowNs atomic.Int64 +} + +func init() { + // Initialize immediately + globalTimeCache.nowNs.Store(time.Now().UnixNano()) + + // Start background updater + go func() { + ticker := time.NewTicker(50 * time.Millisecond) + defer ticker.Stop() + + for range ticker.C { + globalTimeCache.nowNs.Store(time.Now().UnixNano()) + } + }() +} + +// getCachedTimeNs returns the current time in nanoseconds from the global cache. +// This is updated every 50ms by a background goroutine, avoiding expensive syscalls. +// Max staleness: 50ms. +func getCachedTimeNs() int64 { + return globalTimeCache.nowNs.Load() +} + // Global atomic counter for connection IDs var connIDCounter uint64 @@ -458,7 +487,8 @@ func (cn *Conn) getEffectiveReadTimeout(normalTimeout time.Duration) time.Durati return time.Duration(readTimeoutNs) } - nowNs := time.Now().UnixNano() + // Use cached time to avoid expensive syscall (max 50ms staleness is acceptable for timeout checks) + nowNs := getCachedTimeNs() // Check if deadline has passed if nowNs < deadlineNs { // Deadline is in the future, use relaxed timeout @@ -491,7 +521,8 @@ func (cn *Conn) getEffectiveWriteTimeout(normalTimeout time.Duration) time.Durat return time.Duration(writeTimeoutNs) } - nowNs := time.Now().UnixNano() + // Use cached time to avoid expensive syscall (max 50ms staleness is acceptable for timeout checks) + nowNs := getCachedTimeNs() // Check if deadline has passed if nowNs < deadlineNs { // Deadline is in the future, use relaxed timeout @@ -843,8 +874,10 @@ func (cn *Conn) MaybeHasData() bool { // deadline computes the effective deadline time based on context and timeout. // It updates the usedAt timestamp to now. +// Uses cached time to avoid expensive syscall (max 50ms staleness is acceptable for deadline calculation). func (cn *Conn) deadline(ctx context.Context, timeout time.Duration) time.Time { - tm := time.Now() + // Use cached time for deadline calculation (called 2x per command: read + write) + tm := time.Unix(0, getCachedTimeNs()) cn.SetUsedAt(tm) if timeout > 0 { diff --git a/internal/pool/pool.go b/internal/pool/pool.go index f5a094c1..2dedca05 100644 --- a/internal/pool/pool.go +++ b/internal/pool/pool.go @@ -27,6 +27,12 @@ var ( // ErrConnUnusableTimeout is returned when a connection is not usable and we timed out trying to mark it as unusable. ErrConnUnusableTimeout = errors.New("redis: timed out trying to mark connection as unusable") + // errHookRequestedRemoval is returned when a hook requests connection removal. + errHookRequestedRemoval = errors.New("hook requested removal") + + // errConnNotPooled is returned when trying to return a non-pooled connection to the pool. + errConnNotPooled = errors.New("connection not pooled") + // popAttempts is the maximum number of attempts to find a usable connection // when popping from the idle connection pool. This handles cases where connections // are temporarily marked as unusable (e.g., during maintenanceNotifications upgrades or network issues). @@ -446,7 +452,8 @@ func (p *ConnPool) getConn(ctx context.Context) (*Conn, error) { return nil, err } - now := time.Now() + // Use cached time for health checks (max 50ms staleness is acceptable) + now := time.Unix(0, getCachedTimeNs()) attempts := 0 // Lock-free atomic read - no mutex overhead! @@ -661,12 +668,12 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) { // Combine all removal checks into one - reduces branches if shouldRemove || !shouldPool { - p.removeConnInternal(ctx, cn, errors.New("hook requested removal"), freeTurn) + p.removeConnInternal(ctx, cn, errHookRequestedRemoval, freeTurn) return } if !cn.pooled { - p.removeConnInternal(ctx, cn, errors.New("connection not pooled"), freeTurn) + p.removeConnInternal(ctx, cn, errConnNotPooled, freeTurn) return }