Skip to content

Commit 429f325

Browse files
committed
refactor(opensearchtransport)!: drop IncludeDedicatedClusterManagers, exclude DCMs from routing unconditionally
BREAKING CHANGE: remove the IncludeDedicatedClusterManagers config flag from opensearch.Config and opensearchtransport.Config. v5 no longer offers an opt-in for routing to dedicated cluster managers. They are kept in the connection inventory (allConns) for discovery reuse and eviction, but never selected for request traffic. A connection is skipped during selection when it is a dedicated cluster manager unless it is a user-supplied seed, so discovery still bootstraps against a dedicated-cluster-manager seed via the seed-fallback pool. This replaces the flag-gated excludeDCM pool field with an intrinsic conn.seed check, removing the per-pool bool and the policyConfig plumbing. RoundRobinPolicy now excludes dedicated cluster managers from its DiscoveryUpdate add path unconditionally. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
1 parent ca4569f commit 429f325

12 files changed

Lines changed: 179 additions & 131 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,13 +236,15 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
236236

237237
### Removed
238238

239+
- **BREAKING**: Remove the `IncludeDedicatedClusterManagers` config flag from `opensearch.Config` and `opensearchtransport.Config`. Dedicated cluster managers (`cluster_manager` role with no work roles) are now unconditionally excluded from request routing: they are kept in the connection inventory for discovery reuse/eviction but never selected for query traffic (a user-supplied seed remains selectable so discovery can bootstrap against it). The flag previously defaulted to excluding them; opting them into routing is no longer supported. Delete any `IncludeDedicatedClusterManagers` field from your config (it is a compile error otherwise) ([#1004](https://github.com/opensearch-project/opensearch-go/pull/1004))
239240
- 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))
240241
- **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))
241242
- Remove backport.yml and dependabot_pr.yml as we are not using backport app anymore
242243
- 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`
243244

244245
### Fixed
245246

247+
- Fix an unbounded connection/heap leak in node discovery when the cluster has a dedicated cluster manager (`cluster_manager` role with no work roles). The node was filtered out of the `allConns` inventory while the router received the unfiltered added/removed diffs, so `findConnectionByURL` never matched it: a new `*Connection` was created every discovery cycle and the stale one was never evicted, accumulating without bound in the round-robin fallback pool whose `checkDead` health checks repopulated a per-connection `poolRegistry` `sync.Map` each cycle (leak rate scaled with discovery frequency). `allConns` is now the full connection inventory so discovery reuses and evicts symmetrically, and dedicated cluster managers are excluded at request-routing selection instead: `RoundRobinPolicy` skips them in its `DiscoveryUpdate` add path and `multiServerPool.Next()` skips non-seed dedicated cluster managers during selection (including the no-router fallback). A user-supplied seed is exempt, so discovery still bootstraps against a dedicated cluster manager seed. See also the `IncludeDedicatedClusterManagers` removal under Removed ([#1004](https://github.com/opensearch-project/opensearch-go/pull/1004))
246248
- Fix `cmd/osgen` silently dropping typed structs on Go type-name collisions, and add a completeness guard so future collisions fail generation instead of degrading output. Two distinct spec schemas that derived the same Go name were reduced to one by the type registry, dropping the other to raw `json.RawMessage` (or mis-typing a field): the search `profile` container collided with the per-search `SearchProfile` item, and multiple response bodies in the `flow_framework.common` and `security_analytics.findings` groups all derived `<Group>Resp`. Colliding refs are now disambiguated via documented override tables (`typeNameCollisions` / `respTypeNameCollisions`), and a panic-guard requires any new colliding ref to be enumerated. Regenerating restores typed responses for the affected operations (e.g. flow_framework `search`/`search_state`, security_analytics `search_finding_correlations`, and the search response's `profile` field), and a response schema also referenced structurally (a search hit's `_source`) is now emitted as a standalone type instead of dangling ([#989](https://github.com/opensearch-project/opensearch-go/pull/989))
247249
- Fix plugin dispatch methods discarding the transport response, leaving `Inspect().Response` nil on every typed plugin response. The generated plugin dispatch template dropped the `*opensearch.Response` returned by `request()` (`if _, err := request(...)`); it now assigns it (`resp.response, err = request(...)`), matching the core client. Also serialize the discovery-path warmup recalculation and ready-list partitioning in `createOrUpdateMultiNodePoolWithLock` under the pool write lock -- `recalculateWarmupParamsWithLock`/`getWarmupParamsWithLock` (renamed to reflect the requirement) and the `mu.activeCount` write touched `mu`-guarded fields without holding `pool.mu`, racing `resurrectWithLock` (follow-up to [#981](https://github.com/opensearch-project/opensearch-go/pull/981)) ([#989](https://github.com/opensearch-project/opensearch-go/pull/989))
248250
- Fix node discovery hijacking the request stream with unverified, unreachable discovered nodes and masking the user-supplied seed-URL fallback. When discovered `publish_address` values are unroutable from the client (NAT'd or misconfigured clusters, e.g. a Kubernetes stack cluster in CI), a freshly discovered but never-health-checked node could be served to requests as a zombie -- failing every request with `no route to host` -- instead of returning `ErrNoConnections` and cascading to the reachable seed URL. Connections are now considered available for routing only when they are a user-supplied seed (assumed reachable) or a discovered node confirmed reachable, and every routing policy and pool (round-robin, role, coordinator, index/doc router, single-server, and multi-server pools) consistently honors that gate on both the enabled-bit and connection-selection paths, so the seed fallback serves requests until a discovered node health-checks clean ([#952](https://github.com/opensearch-project/opensearch-go/pull/952), [#954](https://github.com/opensearch-project/opensearch-go/pull/954), [#956](https://github.com/opensearch-project/opensearch-go/pull/956))

opensearchtransport/discovery_integration_internal_test.go

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -444,12 +444,12 @@ func TestDiscoverNodesWithNewRoleValidation(t *testing.T) {
444444
}
445445
}
446446

447-
// TestIncludeDedicatedClusterManagersConfiguration verifies the configurable behavior
448-
func TestIncludeDedicatedClusterManagersConfiguration(t *testing.T) {
447+
// TestDedicatedClusterManagersExcludedFromRouting verifies that dedicated
448+
// cluster managers are held in the connection inventory but never routed to.
449+
func TestDedicatedClusterManagersExcludedFromRouting(t *testing.T) {
449450
tests := []struct {
450-
name string
451-
includeDedicatedClusterManagers bool
452-
nodes map[string][]string // nodeName -> roles
451+
name string
452+
nodes map[string][]string // nodeName -> roles
453453
// expectedInInventory lists nodes that must appear in the allConns pool,
454454
// which holds every discovered node regardless of role.
455455
expectedInInventory []string
@@ -458,18 +458,7 @@ func TestIncludeDedicatedClusterManagersConfiguration(t *testing.T) {
458458
expectedNotRoutable []string
459459
}{
460460
{
461-
name: "IncludeDedicatedClusterManagers enabled - all nodes routable",
462-
includeDedicatedClusterManagers: true,
463-
nodes: map[string][]string{
464-
"cm-only": {RoleClusterManager},
465-
"data-node": {RoleData},
466-
},
467-
expectedInInventory: []string{"cm-only", "data-node"},
468-
expectedNotRoutable: []string{},
469-
},
470-
{
471-
name: "IncludeDedicatedClusterManagers disabled (default) - dedicated CM in inventory but not routable",
472-
includeDedicatedClusterManagers: false,
461+
name: "dedicated cluster manager in inventory but not routable",
473462
nodes: map[string][]string{
474463
"cm-only": {RoleClusterManager},
475464
"data-node": {RoleData},
@@ -479,8 +468,7 @@ func TestIncludeDedicatedClusterManagersConfiguration(t *testing.T) {
479468
expectedNotRoutable: []string{"cm-only"},
480469
},
481470
{
482-
name: "Mixed roles with CM always routable regardless of setting",
483-
includeDedicatedClusterManagers: false,
471+
name: "mixed cluster_manager and data role is routable",
484472
nodes: map[string][]string{
485473
"cm-data": {RoleClusterManager, RoleData},
486474
"dummy": {RoleData}, // Add second node to avoid single connection pool
@@ -497,9 +485,8 @@ func TestIncludeDedicatedClusterManagersConfiguration(t *testing.T) {
497485

498486
u := mockhttp.GetOpenSearchURL(t)
499487
c, err := New(Config{
500-
URLs: []*url.URL{u},
501-
Transport: transport,
502-
IncludeDedicatedClusterManagers: tt.includeDedicatedClusterManagers,
488+
URLs: []*url.URL{u},
489+
Transport: transport,
503490
})
504491
require.NoError(t, err)
505492

opensearchtransport/discovery_internal_test.go

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,12 +1177,12 @@ func TestDiscoverNodesWithNewRoleValidation(t *testing.T) {
11771177
}
11781178
}
11791179

1180-
// TestIncludeDedicatedClusterManagersConfiguration verifies the configurable behavior
1181-
func TestIncludeDedicatedClusterManagersConfiguration(t *testing.T) {
1180+
// TestDedicatedClusterManagersExcludedFromRouting verifies that dedicated
1181+
// cluster managers are held in the connection inventory but never routed to.
1182+
func TestDedicatedClusterManagersExcludedFromRouting(t *testing.T) {
11821183
tests := []struct {
1183-
name string
1184-
includeDedicatedClusterManagers bool
1185-
nodes map[string][]string // nodeName -> roles
1184+
name string
1185+
nodes map[string][]string // nodeName -> roles
11861186
// expectedInInventory lists nodes that must appear in the allConns pool.
11871187
// The inventory holds every discovered node regardless of role so that
11881188
// discovery can reuse and evict connections; dedicated cluster managers
@@ -1193,18 +1193,7 @@ func TestIncludeDedicatedClusterManagersConfiguration(t *testing.T) {
11931193
expectedNotRoutable []string
11941194
}{
11951195
{
1196-
name: "IncludeDedicatedClusterManagers enabled - all nodes routable",
1197-
includeDedicatedClusterManagers: true,
1198-
nodes: map[string][]string{
1199-
"cm-only": {RoleClusterManager},
1200-
"data-node": {RoleData},
1201-
},
1202-
expectedInInventory: []string{"cm-only", "data-node"},
1203-
expectedNotRoutable: []string{},
1204-
},
1205-
{
1206-
name: "IncludeDedicatedClusterManagers disabled (default) - dedicated CM in inventory but not routable",
1207-
includeDedicatedClusterManagers: false,
1196+
name: "dedicated cluster manager in inventory but not routable",
12081197
nodes: map[string][]string{
12091198
"cm-only": {RoleClusterManager},
12101199
"data-node": {RoleData},
@@ -1214,8 +1203,7 @@ func TestIncludeDedicatedClusterManagersConfiguration(t *testing.T) {
12141203
expectedNotRoutable: []string{"cm-only"},
12151204
},
12161205
{
1217-
name: "Mixed roles with CM always routable regardless of setting",
1218-
includeDedicatedClusterManagers: false,
1206+
name: "mixed cluster_manager and data role is routable",
12191207
nodes: map[string][]string{
12201208
"cm-data": {RoleClusterManager, RoleData},
12211209
"dummy": {RoleData}, // Add second node to avoid single connection pool
@@ -1314,8 +1302,7 @@ func TestIncludeDedicatedClusterManagersConfiguration(t *testing.T) {
13141302
// Use the seed address for discovery
13151303
urls := []*url.URL{{Scheme: "http", Host: seedAddr}}
13161304
c, err := New(Config{
1317-
URLs: urls,
1318-
IncludeDedicatedClusterManagers: tt.includeDedicatedClusterManagers,
1305+
URLs: urls,
13191306
})
13201307
require.NoError(t, err)
13211308

opensearchtransport/doc.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@ Enable automatic cluster discovery to maintain current node information:
176176
Router: router,
177177
})
178178
179-
The discovery process respects node roles and can exclude dedicated cluster manager nodes
180-
from request routing (controlled by IncludeDedicatedClusterManagers configuration).
179+
The discovery process keeps dedicated cluster manager nodes in the connection
180+
inventory for reuse and eviction, but excludes them from request routing.
181181
182182
When a request fails (transport error or retryable HTTP status), the failing connection is
183183
marked with a needsCatUpdate flag that excludes it from scored routing candidate sets.

opensearchtransport/opensearchtransport.go

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,6 @@ type Config struct {
199199

200200
DiscoverNodesInterval time.Duration
201201

202-
// IncludeDedicatedClusterManagers includes dedicated cluster manager nodes in request routing.
203-
// When false (default), dedicated cluster manager nodes are excluded from client requests,
204-
// following best practices and matching the Java client's NodeSelector.SKIP_DEDICATED_CLUSTER_MASTERS behavior.
205-
// When true, all nodes including dedicated cluster managers can receive client requests.
206-
// Default: false (excludes dedicated cluster managers for better performance)
207-
IncludeDedicatedClusterManagers bool
208-
209202
// DiscoveryHealthCheckRetries sets the number of health check retries during node discovery.
210203
// During cold start, health checks are performed asynchronously without blocking.
211204
// During running cluster discovery, health checks are performed with retries before adding nodes.
@@ -470,11 +463,10 @@ type Transport struct {
470463
discoverNodesInterval time.Duration
471464
verifyDeadAfter time.Duration
472465

473-
includeDedicatedClusterManagers bool
474-
discoveryHealthCheckRetries int
475-
healthCheckTimeout time.Duration
476-
healthCheckMaxRetries int
477-
healthCheckJitter float64
466+
discoveryHealthCheckRetries int
467+
healthCheckTimeout time.Duration
468+
healthCheckMaxRetries int
469+
healthCheckJitter float64
478470

479471
resurrectTimeoutInitial time.Duration
480472
resurrectTimeoutMax time.Duration
@@ -955,11 +947,10 @@ func New(cfg Config) (*Transport, error) {
955947
discoverNodesInterval: cfg.DiscoverNodesInterval,
956948
verifyDeadAfter: verifyDeadAfter,
957949

958-
includeDedicatedClusterManagers: cfg.IncludeDedicatedClusterManagers,
959-
discoveryHealthCheckRetries: cfg.DiscoveryHealthCheckRetries,
960-
healthCheckTimeout: healthCheckTimeout,
961-
healthCheckMaxRetries: healthCheckMaxRetries,
962-
healthCheckJitter: healthCheckJitter,
950+
discoveryHealthCheckRetries: cfg.DiscoveryHealthCheckRetries,
951+
healthCheckTimeout: healthCheckTimeout,
952+
healthCheckMaxRetries: healthCheckMaxRetries,
953+
healthCheckJitter: healthCheckJitter,
963954

964955
resurrectTimeoutInitial: resurrectTimeoutInitial,
965956
resurrectTimeoutMax: resurrectTimeoutMax,
@@ -1179,22 +1170,21 @@ func New(cfg Config) (*Transport, error) {
11791170
// Configure policy settings for all policies in the router
11801171
if client.router != nil {
11811172
config := policyConfig{
1182-
ctx: client.ctx,
1183-
resurrectTimeoutInitial: client.resurrectTimeoutInitial,
1184-
resurrectTimeoutMax: client.resurrectTimeoutMax,
1185-
resurrectTimeoutFactorCutoff: client.resurrectTimeoutFactorCutoff,
1186-
minimumResurrectTimeout: client.minimumResurrectTimeout,
1187-
jitterScale: client.jitterScale,
1188-
serverMaxNewConnsPerSec: client.serverMaxNewConnsPerSec,
1189-
clientsPerServer: client.clientsPerServer,
1190-
healthCheck: client.healthCheck,
1191-
observer: client.observer.Load(),
1192-
poolInfoReady: &client.poolInfoReady,
1193-
clusterSearchCwnd: &client.clusterSearch.cwnd,
1194-
activeListCap: client.activeListCapConfig,
1195-
standbyPromotionChecks: client.standbyPromotionChecks,
1196-
includeDedicatedClusterManagers: client.includeDedicatedClusterManagers,
1197-
metrics: client.metrics,
1173+
ctx: client.ctx,
1174+
resurrectTimeoutInitial: client.resurrectTimeoutInitial,
1175+
resurrectTimeoutMax: client.resurrectTimeoutMax,
1176+
resurrectTimeoutFactorCutoff: client.resurrectTimeoutFactorCutoff,
1177+
minimumResurrectTimeout: client.minimumResurrectTimeout,
1178+
jitterScale: client.jitterScale,
1179+
serverMaxNewConnsPerSec: client.serverMaxNewConnsPerSec,
1180+
clientsPerServer: client.clientsPerServer,
1181+
healthCheck: client.healthCheck,
1182+
observer: client.observer.Load(),
1183+
poolInfoReady: &client.poolInfoReady,
1184+
clusterSearchCwnd: &client.clusterSearch.cwnd,
1185+
activeListCap: client.activeListCapConfig,
1186+
standbyPromotionChecks: client.standbyPromotionChecks,
1187+
metrics: client.metrics,
11981188
}
11991189
// Use type assertion to check if the router (which is a Policy) implements policyConfigurable
12001190
if configurablePolicy, ok := client.router.(policyConfigurable); ok {
@@ -2726,7 +2716,6 @@ func (c *Transport) newMultiServerPoolFromClientWithLock(name string, m *metrics
27262716
metrics: m,
27272717
activeListCapConfig: c.activeListCapConfig,
27282718
standbyPromotionChecks: c.standbyPromotionChecks,
2729-
excludeDCM: !c.includeDedicatedClusterManagers,
27302719
}
27312720
pool.mu.activeListCap = c.activeListCap
27322721
pool.mu.healthCheck = c.healthCheck

0 commit comments

Comments
 (0)