Skip to content

Commit fe97b35

Browse files
ryanyuansean-
andauthored
feat(opensearchtransport)!: remove EnableMetrics, reimplement deep metrics with lock-free structures (#901)
* feat(opensearchtransport)!: remove EnableMetrics, make detailed metrics always-on and lock-free (#892) Remove EnableMetrics from the client and transport Config. Detailed metrics are now always collected; Metrics() always returns the full snapshot. (BREAKING) Convert deadSince/overloadedAt from mu-guarded time.Time to lock-free atomic.Int64 (UnixNano, 0 = unset). Writes still occur under c.mu so the resurrection/standby read-modify-write decisions stay serialized; only the reads went lock-free, so buildConnectionMetric no longer takes each connection's mutex. This eliminates the #1 explicit-lock contention site measured under concurrent Perform() load. Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> * docs(opensearchtransport): disambiguate uninitialized-metrics error The defensive nil-metrics path returned "transport metrics not enabled", recycling the removed EnableMetrics concept and contradicting the Metrics() godoc that the error is non-nil only on snapshot-callback failure. Reword the error to "transport metrics not initialized", state both error conditions in the godoc, and align the test comment. Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> * test(opensearchtransport): drop EnableMetrics references from comments The test comments narrated the removed EnableMetrics flag as history. Describe the end-state behavior instead: callbacks register when a router has policies, and the detailed snapshot runs unconditionally. Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> * fix(opensearchtransport): lock pool write in RolePolicy.DiscoveryUpdate RolePolicy.DiscoveryUpdate called recalculateWarmupParams without holding the pool write lock, while the roundrobin and cluster_coordinator policies took pool.Lock() for the identical call. recalculateWarmupParams writes the pool's warmupRounds, warmupSkipCount, and activeListCap fields, which getWarmupParams and the other DiscoveryUpdate callers read and write under that same lock. Two concurrent DiscoverNodes calls on a shared transport therefore raced on those fields (observed in CI: two goroutines writing in recalculateWarmupParams via RolePolicy.DiscoveryUpdate). This is pre-existing on main, unrelated to the EnableMetrics removal. Compute the projected pool size and recalculate the warmup parameters under pool.Lock(), releasing before discoveryUpdateAdd/Remove (which acquire the lock per-connection). Add TestRolePolicyDiscoveryUpdateConcurrent, which reproduces the race under -race and passes with the fix. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com> --------- Signed-off-by: Ryan Yuan <ryan.yuan@crowdstrike.com> Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com> Co-authored-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
1 parent 2ca7893 commit fe97b35

41 files changed

Lines changed: 673 additions & 238 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
167167
### Changed
168168

169169
- **BREAKING**: Per-request transport metrics (`requests`, `failures`, responses-by-status) are now always collected via lock-free atomics, independent of `EnableMetrics`. `EnableMetrics` now gates only the detailed-metrics snapshot (per-connection, per-policy, and router state returned by `Metrics()`). The responses-by-status counter moved from a mutex-guarded map to a lock-free atomic array. `Metrics()` no longer returns an error when metrics are disabled -- it always returns the per-request counters (callers that branched on `if err != nil` for the disabled case should drop that check). See [`UPGRADING_V5.md`](UPGRADING_V5.md#metrics-error-on-disabled-removed) for migration. ([#891](https://github.com/opensearch-project/opensearch-go/issues/891))
170+
- Make the detailed-metrics snapshot path lock-free at call time. The per-connection `deadSince`/`overloadedAt` timestamps moved from `Connection.mu`-guarded `time.Time` fields to lock-free atomic Unix-nanosecond values, so `Metrics()` enumerates connections without taking each connection's mutex. Under concurrent request load this was the dominant lock-contention site (a mutex profile attributed ~3.85% of total contention delay to the snapshot reader taking a write lock merely to read two fields); the conversion drops that to ~0.1%. Writes still occur under `Connection.mu` so the resurrection/standby read-modify-write decisions stay serialized. Benchmarks (`BenchmarkMetrics`, `BenchmarkMetricsParallel`, `BenchmarkMetricsUnderLoad`) confirm the always-on detailed path is acceptable. ([#892](https://github.com/opensearch-project/opensearch-go/issues/892))
170171
- Reorganize the documentation. Split `UPGRADING.md` into a version-history index plus per-major-version guides (`UPGRADING_V5.md` through `UPGRADING_V2.md`) and rename `opensearchapi/MIGRATING.md` to `opensearchapi/UPGRADING_V4_TO_V5.md`. Group the `guides/` and `_samples/` files by subsystem (`transport-`, `indexing-`, `usage-`, `config-`) and add a `guides/README.md` index. Make `guides/usage-error_handling.md` the single source for partial-error handling and `guides/transport-retry_backoff.md` the single source for resurrection-timeout config, replacing the duplicated copies in `opensearchapi/README.md` and `guides/transport-routing.md` with links. Add package documentation (`doc.go`) for `opensearchapi`, `plugins`, `signer`, and `signer/awsv2`.
171172
- Trim the CI compatibility matrix to the currently-patched OpenSearch set (2.19.x and 3.x) per the 12-month support policy; older lines (1.3.x - 2.18.x) are no longer part of the tested matrix and the 4.x client remains their supported path. No client code change ([#856](https://github.com/opensearch-project/opensearch-go/issues/856))
172173
- **BREAKING**: Module path is now `github.com/opensearch-project/opensearch-go/v5`. Update import paths from `/v4` to `/v5`; the in-source `opensearchapi.X` package qualifier is unchanged
@@ -219,11 +220,13 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
219220
### Removed
220221

221222
- Remove deprecated `(*opensearch.Client).Perform` and `(*opensearchtransport.Transport).Perform`; `Stream(*http.Request) (*http.Response, error)` is now the sole method on `opensearchtransport.Interface`. Custom transport implementations must implement `Stream` instead of `Perform`. The `opensearch.Streamer` opt-in interface and `opensearch.ErrTransportMissingMethodStream` sentinel are removed. ([#872](https://github.com/opensearch-project/opensearch-go/issues/872))
223+
- **BREAKING**: Remove the `EnableMetrics` config flag from `opensearch.Config` and `opensearchtransport.Config`. The detailed-metrics snapshot (per-connection enumeration, per-policy breakdowns, and router cache state) is now always available; `Metrics()` returns the full snapshot unconditionally. The flag's only remaining purpose after [#891](https://github.com/opensearch-project/opensearch-go/issues/891) was to gate the detailed path, which now does its work lazily and lock-free at call time and so costs nothing until `Metrics()` is called. Delete any `EnableMetrics` field from your config (it is a compile error otherwise); see [`UPGRADING_V5.md`](UPGRADING_V5.md#enablemetrics-removed). ([#892](https://github.com/opensearch-project/opensearch-go/issues/892))
222224
- Remove backport.yml and dependabot_pr.yml as we are not using backport app anymore
223225
- Stop emitting `opensearchapi.Client` sub-client fields that have no operations routed to them. `cmd/osgen` now emits a sub-client only when at least one operation targets it, dropping the previously-empty `Script`, `ComponentTemplate`, `IndexTemplate`, `Template`, and `DataStream` fields. Index-template and data-stream operations are reached through `client.Indices.*` (e.g. `client.Indices.PutIndexTemplate`, `client.Indices.CreateDataStream`); stored-script operations remain top-level on `Client`
224226

225227
### Fixed
226228

229+
- Fix a data race on the multi-server pool's `warmupRounds`, `warmupSkipCount`, and `activeListCap` fields when two concurrent `DiscoverNodes` calls drive `RolePolicy.DiscoveryUpdate` on a shared transport. `RolePolicy` called `recalculateWarmupParams` (which writes those fields) without holding the pool write lock, while the `roundrobin` and `cluster_coordinator` policies took the lock for the identical call. `RolePolicy.DiscoveryUpdate` now computes the projected pool size and recalculates the warmup parameters under `pool.Lock()`, matching the other callers
227230
- Cache credentials in the `signer/awsv2` constructors. A raw `CredentialsProvider` is wrapped in an `aws.CredentialsCache` (an already-cached provider, such as one from `config.LoadDefaultConfig`, is left as-is), so SigV4 signing no longer calls `Credentials.Retrieve` on every request. For STS-backed providers (assume-role, web identity, IRSA) the previous behavior was a per-request STS call that could exhaust the account's STS rate limits under load. `signer/awsv2` shipped without this in v4.6.0.
228231
- Fix `cmd/osgen` silently dropping a response struct when a response schema has a `oneOf`/`anyOf` field whose parent-scoped union name collides with the parent struct's own Go name. The union registered first and the parent struct was then dropped by the type registry (its name already taken), degrading the response to raw `json.RawMessage`. Such a union is now re-keyed by its referenced schema so the parent struct survives. The generator also reports any remaining Go type name collisions to stderr at generation time instead of dropping types silently. Regenerating fixes two type families: `tasks.list`, `tasks.cancel`, and `delete_by_query_rethrottle` change from raw `Body json.RawMessage` to typed structs (`NodeFailures`, `TaskFailures`, `Nodes map[string]TasksTaskExecutingNode`, `Tasks *TasksTaskInfos`), and the `_common.mapping___DynamicTemplate.mapping` field becomes typed `*CommonMappingProperty` (accounting for the large `unions_gen.go`/`indices-put_mapping_gen.go` churn). ([#890](https://github.com/opensearch-project/opensearch-go/pull/890))
229232
- Fix `cmd/osgen` degrading two more schema shapes to raw `json.RawMessage`: an OpenAPI 3.1 nullable scalar (`type: ["null", "<primitive>"]`) fell through because kin-openapi's `Type.Is` matches only single-element type sets, and a response whose component schema is a bare `$ref` alias (`Foo: {$ref: Bar}`) missed the registry lookup under its alias key. Nullable scalars now resolve to the pointer primitive (`*string`/`*int`/`*bool`/`*float64`), clearing the CAT `*Record` cluster, and alias responses follow the `$ref` chain to the registered struct, fixing ISM `add`/`delete`/`get`/`remove_policy` + `retry_index` and the seven `ml.search_*` responses. ([#890](https://github.com/opensearch-project/opensearch-go/pull/890))

UPGRADING_V5.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ use(m.Requests, m.Failures)
124124

125125
Detailed fields such as `Policies` and `Router` remain populated only when `EnableMetrics` is set; reading them without it yields nil, unchanged from v4.
126126

127+
> Note: a later v5 change removed `EnableMetrics` entirely -- see [`EnableMetrics` removed](#enablemetrics-removed) below. The behavior described above was the intermediate state; in the shipped v5 the detailed fields are always populated.
128+
129+
## `EnableMetrics` removed
130+
131+
`EnableMetrics` has been removed from both `opensearch.Config` and `opensearchtransport.Config`. The detailed-metrics snapshot (per-connection enumeration, per-policy breakdowns, and router cache state) is now always available -- it is assembled lazily and lock-free at the moment you call `Metrics()`, so it adds no per-request cost. The per-request counters were already always-on.
132+
133+
Delete any `EnableMetrics` field from your config; leaving it in place is a compile error.
134+
135+
`Metrics()` now returns the full snapshot unconditionally, including `Connections`, `Policies`, and `Router` (the latter two populate when a router with policies is active). The returned error is still non-nil only when a snapshot callback fails.
136+
127137
## `opensearchtransport.Client` renamed to `opensearchtransport.Transport`
128138

129139
The concrete `opensearchtransport.Client` type was renamed to `opensearchtransport.Transport`. The type owns HTTP round-trip concerns -- connection pooling, retries, node selection, and discovery -- so `Transport` reflects its role and avoids colliding conceptually with the API clients above it (`opensearch.Client` and `opensearchapi.Client`).

_samples/transport-discovery_demo.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,7 @@ func main() {
4646
{Scheme: "http", Host: "localhost:9201"},
4747
},
4848
DiscoverNodesInterval: 5 * time.Second, // Fast for demo
49-
EnableMetrics: true,
50-
EnableDebugLogger: !isCI(), // Enable locally, disable in CI
49+
EnableDebugLogger: !isCI(), // Enable locally, disable in CI
5150
Logger: &opensearchtransport.ColorLogger{Output: os.Stdout, EnableRequestBody: false, EnableResponseBody: false},
5251
Router: router,
5352

guides/transport-metrics.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@
22

33
The opensearch-go transport exposes a pull-based metrics API that returns a point-in-time snapshot of request counters, connection pool state, per-connection health, policy-level breakdowns, and router cache state. All fields are JSON-tagged for easy serialization.
44

5-
Metrics come in two tiers. The **per-request counters** (`requests`, `failures`, and responses-by-status) are always collected via lock-free atomics and returned by `Metrics()` regardless of configuration. The **detailed-metrics snapshot** (connection-pool state, per-connection health, per-policy breakdowns, and router cache state) is opt-in behind `EnableMetrics`.
5+
Metrics are collected using atomic, per-request counters (e.g. `requests`, `failures`, and responses-by-status) recorded on the request hot path. Additional detailed metrics (connection-pool state, per-connection health, per-policy breakdowns, and router cache state) are lazily accumulated and read lock-free when you call `Metrics()`, so they cost nothing until you ask for them.
66

77
## Quick Start
88

9-
The per-request counters require no configuration. To also populate the detailed-metrics snapshot, set `EnableMetrics: true` on `opensearch.Config`. When constructing through `opensearchapi.NewClient`, set the flag on the embedded `opensearch.Config` and reach the method via `apiClient.Client.Metrics()`.
9+
Metrics require no configuration. Construct a client and call `Metrics()`. When constructing through `opensearchapi.NewClient`, reach the method via `apiClient.Client.Metrics()`.
1010

1111
```go
1212
client, err := opensearch.NewClient(opensearch.Config{
13-
Addresses: []string{"https://localhost:9200"},
14-
EnableMetrics: true,
13+
Addresses: []string{"https://localhost:9200"},
1514
})
1615
if err != nil {
1716
log.Fatal(err)
@@ -26,7 +25,7 @@ data, _ := json.MarshalIndent(m, "", " ")
2625
fmt.Println(string(data))
2726
```
2827

29-
The `Metrics()` method lives on `opensearch.Client`. It returns an `opensearchtransport.Metrics` struct and an error -- non-nil when a detailed-metrics snapshot callback fails. A `New()`-constructed transport always returns the per-request counters, so `Metrics()` does not error merely because `EnableMetrics` is unset.
28+
The `Metrics()` method lives on `opensearch.Client`. It returns an `opensearchtransport.Metrics` struct and an error -- non-nil only when a detailed-snapshot callback fails. It never errors merely because metrics are "disabled"; they are always available.
3029

3130
---
3231

opensearch.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,6 @@ type Config struct {
188188
// Default: nil (no modification)
189189
HealthCheckRequestModifier func(*http.Request)
190190

191-
EnableMetrics bool // Enable the metrics collection.
192191
EnableDebugLogger bool // Enable the debug logging.
193192

194193
// ActiveListCap sets the maximum number of connections in the ready list's active partition per pool.
@@ -346,7 +345,6 @@ func NewClient(cfg Config) (*Client, error) {
346345

347346
CompressRequestBody: cfg.CompressRequestBody,
348347

349-
EnableMetrics: cfg.EnableMetrics,
350348
EnableDebugLogger: cfg.EnableDebugLogger,
351349

352350
DiscoverNodesInterval: cfg.DiscoverNodesInterval,

opensearch_internal_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ func TestVersion(t *testing.T) {
569569
}
570570

571571
func TestClientMetrics(t *testing.T) {
572-
c, _ := NewClient(Config{EnableMetrics: true, Transport: mockhttp.NewRoundTripFunc(t, defaultRoundTripFunc)})
572+
c, _ := NewClient(Config{Transport: mockhttp.NewRoundTripFunc(t, defaultRoundTripFunc)})
573573

574574
m, err := c.Metrics()
575575
require.NoError(t, err)

opensearchtransport/address_resolver_internal_test.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,6 @@ func TestAddressResolver(t *testing.T) {
208208
tp, err := New(Config{
209209
URLs: []*url.URL{testSeedURL(t)},
210210
Transport: newResolverTestTransport(t, nodesJSON),
211-
EnableMetrics: true,
212211
HealthCheck: NoOpHealthCheck,
213212
AddressResolver: tt.resolver,
214213
MaxAddressResolvers: 1, // serial for deterministic behavior
@@ -719,10 +718,9 @@ func TestAddressResolverRunner(t *testing.T) {
719718

720719
var resolverCalled atomic.Int32
721720
tp, err := New(Config{
722-
URLs: []*url.URL{testSeedURL(t)},
723-
Transport: newResolverTestTransport(t, nodesJSON),
724-
EnableMetrics: true,
725-
HealthCheck: NoOpHealthCheck,
721+
URLs: []*url.URL{testSeedURL(t)},
722+
Transport: newResolverTestTransport(t, nodesJSON),
723+
HealthCheck: NoOpHealthCheck,
726724
AddressResolver: func(_ context.Context, _ NodeInfo) (*url.URL, error) {
727725
resolverCalled.Add(1)
728726
return nil, nil //nolint:nilnil // testing (nil, nil) protocol case
@@ -1143,7 +1141,6 @@ func TestAddressResolverRunnerProtocol(t *testing.T) {
11431141
tp, err := New(Config{
11441142
URLs: []*url.URL{testSeedURL(t)},
11451143
Transport: newResolverTestTransport(t, nodesJSON),
1146-
EnableMetrics: true,
11471144
HealthCheck: NoOpHealthCheck,
11481145
AddressResolver: tt.resolver,
11491146
MaxAddressResolvers: 1,

opensearchtransport/cluster_health_internal_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1382,7 +1382,7 @@ func TestFetchAndEvaluateNodeStats(t *testing.T) {
13821382
conn := &Connection{URL: serverURL}
13831383
conn.state.Store(int64(newConnState(lcStandby | lcOverloaded)))
13841384
conn.mu.Lock()
1385-
conn.mu.overloadedAt = time.Now()
1385+
conn.storeOverloadedAt(time.Now())
13861386
conn.mu.Unlock()
13871387

13881388
a1 := newActiveConn("a1")

opensearchtransport/connection.go

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,14 @@ type Connection struct {
187187
failures atomic.Int64
188188
state atomic.Int64 // Packed connState: connLifecycle (12b) + 2*warmupManager (26b each)
189189

190+
// deadSinceNano and overloadedAtNano hold Unix-nanosecond timestamps, with 0
191+
// meaning "unset" (the zero time). They are read lock-free by Metrics() and
192+
// written under c.mu; see c.mu for the locking protocol. Use the
193+
// loadDeadSince/storeDeadSince/deadSinceIsZero accessors (and the overloadedAt
194+
// equivalents) rather than touching these directly.
195+
deadSinceNano atomic.Int64
196+
overloadedAtNano atomic.Int64
197+
190198
// drainingQuiescingRemaining counts the number of successful health checks remaining
191199
// before this connection can be resurrected. Set to defaultDrainingQuiescingChecks when
192200
// an HTTP/2 stream reset is observed (RST_STREAM, e.g., REFUSED_STREAM). Each successful
@@ -198,13 +206,14 @@ type Connection struct {
198206
// defaultDrainingQuiescingChecks * resurrectTimeout.
199207
drainingQuiescingRemaining atomic.Int64
200208

209+
// mu guards the fields below and serializes the resurrection/standby
210+
// read-modify-write decisions. The deadSinceNano/overloadedAtNano atomics are
211+
// written under mu but read lock-free.
201212
mu struct {
202213
sync.RWMutex
203-
deadSince time.Time
204214
checkStartedAt time.Time
205215
clusterHealth *ClusterHealthLocal // Populated when lcClusterHealthAvailable is set
206216
clusterHealthCheckedAt time.Time // When cluster health was last probed (for retry timing)
207-
overloadedAt time.Time // When overloaded state was last set (lcOverloaded metadata bit)
208217
lastBreakerTripped map[string]int64 // Previous tripped counts for delta detection
209218
}
210219

@@ -226,6 +235,44 @@ type Connection struct {
226235
}
227236
}
228237

238+
// timeToNano converts a time.Time to its Unix-nanosecond representation. The
239+
// zero value is preserved to imply "unset."
240+
func timeToNano(t time.Time) int64 {
241+
if t.IsZero() {
242+
return 0
243+
}
244+
return t.UnixNano()
245+
}
246+
247+
// nanoToTime converts a stored Unix-nanosecond value back to time.Time in UTC,
248+
// mapping the 0 sentinel to the zero time.
249+
func nanoToTime(n int64) time.Time {
250+
if n == 0 {
251+
return time.Time{}
252+
}
253+
return time.Unix(0, n).UTC()
254+
}
255+
256+
// loadDeadSince returns the time the connection was marked dead, or the zero
257+
// time if it is alive. Lock-free.
258+
func (c *Connection) loadDeadSince() time.Time { return nanoToTime(c.deadSinceNano.Load()) }
259+
260+
// storeDeadSince records (or clears, with the zero time) the dead timestamp.
261+
// Callers hold c.mu; see c.mu for the locking protocol.
262+
func (c *Connection) storeDeadSince(t time.Time) { c.deadSinceNano.Store(timeToNano(t)) }
263+
264+
// deadSinceIsZero reports whether the connection is alive (no dead timestamp).
265+
// Lock-free.
266+
func (c *Connection) deadSinceIsZero() bool { return c.deadSinceNano.Load() == 0 }
267+
268+
// loadOverloadedAt returns the time the connection was last marked overloaded,
269+
// or the zero time. Lock-free.
270+
func (c *Connection) loadOverloadedAt() time.Time { return nanoToTime(c.overloadedAtNano.Load()) }
271+
272+
// storeOverloadedAt records (or clears, with the zero time) the overloaded
273+
// timestamp. Callers hold c.mu; see c.mu for the locking protocol.
274+
func (c *Connection) storeOverloadedAt(t time.Time) { c.overloadedAtNano.Store(timeToNano(t)) }
275+
229276
// effectiveWeight returns the connection's weight for round-robin selection.
230277
// Returns 1 if weight is zero (default for connections created without explicit weight).
231278
func (c *Connection) effectiveWeight() int {
@@ -274,20 +321,20 @@ func (c *Connection) decrementDrainingQuiescing() int64 {
274321

275322
// markAsDeadWithLock marks the connection as dead (caller must hold lock).
276323
func (c *Connection) markAsDeadWithLock() {
277-
if c.mu.deadSince.IsZero() {
278-
c.mu.deadSince = time.Now().UTC()
324+
if c.deadSinceIsZero() {
325+
c.storeDeadSince(time.Now().UTC())
279326
}
280327
c.failures.Add(1)
281328
}
282329

283330
// markAsReadyWithLock marks the connection as alive (caller must hold lock).
284331
func (c *Connection) markAsReadyWithLock() {
285-
c.mu.deadSince = time.Time{}
332+
c.storeDeadSince(time.Time{})
286333
}
287334

288335
// markAsHealthyWithLock marks the connection as healthy (caller must hold lock).
289336
func (c *Connection) markAsHealthyWithLock() {
290-
c.mu.deadSince = time.Time{}
337+
c.storeDeadSince(time.Time{})
291338
c.failures.Store(0)
292339
}
293340

@@ -456,9 +503,7 @@ func (c *Connection) storeMaxCwnd(poolName string, size int) {
456503

457504
// String returns a readable connection representation.
458505
func (c *Connection) String() string {
459-
c.mu.RLock()
460-
deadAt := c.mu.deadSince
461-
c.mu.RUnlock()
506+
deadAt := c.loadDeadSince()
462507

463508
if deadAt.IsZero() {
464509
return fmt.Sprintf("<%s> dead=false failures=%d", c.URL, c.failures.Load())

opensearchtransport/connection_benchmark_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func createMultiServerPool(conns []*Connection) *multiServerPool {
160160
for _, conn := range ready {
161161
conn.state.Store(int64(newConnState(lcActive)))
162162
conn.mu.Lock()
163-
conn.mu.deadSince = time.Time{} // Reset from prior benchmark sub-runs
163+
conn.storeDeadSince(time.Time{}) // Reset from prior benchmark sub-runs
164164
conn.mu.Unlock()
165165
}
166166
pool.mu.ready = ready

0 commit comments

Comments
 (0)