mirror of
https://github.com/hanzokv/go.git
synced 2026-09-24 00:31:51 +00:00
fix(otel): otel-native: Fix metric instrument types (#3743)
* Change metric type from gauge to upAndDownCounter * Fix pending requests metric * Fix go version * Bump grpc version * Fix UpDownCounter metric accounting bugs in connection pool Fix two bugs in the UpDownCounter metric instrumentation: 1. Cache miss path double-counts idle connections (High Severity): newConn() records +1 idle for every new connection, but the cache miss path in getConn() only recorded +1 used without compensating -1 idle. This caused the idle counter to grow unboundedly. Fixed by adding -1 idle compensation in the cache miss path and in the hook failure path for new connections. 2. Hook-rejected idle connection causes incorrect state accounting (Medium Severity): When a hook rejects an idle connection without error, putConnWithoutTurn calls putConn which records -1 used, +1 idle. But the connection was never transitioned to used state, causing the used counter to go negative. Fixed by recording the idle->used transition before calling putConnWithoutTurn so putConn's used->idle accounting is balanced. * Fix UpDownCounter metric gaps * fix: guard putConn metric emission against closed connections to prevent idle counter drift * fix: Non-pooled connections get wrong metric state attribution * fix: Pool Close() double-decrements in-use connection counters * fix: Monotonic counter suppressed inside pool-closed guard * fix: Shutdown race causes permanent UpDownCounter metric drift * fix: Connection closed metric double-counted during pool shutdown * fix:Race between putConn and Close causes permanent counter drift * fix:Missing metric decrement when putIdleConn delivers to closed pool --------- Co-authored-by: Nedyalko Dyakov <[email protected]>
This commit is contained in:
co-authored by
Nedyalko Dyakov
parent
d6b95bcdcf
commit
6cc94ebe5c
@@ -7,7 +7,6 @@ import (
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
@@ -38,31 +37,19 @@ func addServerPortIfNonDefault(attrs []attribute.KeyValue, serverPort string) []
|
||||
return attrs
|
||||
}
|
||||
|
||||
// poolInfo stores information about a registered main connection pool
|
||||
type poolInfo struct {
|
||||
name string
|
||||
pool redis.Pooler
|
||||
}
|
||||
|
||||
// pubsubPoolInfo stores information about a registered PubSub pool
|
||||
type pubsubPoolInfo struct {
|
||||
name string
|
||||
pool redis.PubSubPooler
|
||||
}
|
||||
|
||||
// metricsRecorder implements the otel.Recorder interface
|
||||
type metricsRecorder struct {
|
||||
operationDuration metric.Float64Histogram
|
||||
connectionCountGauge metric.Int64ObservableGauge
|
||||
connectionCount metric.Int64UpDownCounter // OTel semconv: UpDownCounter for connection.count
|
||||
connectionCreateTime metric.Float64Histogram
|
||||
connectionRelaxedTimeout metric.Int64UpDownCounter
|
||||
connectionHandoff metric.Int64Counter
|
||||
clientErrors metric.Int64Counter
|
||||
maintenanceNotifications metric.Int64Counter
|
||||
|
||||
connectionWaitTime metric.Float64Histogram
|
||||
connectionClosed metric.Int64Counter
|
||||
connectionPendingReqsGauge metric.Int64ObservableGauge
|
||||
connectionWaitTime metric.Float64Histogram
|
||||
connectionClosed metric.Int64Counter
|
||||
connectionPendingReqs metric.Int64UpDownCounter // OTel semconv: UpDownCounter for pending_requests
|
||||
|
||||
pubsubMessages metric.Int64Counter
|
||||
|
||||
@@ -70,11 +57,6 @@ type metricsRecorder struct {
|
||||
|
||||
// Configuration
|
||||
cfg *config
|
||||
|
||||
// Pool registry for tracking multiple pools
|
||||
poolsMu sync.RWMutex
|
||||
pools []poolInfo
|
||||
pubsubPools []pubsubPoolInfo
|
||||
}
|
||||
|
||||
// RecordOperationDuration records db.client.operation.duration metric
|
||||
@@ -849,62 +831,86 @@ func (r *metricsRecorder) RecordStreamLag(
|
||||
r.streamLag.Record(ctx, lag.Seconds(), metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RegisterPool implements the OTelPoolRegistrar interface.
|
||||
// The pools are used by async gauge callbacks to pull statistics.
|
||||
func (r *metricsRecorder) RegisterPool(poolName string, pool redis.Pooler) {
|
||||
r.poolsMu.Lock()
|
||||
defer r.poolsMu.Unlock()
|
||||
// RecordConnectionCount records a change in connection count (UpDownCounter).
|
||||
// Per OTel semantic conventions, db.client.connection.count is an UpDownCounter.
|
||||
// delta: +1 when connection added, -1 when connection removed
|
||||
// state: connection state (e.g., "idle", "used")
|
||||
// isPubSub: true if this is a PubSub connection
|
||||
func (r *metricsRecorder) RecordConnectionCount(
|
||||
ctx context.Context,
|
||||
delta int,
|
||||
cn redis.ConnInfo,
|
||||
state string,
|
||||
isPubSub bool,
|
||||
) {
|
||||
if r.connectionCount == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add pool to registry
|
||||
r.pools = append(r.pools, poolInfo{
|
||||
name: poolName,
|
||||
pool: pool,
|
||||
})
|
||||
}
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
attribute.String(AttrDBClientConnectionState, state),
|
||||
attribute.Bool(AttrRedisClientConnectionPubSub, isPubSub),
|
||||
}
|
||||
|
||||
// UnregisterPool implements the OTelPoolRegistrar interface.
|
||||
// This ensures async gauge callbacks don't try to access closed pools.
|
||||
func (r *metricsRecorder) UnregisterPool(pool redis.Pooler) {
|
||||
r.poolsMu.Lock()
|
||||
defer r.poolsMu.Unlock()
|
||||
// Use pool name from connection (set when connection was created)
|
||||
if cn != nil {
|
||||
poolName := cn.PoolName()
|
||||
if poolName != "" {
|
||||
attrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, poolName))
|
||||
|
||||
// Find and remove the pool from registry
|
||||
for i, p := range r.pools {
|
||||
if p.pool == pool {
|
||||
// Remove by swapping with last element and truncating
|
||||
r.pools[i] = r.pools[len(r.pools)-1]
|
||||
r.pools = r.pools[:len(r.pools)-1]
|
||||
return
|
||||
// Extract server info from pool name
|
||||
serverAddr, serverPort, _ := parsePoolName(poolName)
|
||||
if serverAddr != "" {
|
||||
attrs = append(attrs, attribute.String(AttrServerAddress, serverAddr))
|
||||
}
|
||||
attrs = addServerPortIfNonDefault(attrs, serverPort)
|
||||
}
|
||||
}
|
||||
|
||||
// Record the counter (delta can be +1 or -1)
|
||||
r.connectionCount.Add(ctx, int64(delta), metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
// RegisterPubSubPool implements the OTelPoolRegistrar interface.
|
||||
// The pools are used by async gauge callbacks to pull statistics.
|
||||
func (r *metricsRecorder) RegisterPubSubPool(poolName string, pool redis.PubSubPooler) {
|
||||
r.poolsMu.Lock()
|
||||
defer r.poolsMu.Unlock()
|
||||
|
||||
// Add PubSub pool to registry
|
||||
r.pubsubPools = append(r.pubsubPools, pubsubPoolInfo{
|
||||
name: poolName,
|
||||
pool: pool,
|
||||
})
|
||||
}
|
||||
|
||||
// UnregisterPubSubPool implements the OTelPoolRegistrar interface.
|
||||
// This ensures async gauge callbacks don't try to access closed pools.
|
||||
func (r *metricsRecorder) UnregisterPubSubPool(pool redis.PubSubPooler) {
|
||||
r.poolsMu.Lock()
|
||||
defer r.poolsMu.Unlock()
|
||||
|
||||
// Find and remove the pool from registry
|
||||
for i, p := range r.pubsubPools {
|
||||
if p.pool == pool {
|
||||
// Remove by swapping with last element and truncating
|
||||
r.pubsubPools[i] = r.pubsubPools[len(r.pubsubPools)-1]
|
||||
r.pubsubPools = r.pubsubPools[:len(r.pubsubPools)-1]
|
||||
return
|
||||
}
|
||||
// RecordPendingRequests records a change in pending requests (UpDownCounter).
|
||||
// Per OTel semantic conventions, db.client.connection.pending_requests is an UpDownCounter.
|
||||
// delta: +1 when request starts waiting, -1 when request stops waiting
|
||||
// poolName is passed explicitly because we may not have a connection yet when request starts
|
||||
func (r *metricsRecorder) RecordPendingRequests(
|
||||
ctx context.Context,
|
||||
delta int,
|
||||
cn redis.ConnInfo,
|
||||
poolName string,
|
||||
) {
|
||||
if r.connectionPendingReqs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Build attributes
|
||||
attrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
|
||||
// Use explicit pool name (preferred) or fall back to connection's pool name
|
||||
effectivePoolName := poolName
|
||||
if effectivePoolName == "" && cn != nil {
|
||||
effectivePoolName = cn.PoolName()
|
||||
}
|
||||
|
||||
if effectivePoolName != "" {
|
||||
attrs = append(attrs, attribute.String(AttrDBClientConnectionPoolName, effectivePoolName))
|
||||
|
||||
// Extract server info from pool name
|
||||
serverAddr, serverPort, _ := parsePoolName(effectivePoolName)
|
||||
if serverAddr != "" {
|
||||
attrs = append(attrs, attribute.String(AttrServerAddress, serverAddr))
|
||||
}
|
||||
attrs = addServerPortIfNonDefault(attrs, serverPort)
|
||||
}
|
||||
|
||||
// Record the counter (delta can be +1 or -1)
|
||||
r.connectionPendingReqs.Add(ctx, int64(delta), metric.WithAttributes(attrs...))
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package redisotel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
"go.opentelemetry.io/otel/semconv/v1.38.0/dbconv"
|
||||
|
||||
@@ -198,13 +196,13 @@ func (o *ObservabilityInstance) createRecorder(meter metric.Meter, cfg config) (
|
||||
operationDuration = operationDurationConv.Inst()
|
||||
}
|
||||
|
||||
var connectionCountGauge metric.Int64ObservableGauge
|
||||
var connectionCount metric.Int64UpDownCounter // OTel semconv: UpDownCounter
|
||||
var connectionCreateTime metric.Float64Histogram
|
||||
var connectionRelaxedTimeout metric.Int64UpDownCounter
|
||||
var connectionHandoff metric.Int64Counter
|
||||
|
||||
if cfg.isMetricGroupEnabled(MetricGroupConnectionBasic) {
|
||||
connectionCountGauge, err = meter.Int64ObservableGauge(
|
||||
connectionCount, err = meter.Int64UpDownCounter(
|
||||
dbconv.ClientConnectionCount{}.Name(),
|
||||
metric.WithDescription(dbconv.ClientConnectionCount{}.Description()),
|
||||
metric.WithUnit(dbconv.ClientConnectionCount{}.Unit()),
|
||||
@@ -269,7 +267,7 @@ func (o *ObservabilityInstance) createRecorder(meter metric.Meter, cfg config) (
|
||||
|
||||
var connectionWaitTime metric.Float64Histogram
|
||||
var connectionClosed metric.Int64Counter
|
||||
var connectionPendingReqsGauge metric.Int64ObservableGauge
|
||||
var connectionPendingReqs metric.Int64UpDownCounter // OTel semconv: UpDownCounter
|
||||
|
||||
if cfg.isMetricGroupEnabled(MetricGroupConnectionAdvanced) {
|
||||
var connectionWaitTimeOpts []metric.Float64HistogramOption
|
||||
@@ -294,7 +292,7 @@ func (o *ObservabilityInstance) createRecorder(meter metric.Meter, cfg config) (
|
||||
return nil, fmt.Errorf("failed to create connection closed metric: %w", err)
|
||||
}
|
||||
|
||||
connectionPendingReqsGauge, err = meter.Int64ObservableGauge(
|
||||
connectionPendingReqs, err = meter.Int64UpDownCounter(
|
||||
dbconv.ClientConnectionPendingRequests{}.Name(),
|
||||
metric.WithDescription(dbconv.ClientConnectionPendingRequests{}.Description()),
|
||||
metric.WithUnit(dbconv.ClientConnectionPendingRequests{}.Unit()),
|
||||
@@ -341,26 +339,19 @@ func (o *ObservabilityInstance) createRecorder(meter metric.Meter, cfg config) (
|
||||
|
||||
// Create recorder
|
||||
recorder := &metricsRecorder{
|
||||
operationDuration: operationDuration,
|
||||
connectionCountGauge: connectionCountGauge,
|
||||
connectionCreateTime: connectionCreateTime,
|
||||
connectionRelaxedTimeout: connectionRelaxedTimeout,
|
||||
connectionHandoff: connectionHandoff,
|
||||
clientErrors: clientErrors,
|
||||
maintenanceNotifications: maintenanceNotifications,
|
||||
connectionWaitTime: connectionWaitTime,
|
||||
connectionClosed: connectionClosed,
|
||||
connectionPendingReqsGauge: connectionPendingReqsGauge,
|
||||
pubsubMessages: pubsubMessages,
|
||||
streamLag: streamLag,
|
||||
cfg: &cfg,
|
||||
pools: make([]poolInfo, 0),
|
||||
}
|
||||
|
||||
// Register async callbacks for ObservableGauges
|
||||
// These callbacks will pull stats from registered pools
|
||||
if err := o.registerAsyncCallbacks(meter, recorder); err != nil {
|
||||
return nil, fmt.Errorf("failed to register async callbacks: %w", err)
|
||||
operationDuration: operationDuration,
|
||||
connectionCount: connectionCount,
|
||||
connectionCreateTime: connectionCreateTime,
|
||||
connectionRelaxedTimeout: connectionRelaxedTimeout,
|
||||
connectionHandoff: connectionHandoff,
|
||||
clientErrors: clientErrors,
|
||||
maintenanceNotifications: maintenanceNotifications,
|
||||
connectionWaitTime: connectionWaitTime,
|
||||
connectionClosed: connectionClosed,
|
||||
connectionPendingReqs: connectionPendingReqs,
|
||||
pubsubMessages: pubsubMessages,
|
||||
streamLag: streamLag,
|
||||
cfg: &cfg,
|
||||
}
|
||||
|
||||
return recorder, nil
|
||||
@@ -384,149 +375,3 @@ func parsePoolName(poolName string) (string, string, string) {
|
||||
host, port := parseAddr(addrPart)
|
||||
return host, port, dbIndex
|
||||
}
|
||||
|
||||
func (o *ObservabilityInstance) registerAsyncCallbacks(meter metric.Meter, recorder *metricsRecorder) error {
|
||||
// Register connection count gauge callback
|
||||
if recorder.connectionCountGauge != nil {
|
||||
_, err := meter.RegisterCallback(
|
||||
func(ctx context.Context, observer metric.Observer) error {
|
||||
recorder.poolsMu.RLock()
|
||||
pools := recorder.pools
|
||||
pubsubPools := recorder.pubsubPools
|
||||
recorder.poolsMu.RUnlock()
|
||||
|
||||
// Iterate over all registered main pools
|
||||
for _, poolInfo := range pools {
|
||||
stats := poolInfo.pool.PoolStats()
|
||||
if stats == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract server info from pool name
|
||||
serverAddr, serverPort, _ := parsePoolName(poolInfo.name)
|
||||
|
||||
// Build base attributes
|
||||
baseAttrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
if serverAddr != "" {
|
||||
baseAttrs = append(baseAttrs, attribute.String(AttrServerAddress, serverAddr))
|
||||
}
|
||||
if serverPort != "" && serverPort != "6379" {
|
||||
baseAttrs = append(baseAttrs, attribute.String(AttrServerPort, serverPort))
|
||||
}
|
||||
|
||||
// Add pool name
|
||||
baseAttrs = append(baseAttrs, attribute.String(AttrDBClientConnectionPoolName, poolInfo.name))
|
||||
|
||||
// Observe idle connections
|
||||
idleAttrs := append([]attribute.KeyValue{}, baseAttrs...)
|
||||
idleAttrs = append(idleAttrs,
|
||||
attribute.String(AttrDBClientConnectionState, ConnectionStateIdle),
|
||||
attribute.Bool(AttrRedisClientConnectionPubSub, false),
|
||||
)
|
||||
observer.ObserveInt64(recorder.connectionCountGauge, int64(stats.IdleConns),
|
||||
metric.WithAttributes(idleAttrs...))
|
||||
|
||||
// Observe used connections
|
||||
usedConns := stats.TotalConns - stats.IdleConns
|
||||
usedAttrs := append([]attribute.KeyValue{}, baseAttrs...)
|
||||
usedAttrs = append(usedAttrs,
|
||||
attribute.String(AttrDBClientConnectionState, ConnectionStateUsed),
|
||||
attribute.Bool(AttrRedisClientConnectionPubSub, false),
|
||||
)
|
||||
observer.ObserveInt64(recorder.connectionCountGauge, int64(usedConns),
|
||||
metric.WithAttributes(usedAttrs...))
|
||||
}
|
||||
|
||||
for _, pubsubPoolInfo := range pubsubPools {
|
||||
stats := pubsubPoolInfo.pool.Stats()
|
||||
if stats == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Build base attributes
|
||||
baseAttrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
attribute.String(AttrDBClientConnectionPoolName, pubsubPoolInfo.name),
|
||||
}
|
||||
|
||||
// PubSub pools report Active connections (not idle/used split
|
||||
// We'll report Active as "used" and 0 as "idle"
|
||||
idleAttrs := append([]attribute.KeyValue{}, baseAttrs...)
|
||||
idleAttrs = append(idleAttrs,
|
||||
attribute.String(AttrDBClientConnectionState, ConnectionStateIdle),
|
||||
attribute.Bool(AttrRedisClientConnectionPubSub, true),
|
||||
)
|
||||
observer.ObserveInt64(recorder.connectionCountGauge, 0,
|
||||
metric.WithAttributes(idleAttrs...))
|
||||
|
||||
usedAttrs := append([]attribute.KeyValue{}, baseAttrs...)
|
||||
usedAttrs = append(usedAttrs,
|
||||
attribute.String(AttrDBClientConnectionState, ConnectionStateUsed),
|
||||
attribute.Bool(AttrRedisClientConnectionPubSub, true),
|
||||
)
|
||||
observer.ObserveInt64(recorder.connectionCountGauge, int64(stats.Active),
|
||||
metric.WithAttributes(usedAttrs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
recorder.connectionCountGauge,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register connection count callback: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Register pending requests gauge callback
|
||||
if recorder.connectionPendingReqsGauge != nil {
|
||||
_, err := meter.RegisterCallback(
|
||||
func(ctx context.Context, observer metric.Observer) error {
|
||||
recorder.poolsMu.RLock()
|
||||
pools := recorder.pools
|
||||
recorder.poolsMu.RUnlock()
|
||||
|
||||
// Iterate over all registered pools
|
||||
for _, poolInfo := range pools {
|
||||
stats := poolInfo.pool.PoolStats()
|
||||
if stats == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract server info from pool name
|
||||
serverAddr, serverPort, _ := parsePoolName(poolInfo.name)
|
||||
|
||||
// Build base attributes
|
||||
baseAttrs := []attribute.KeyValue{
|
||||
attribute.String(AttrDBSystemName, DBSystemRedis),
|
||||
getLibraryVersionAttr(),
|
||||
}
|
||||
if serverAddr != "" {
|
||||
baseAttrs = append(baseAttrs, attribute.String(AttrServerAddress, serverAddr))
|
||||
}
|
||||
if serverPort != "" && serverPort != "6379" {
|
||||
baseAttrs = append(baseAttrs, attribute.String(AttrServerPort, serverPort))
|
||||
}
|
||||
|
||||
// Add pool name
|
||||
baseAttrs = append(baseAttrs, attribute.String(AttrDBClientConnectionPoolName, poolInfo.name))
|
||||
|
||||
// Observe pending requests count
|
||||
observer.ObserveInt64(recorder.connectionPendingReqsGauge, int64(stats.PendingRequests),
|
||||
metric.WithAttributes(baseAttrs...))
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
recorder.connectionPendingReqsGauge,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register pending requests callback: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -85,6 +85,17 @@ type Recorder interface {
|
||||
// consumerGroup: name of the consumer group
|
||||
// consumerName: name of the consumer
|
||||
RecordStreamLag(ctx context.Context, lag time.Duration, cn *pool.Conn, streamName, consumerGroup, consumerName string)
|
||||
|
||||
// RecordConnectionCount records a change in connection count (UpDownCounter)
|
||||
// delta: +1 when connection added, -1 when connection removed
|
||||
// state: connection state (e.g., "idle", "used")
|
||||
// isPubSub: true if this is a PubSub connection
|
||||
RecordConnectionCount(ctx context.Context, delta int, cn *pool.Conn, state string, isPubSub bool)
|
||||
|
||||
// RecordPendingRequests records a change in pending requests (UpDownCounter)
|
||||
// delta: +1 when request starts waiting, -1 when request stops waiting
|
||||
// poolName is passed explicitly because we may not have a connection yet when request starts
|
||||
RecordPendingRequests(ctx context.Context, delta int, cn *pool.Conn, poolName string)
|
||||
}
|
||||
|
||||
type PubSubPooler interface {
|
||||
@@ -193,6 +204,12 @@ func SetGlobalRecorder(r Recorder) {
|
||||
ConnectionClosed: func(ctx context.Context, cn *pool.Conn, reason string, err error) {
|
||||
getRecorder().RecordConnectionClosed(ctx, cn, reason, err)
|
||||
},
|
||||
ConnectionCount: func(ctx context.Context, delta int, cn *pool.Conn, state string, isPubSub bool) {
|
||||
getRecorder().RecordConnectionCount(ctx, delta, cn, state, isPubSub)
|
||||
},
|
||||
PendingRequests: func(ctx context.Context, delta int, cn *pool.Conn, poolName string) {
|
||||
getRecorder().RecordPendingRequests(ctx, delta, cn, poolName)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -246,6 +263,8 @@ func (noopRecorder) RecordPubSubMessage(context.Context, *pool.Conn, string, str
|
||||
|
||||
func (noopRecorder) RecordStreamLag(context.Context, time.Duration, *pool.Conn, string, string, string) {
|
||||
}
|
||||
func (noopRecorder) RecordConnectionCount(context.Context, int, *pool.Conn, string, bool) {}
|
||||
func (noopRecorder) RecordPendingRequests(context.Context, int, *pool.Conn, string) {}
|
||||
|
||||
// RegisterPools registers connection pools with the global recorder.
|
||||
func RegisterPools(connPool pool.Pooler, pubSubPool PubSubPooler, addr string) {
|
||||
|
||||
+252
-61
@@ -101,6 +101,15 @@ var (
|
||||
// Parameters: ctx, cn, reason, err
|
||||
metricConnectionClosedCallback func(ctx context.Context, cn *Conn, reason string, err error)
|
||||
|
||||
// Global metric callback for connection count changes (UpDownCounter)
|
||||
// Parameters: ctx, delta (+1/-1), cn, state, isPubSub
|
||||
metricConnectionCountCallback func(ctx context.Context, delta int, cn *Conn, state string, isPubSub bool)
|
||||
|
||||
// Global metric callback for pending requests changes (UpDownCounter)
|
||||
// Parameters: ctx, delta (+1/-1), cn, poolName
|
||||
// poolName is passed explicitly because we may not have a connection yet when request starts
|
||||
metricPendingRequestsCallback func(ctx context.Context, delta int, cn *Conn, poolName string)
|
||||
|
||||
// errPanicInDial is returned when a panic occurs in the dial function.
|
||||
errPanicInQueuedNewConn = errors.New("panic in queuedNewConn")
|
||||
|
||||
@@ -146,6 +155,17 @@ type MetricCallbacks struct {
|
||||
|
||||
// ConnectionClosed is called when a connection is closed
|
||||
ConnectionClosed func(ctx context.Context, cn *Conn, reason string, err error)
|
||||
|
||||
// ConnectionCount is called when connection count changes (UpDownCounter)
|
||||
// delta: +1 when connection added, -1 when connection removed
|
||||
// state: connection state (e.g., "idle", "used")
|
||||
// isPubSub: true if this is a PubSub connection
|
||||
ConnectionCount func(ctx context.Context, delta int, cn *Conn, state string, isPubSub bool)
|
||||
|
||||
// PendingRequests is called when pending requests count changes (UpDownCounter)
|
||||
// delta: +1 when request starts waiting, -1 when request stops waiting
|
||||
// poolName is passed explicitly because we may not have a connection yet when request starts
|
||||
PendingRequests func(ctx context.Context, delta int, cn *Conn, poolName string)
|
||||
}
|
||||
|
||||
// SetAllMetricCallbacks sets all metric callbacks atomically.
|
||||
@@ -170,6 +190,8 @@ func SetAllMetricCallbacks(callbacks *MetricCallbacks) {
|
||||
metricMaintenanceNotificationCallback = nil
|
||||
metricConnectionWaitTimeCallback = nil
|
||||
metricConnectionClosedCallback = nil
|
||||
metricConnectionCountCallback = nil
|
||||
metricPendingRequestsCallback = nil
|
||||
return
|
||||
}
|
||||
|
||||
@@ -180,6 +202,8 @@ func SetAllMetricCallbacks(callbacks *MetricCallbacks) {
|
||||
metricMaintenanceNotificationCallback = callbacks.MaintenanceNotification
|
||||
metricConnectionWaitTimeCallback = callbacks.ConnectionWaitTime
|
||||
metricConnectionClosedCallback = callbacks.ConnectionClosed
|
||||
metricConnectionCountCallback = callbacks.ConnectionCount
|
||||
metricPendingRequestsCallback = callbacks.PendingRequests
|
||||
}
|
||||
|
||||
// getMetricConnectionStateChangeCallback returns the metric callback for connection state changes.
|
||||
@@ -255,6 +279,22 @@ func getMetricConnectionClosedCallback() func(ctx context.Context, cn *Conn, rea
|
||||
return cb
|
||||
}
|
||||
|
||||
// getMetricConnectionCountCallback returns the metric callback for connection count changes (UpDownCounter).
|
||||
func getMetricConnectionCountCallback() func(ctx context.Context, delta int, cn *Conn, state string, isPubSub bool) {
|
||||
metricCallbackMu.RLock()
|
||||
cb := metricConnectionCountCallback
|
||||
metricCallbackMu.RUnlock()
|
||||
return cb
|
||||
}
|
||||
|
||||
// getMetricPendingRequestsCallback returns the metric callback for pending requests changes (UpDownCounter).
|
||||
func getMetricPendingRequestsCallback() func(ctx context.Context, delta int, cn *Conn, poolName string) {
|
||||
metricCallbackMu.RLock()
|
||||
cb := metricPendingRequestsCallback
|
||||
metricCallbackMu.RUnlock()
|
||||
return cb
|
||||
}
|
||||
|
||||
// Stats contains pool state information and accumulated stats.
|
||||
type Stats struct {
|
||||
Hits uint32 // number of times free connection was found in the pool
|
||||
@@ -513,6 +553,12 @@ func (p *ConnPool) addIdleConn() error {
|
||||
|
||||
p.conns[cn.GetID()] = cn
|
||||
p.idleConns = append(p.idleConns, cn)
|
||||
|
||||
// Record connection count increment (new idle connection from min-idle prewarm)
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil {
|
||||
cb(context.Background(), 1, cn, "idle", false)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -577,9 +623,14 @@ func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Notify metrics: new connection created and idle
|
||||
// All new connections start as "used" metrically. For the miss path in getConn,
|
||||
// this is the final state. For putIdleConn (undelivered conn), a used→idle
|
||||
// transition is emitted when it's added to idleConns.
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, "", MetricStateIdle)
|
||||
cb(ctx, cn, "", MetricStateUsed)
|
||||
}
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil {
|
||||
cb(ctx, 1, cn, "used", false)
|
||||
}
|
||||
|
||||
return cn, nil
|
||||
@@ -758,12 +809,21 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) {
|
||||
}
|
||||
|
||||
// Track pending requests in pool stats
|
||||
// NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly.
|
||||
atomic.AddUint32(&p.stats.PendingRequests, 1)
|
||||
// Record pending request increment (UpDownCounter)
|
||||
// Pass pool name explicitly since we don't have a connection yet
|
||||
poolName := p.cfg.Name
|
||||
if cb := getMetricPendingRequestsCallback(); cb != nil {
|
||||
cb(ctx, 1, nil, poolName)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// Failed to get connection, decrement pending requests
|
||||
atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1
|
||||
// Record pending request decrement on failure
|
||||
if cb := getMetricPendingRequestsCallback(); cb != nil {
|
||||
cb(ctx, -1, nil, poolName)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -801,6 +861,17 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) {
|
||||
|
||||
p.connsMu.Lock()
|
||||
cn, err = p.popIdle()
|
||||
if cn != nil {
|
||||
// Emit idle→used transition inside the lock so Close() sees
|
||||
// consistent state (conn removed from idleConns = "used").
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, MetricStateIdle, MetricStateUsed)
|
||||
}
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil {
|
||||
cb(ctx, -1, cn, "idle", false)
|
||||
cb(ctx, 1, cn, "used", false)
|
||||
}
|
||||
}
|
||||
p.connsMu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
@@ -813,8 +884,8 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) {
|
||||
}
|
||||
|
||||
if !p.isHealthyConn(cn, nowNs) {
|
||||
// Connection was popped from idle pool, so fromState is MetricStateIdle
|
||||
_ = p.CloseConn(ctx, cn, CloseReasonStale, MetricStateIdle)
|
||||
// Connection was already transitioned to MetricStateUsed under the lock above.
|
||||
_ = p.CloseConn(ctx, cn, CloseReasonStale, MetricStateUsed)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -825,12 +896,13 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) {
|
||||
if hookErr != nil || !acceptConn {
|
||||
if hookErr != nil {
|
||||
internal.Logger.Printf(ctx, "redis: connection pool: failed to process idle connection by hook: %v", hookErr)
|
||||
// Connection was popped from idle pool, so fromState is MetricStateIdle
|
||||
_ = p.CloseConn(ctx, cn, CloseReasonHookError, MetricStateIdle)
|
||||
// Connection was already transitioned to MetricStateUsed under the lock above.
|
||||
_ = p.CloseConn(ctx, cn, CloseReasonHookError, MetricStateUsed)
|
||||
} else {
|
||||
internal.Logger.Printf(ctx, "redis: connection pool: conn[%d] rejected by hook, returning to pool", cn.GetID())
|
||||
// Connection is already in MetricStateUsed (transitioned under the lock above).
|
||||
// Return connection to pool without freeing the turn that this Get() call holds.
|
||||
// We use putConnWithoutTurn() to run all the Put hooks and logic without freeing a turn.
|
||||
// putConnWithoutTurn will emit used→idle transition.
|
||||
p.putConnWithoutTurn(ctx, cn)
|
||||
cn = nil
|
||||
}
|
||||
@@ -840,19 +912,17 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) {
|
||||
|
||||
atomic.AddUint32(&p.stats.Hits, 1)
|
||||
|
||||
// Notify metrics: connection moved from idle to used
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, MetricStateIdle, MetricStateUsed)
|
||||
}
|
||||
|
||||
// Record wait time (use cached callback from above)
|
||||
if waitTimeCallback != nil {
|
||||
waitTimeCallback(ctx, waitDuration, cn)
|
||||
}
|
||||
|
||||
// Decrement pending requests (connection acquired successfully)
|
||||
// NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly.
|
||||
atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1
|
||||
// Record pending request decrement (UpDownCounter)
|
||||
if cb := getMetricPendingRequestsCallback(); cb != nil {
|
||||
cb(ctx, -1, cn, poolName)
|
||||
}
|
||||
|
||||
return cn, nil
|
||||
}
|
||||
@@ -873,18 +943,26 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) {
|
||||
// both errors and accept=false mean a hook rejected the connection
|
||||
// this should not happen with a new connection, but we handle it gracefully
|
||||
if err != nil || !acceptConn {
|
||||
// Failed to process connection, discard it
|
||||
internal.Logger.Printf(ctx, "redis: connection pool: failed to process new connection conn[%d] by hook: accept=%v, err=%v", newcn.GetID(), acceptConn, err)
|
||||
// New connection was recorded as "" → MetricStateIdle in newConn, so fromState is MetricStateIdle
|
||||
_ = p.CloseConn(ctx, newcn, CloseReasonHookError, MetricStateIdle)
|
||||
// newConn emitted +1 used; CloseConn will emit -1 used if we own the removal.
|
||||
_ = p.CloseConn(ctx, newcn, CloseReasonHookError, MetricStateUsed)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Record connection creation time metric when hooks are used.
|
||||
// When hookManager is set, ProcessOnGet initializes the connection (AUTH/HELLO),
|
||||
// causing IsInited()=true. This means _getConn() in redis.go will take the
|
||||
// early return path and never reach its create time recording.
|
||||
// When hookManager is nil, _getConn() handles both initialization and create time recording.
|
||||
if dialStartNs := newcn.GetDialStartNs(); dialStartNs > 0 {
|
||||
if cb := GetMetricConnectionCreateTimeCallback(); cb != nil {
|
||||
duration := time.Duration(time.Now().UnixNano() - dialStartNs)
|
||||
cb(ctx, duration, newcn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notify metrics: new connection is created and used
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, newcn, "", MetricStateUsed)
|
||||
}
|
||||
// newConn already emitted +1 used, so no transition needed here.
|
||||
|
||||
// Record wait time (use cached callback from above)
|
||||
if waitTimeCallback != nil {
|
||||
@@ -892,8 +970,11 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) {
|
||||
}
|
||||
|
||||
// Decrement pending requests (connection acquired successfully)
|
||||
// NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly.
|
||||
atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1
|
||||
// Record pending request decrement (UpDownCounter)
|
||||
if cb := getMetricPendingRequestsCallback(); cb != nil {
|
||||
cb(ctx, -1, newcn, poolName)
|
||||
}
|
||||
|
||||
return newcn, nil
|
||||
}
|
||||
@@ -992,14 +1073,24 @@ func (p *ConnPool) putIdleConn(ctx context.Context, cn *Conn) bool {
|
||||
defer p.connsMu.Unlock()
|
||||
|
||||
if p.closed() {
|
||||
_ = cn.Close()
|
||||
// Don't close here — this connection is still in p.conns and Close()
|
||||
// will handle closing it and emitting the correct metric decrements.
|
||||
// We just skip adding it to idleConns.
|
||||
return true
|
||||
}
|
||||
|
||||
// poolSize is increased in newConn
|
||||
p.idleConns = append(p.idleConns, cn)
|
||||
p.idleConnsLen.Add(1)
|
||||
|
||||
// Connection was created as "used" in newConn; transition to idle.
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, MetricStateUsed, MetricStateIdle)
|
||||
}
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil {
|
||||
cb(ctx, -1, cn, "used", false)
|
||||
cb(ctx, 1, cn, "idle", false)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1160,6 +1251,7 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {
|
||||
}
|
||||
|
||||
var shouldCloseConn bool
|
||||
var removedFromPool bool
|
||||
|
||||
if p.cfg.MaxIdleConns == 0 || p.idleConnsLen.Load() < p.cfg.MaxIdleConns {
|
||||
// Hot path optimization: try fast IN_USE → IDLE transition
|
||||
@@ -1184,7 +1276,7 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {
|
||||
case StateClosed:
|
||||
internal.Logger.Printf(ctx, "Unexpected conn[%d] state changed by hook to %v, closing it", cn.GetID(), currentState)
|
||||
shouldCloseConn = true
|
||||
p.removeConnWithLock(cn)
|
||||
removedFromPool = p.removeConnWithLock(cn)
|
||||
default:
|
||||
// Pool as-is
|
||||
internal.Logger.Printf(ctx, "Unexpected conn[%d] state changed by hook to %v, pooling as-is", cn.GetID(), currentState)
|
||||
@@ -1195,34 +1287,73 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {
|
||||
// put them at the opposite end of the queue
|
||||
// Optimization: if we just transitioned to IDLE, we know it's usable - skip the check
|
||||
if !transitionedToIdle && !cn.IsUsable() {
|
||||
if p.cfg.PoolFIFO {
|
||||
p.connsMu.Lock()
|
||||
p.idleConns = append(p.idleConns, cn)
|
||||
p.connsMu.Lock()
|
||||
// Check if Close() already removed this connection from p.conns.
|
||||
// If so, skip the append and metrics — Close() already accounted for it.
|
||||
if _, inPool := p.conns[cn.GetID()]; inPool {
|
||||
if p.cfg.PoolFIFO {
|
||||
p.idleConns = append(p.idleConns, cn)
|
||||
} else {
|
||||
p.idleConns = append([]*Conn{cn}, p.idleConns...)
|
||||
}
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, MetricStateUsed, MetricStateIdle)
|
||||
}
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil {
|
||||
cb(ctx, -1, cn, "used", false)
|
||||
cb(ctx, 1, cn, "idle", false)
|
||||
}
|
||||
p.connsMu.Unlock()
|
||||
p.idleConnsLen.Add(1)
|
||||
} else {
|
||||
p.connsMu.Lock()
|
||||
p.idleConns = append([]*Conn{cn}, p.idleConns...)
|
||||
shouldCloseConn = true
|
||||
p.connsMu.Unlock()
|
||||
}
|
||||
p.idleConnsLen.Add(1)
|
||||
} else if !shouldCloseConn {
|
||||
p.connsMu.Lock()
|
||||
p.idleConns = append(p.idleConns, cn)
|
||||
p.connsMu.Unlock()
|
||||
p.idleConnsLen.Add(1)
|
||||
if _, inPool := p.conns[cn.GetID()]; inPool {
|
||||
p.idleConns = append(p.idleConns, cn)
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, MetricStateUsed, MetricStateIdle)
|
||||
}
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil {
|
||||
cb(ctx, -1, cn, "used", false)
|
||||
cb(ctx, 1, cn, "idle", false)
|
||||
}
|
||||
p.connsMu.Unlock()
|
||||
p.idleConnsLen.Add(1)
|
||||
} else {
|
||||
shouldCloseConn = true
|
||||
p.connsMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Notify metrics: connection moved from used to idle
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, MetricStateUsed, MetricStateIdle)
|
||||
if shouldCloseConn {
|
||||
// Connection was removed (e.g., hook set state to StateClosed).
|
||||
// Only emit if we actually removed it from the map (not already taken by Close()).
|
||||
if removedFromPool {
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, MetricStateUsed, "")
|
||||
}
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil {
|
||||
cb(ctx, -1, cn, "used", false)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
shouldCloseConn = true
|
||||
p.removeConnWithLock(cn)
|
||||
removedFromPool = p.removeConnWithLock(cn)
|
||||
|
||||
// Notify metrics: connection removed (used -> nothing)
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, MetricStateUsed, "")
|
||||
// Only emit if we actually removed it from the map (not already taken by Close()).
|
||||
if removedFromPool {
|
||||
// Notify metrics: connection removed (used -> nothing)
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, MetricStateUsed, "")
|
||||
}
|
||||
// Record connection count decrement (connection removed while in used state)
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil {
|
||||
cb(ctx, -1, cn, "used", false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1231,6 +1362,13 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) {
|
||||
}
|
||||
|
||||
if shouldCloseConn {
|
||||
// Only emit connection closed if we actually owned the removal.
|
||||
// If removedFromPool is false, Close() already emitted connectionClosed for this conn.
|
||||
if removedFromPool {
|
||||
if cb := getMetricConnectionClosedCallback(); cb != nil {
|
||||
cb(ctx, cn, "conn_pool_close", nil)
|
||||
}
|
||||
}
|
||||
_ = p.closeConn(cn)
|
||||
}
|
||||
|
||||
@@ -1258,24 +1396,35 @@ func (p *ConnPool) removeConnInternal(ctx context.Context, cn *Conn, reason erro
|
||||
hookManager.ProcessOnRemove(ctx, cn, reason)
|
||||
}
|
||||
|
||||
p.removeConnWithLock(cn)
|
||||
removed := p.removeConnWithLock(cn)
|
||||
|
||||
if freeTurn {
|
||||
p.freeTurn()
|
||||
}
|
||||
|
||||
// Notify metrics: connection removed (assume from used state)
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, MetricStateUsed, "")
|
||||
// Only emit metric decrements if we actually removed the connection from the map.
|
||||
// If removed is false, Close() already removed it and emitted the -1 delta.
|
||||
if removed {
|
||||
// Notify metrics: connection removed (assume from used state)
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil {
|
||||
cb(ctx, cn, MetricStateUsed, "")
|
||||
}
|
||||
// Record connection count decrement (connection removed, assume from used state)
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil {
|
||||
cb(ctx, -1, cn, "used", false)
|
||||
}
|
||||
}
|
||||
|
||||
// Record connection closed
|
||||
if cb := getMetricConnectionClosedCallback(); cb != nil {
|
||||
reasonStr := "unknown"
|
||||
if reason != nil {
|
||||
reasonStr = reason.Error()
|
||||
// Only emit connection closed if we actually owned the removal.
|
||||
// If removed is false, Close() already emitted connectionClosed for this conn.
|
||||
if removed {
|
||||
if cb := getMetricConnectionClosedCallback(); cb != nil {
|
||||
reasonStr := "unknown"
|
||||
if reason != nil {
|
||||
reasonStr = reason.Error()
|
||||
}
|
||||
cb(ctx, cn, reasonStr, reason)
|
||||
}
|
||||
cb(ctx, cn, reasonStr, reason)
|
||||
}
|
||||
|
||||
_ = p.closeConn(cn)
|
||||
@@ -1291,29 +1440,51 @@ func (p *ConnPool) removeConnInternal(ctx context.Context, cn *Conn, reason erro
|
||||
// - reason: why the connection is being closed (use CloseReason* constants)
|
||||
// - fromState: the metric state the connection was in (use MetricState* constants)
|
||||
func (p *ConnPool) CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error {
|
||||
p.removeConnWithLock(cn)
|
||||
removed := p.removeConnWithLock(cn)
|
||||
|
||||
// Record connection state change: connection is being removed from the specified state
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil && fromState != "" {
|
||||
cb(ctx, cn, fromState, "")
|
||||
// Only emit UpDownCounter decrements if we actually removed the connection.
|
||||
// If removed is false, Close() already removed it and emitted the -1 delta.
|
||||
if removed {
|
||||
// Record connection state change: connection is being removed from the specified state
|
||||
if cb := getMetricConnectionStateChangeCallback(); cb != nil && fromState != "" {
|
||||
cb(ctx, cn, fromState, "")
|
||||
}
|
||||
|
||||
// Record connection count decrement (UpDownCounter) for the state the connection was in
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil && fromState != "" {
|
||||
cb(ctx, -1, cn, fromState, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Record connection closed metric with the specified reason
|
||||
if cb := getMetricConnectionClosedCallback(); cb != nil {
|
||||
cb(ctx, cn, reason, nil)
|
||||
// Only emit connection closed if we actually owned the removal.
|
||||
// If removed is false, Close() already emitted connectionClosed for this conn.
|
||||
if removed {
|
||||
if cb := getMetricConnectionClosedCallback(); cb != nil {
|
||||
cb(ctx, cn, reason, nil)
|
||||
}
|
||||
}
|
||||
|
||||
return p.closeConn(cn)
|
||||
}
|
||||
|
||||
func (p *ConnPool) removeConnWithLock(cn *Conn) {
|
||||
// removeConnWithLock removes a connection from the pool under the connsMu lock.
|
||||
// Returns true if the connection was actually present in p.conns and was removed,
|
||||
// false if it was already gone (e.g., removed by Close()). Callers must use the
|
||||
// return value to decide whether to emit metric decrements — this eliminates the
|
||||
// shutdown race between Close() and concurrent removal paths.
|
||||
func (p *ConnPool) removeConnWithLock(cn *Conn) bool {
|
||||
p.connsMu.Lock()
|
||||
defer p.connsMu.Unlock()
|
||||
p.removeConn(cn)
|
||||
return p.removeConn(cn)
|
||||
}
|
||||
|
||||
func (p *ConnPool) removeConn(cn *Conn) {
|
||||
// removeConn removes a connection from the pool's internal data structures.
|
||||
// Returns true if the connection was present and removed, false otherwise.
|
||||
func (p *ConnPool) removeConn(cn *Conn) bool {
|
||||
cid := cn.GetID()
|
||||
if _, exists := p.conns[cid]; !exists {
|
||||
return false
|
||||
}
|
||||
delete(p.conns, cid)
|
||||
atomic.AddUint32(&p.stats.StaleConns, 1)
|
||||
|
||||
@@ -1329,6 +1500,7 @@ func (p *ConnPool) removeConn(cn *Conn) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *ConnPool) closeConn(cn *Conn) error {
|
||||
@@ -1401,7 +1573,26 @@ func (p *ConnPool) Close() error {
|
||||
|
||||
var firstErr error
|
||||
p.connsMu.Lock()
|
||||
|
||||
// Emit -1 for each connection. Since all idle↔used transitions happen
|
||||
// under connsMu, the idleConns slice is the source of truth for state.
|
||||
cb := getMetricConnectionCountCallback()
|
||||
idleSet := make(map[uint64]struct{}, len(p.idleConns))
|
||||
for _, cn := range p.idleConns {
|
||||
idleSet[cn.GetID()] = struct{}{}
|
||||
}
|
||||
ctx := context.Background()
|
||||
for _, cn := range p.conns {
|
||||
if cb != nil {
|
||||
if _, isIdle := idleSet[cn.GetID()]; isIdle {
|
||||
cb(ctx, -1, cn, "idle", false)
|
||||
} else {
|
||||
cb(ctx, -1, cn, "used", false)
|
||||
}
|
||||
}
|
||||
if closedCb := getMetricConnectionClosedCallback(); closedCb != nil {
|
||||
closedCb(ctx, cn, "pool_shutdown", nil)
|
||||
}
|
||||
if err := p.closeConn(cn); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
|
||||
+25
-1
@@ -53,18 +53,42 @@ func (p *PubSubPool) NewConn(ctx context.Context, network string, addr string, c
|
||||
func (p *PubSubPool) TrackConn(cn *Conn) {
|
||||
atomic.AddUint32(&p.stats.Active, 1)
|
||||
p.activeConns.Store(cn.GetID(), cn)
|
||||
// Emit +1 used for PubSub connection
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil {
|
||||
cb(context.Background(), 1, cn, "used", true)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PubSubPool) UntrackConn(cn *Conn) {
|
||||
// LoadAndDelete ensures each connection is only decremented once,
|
||||
// guarding against double-decrement if Close() already untracked it.
|
||||
if _, loaded := p.activeConns.LoadAndDelete(cn.GetID()); !loaded {
|
||||
return
|
||||
}
|
||||
atomic.AddUint32(&p.stats.Active, ^uint32(0))
|
||||
atomic.AddUint32(&p.stats.Untracked, 1)
|
||||
p.activeConns.Delete(cn.GetID())
|
||||
// Emit -1 used for PubSub connection
|
||||
if cb := getMetricConnectionCountCallback(); cb != nil {
|
||||
cb(context.Background(), -1, cn, "used", true)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PubSubPool) Close() error {
|
||||
p.closed.Store(true)
|
||||
cb := getMetricConnectionCountCallback()
|
||||
p.activeConns.Range(func(key, value interface{}) bool {
|
||||
cn := value.(*Conn)
|
||||
// Use LoadAndDelete to atomically claim ownership of this entry.
|
||||
// If a concurrent UntrackConn already removed it, skip to avoid double-decrement.
|
||||
if _, loaded := p.activeConns.LoadAndDelete(key); !loaded {
|
||||
return true
|
||||
}
|
||||
atomic.AddUint32(&p.stats.Active, ^uint32(0))
|
||||
atomic.AddUint32(&p.stats.Untracked, 1)
|
||||
// Emit -1 used for each PubSub connection being closed
|
||||
if cb != nil {
|
||||
cb(context.Background(), -1, cn, "used", true)
|
||||
}
|
||||
_ = cn.Close()
|
||||
return true
|
||||
})
|
||||
|
||||
@@ -79,6 +79,25 @@ type OTelRecorder interface {
|
||||
RecordStreamLag(ctx context.Context, lag time.Duration, cn ConnInfo, streamName, consumerGroup, consumerName string)
|
||||
}
|
||||
|
||||
// OTelConnectionCounter is an optional capability interface for recording
|
||||
// connection count and pending request changes via UpDownCounters.
|
||||
// Implementations of OTelRecorder can optionally implement this interface
|
||||
// to receive connection count and pending request delta notifications.
|
||||
// This is kept separate from OTelRecorder to avoid breaking existing
|
||||
// third-party implementations when new methods are added.
|
||||
type OTelConnectionCounter interface {
|
||||
// RecordConnectionCount records a change in connection count (UpDownCounter)
|
||||
// delta: +1 when connection added, -1 when connection removed
|
||||
// state: connection state (e.g., "idle", "used")
|
||||
// isPubSub: true if this is a PubSub connection
|
||||
RecordConnectionCount(ctx context.Context, delta int, cn ConnInfo, state string, isPubSub bool)
|
||||
|
||||
// RecordPendingRequests records a change in pending requests (UpDownCounter)
|
||||
// delta: +1 when request starts waiting, -1 when request stops waiting
|
||||
// poolName is passed explicitly because we may not have a connection yet when request starts
|
||||
RecordPendingRequests(ctx context.Context, delta int, cn ConnInfo, poolName string)
|
||||
}
|
||||
|
||||
// This is used for async gauge metrics that need to pull stats from pools periodically.
|
||||
type OTelPoolRegistrar interface {
|
||||
// RegisterPool is called when a new client is created with its main connection pool.
|
||||
@@ -163,6 +182,18 @@ func (a *otelRecorderAdapter) RecordStreamLag(ctx context.Context, lag time.Dura
|
||||
a.recorder.RecordStreamLag(ctx, lag, toConnInfo(cn), streamName, consumerGroup, consumerName)
|
||||
}
|
||||
|
||||
func (a *otelRecorderAdapter) RecordConnectionCount(ctx context.Context, delta int, cn *pool.Conn, state string, isPubSub bool) {
|
||||
if counter, ok := a.recorder.(OTelConnectionCounter); ok {
|
||||
counter.RecordConnectionCount(ctx, delta, toConnInfo(cn), state, isPubSub)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *otelRecorderAdapter) RecordPendingRequests(ctx context.Context, delta int, cn *pool.Conn, poolName string) {
|
||||
if counter, ok := a.recorder.(OTelConnectionCounter); ok {
|
||||
counter.RecordPendingRequests(ctx, delta, toConnInfo(cn), poolName)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *otelRecorderAdapter) RegisterPool(poolName string, p pool.Pooler) {
|
||||
if registrar, ok := a.recorder.(OTelPoolRegistrar); ok {
|
||||
registrar.RegisterPool(poolName, &poolerAdapter{p})
|
||||
|
||||
Reference in New Issue
Block a user