Skip to content

Commit 4ddced1

Browse files
committed
Fix node discovery logic to properly handle dedicated cluster managers
Fixes dedicated cluster manager detection by implementing proper role constants and upstream-compatible node filtering logic. Addresses the core issue where nodes with only cluster management roles were incorrectly being included in the connection pool for client requests. Key changes: - Add proper role constants matching OpenSearch server definitions - Implement isDedicatedClusterManager with role-based logic matching Java client - Add IncludeDedicatedClusterManagers configuration option - Remove client-side role validation (handled server-side) BEHAVIOR CHANGE: Dedicated cluster manager nodes are now excluded from client request routing by default to match upstream Java client behavior. Set IncludeDedicatedClusterManagers: true to preserve legacy behavior. Fixes: opensearch-project#765 Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
1 parent 3deff37 commit 4ddced1

4 files changed

Lines changed: 588 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
55
## [Unreleased]
66

77
### Added
8+
- Configuration option `IncludeDedicatedClusterManagers` for controlling cluster manager node routing ([#765](https://github.com/opensearch-project/opensearch-go/issues/765))
89

910
### Changed
1011
- Refactor Client struct to use embedded mutex pattern for improved thread safety ([#775](https://github.com/opensearch-project/opensearch-go/pull/775))
1112
- Refactor metrics struct to use atomic counters for lock-free request/failure tracking ([#776](https://github.com/opensearch-project/opensearch-go/pull/776))
13+
- **BREAKING**: Enhanced node discovery to match OpenSearch server behavior ([#765](https://github.com/opensearch-project/opensearch-go/issues/765))
14+
- Dedicated cluster manager nodes are now excluded from client request routing by default (best practice)
15+
- Node selection logic now matches Java client `NodeSelector.SKIP_DEDICATED_CLUSTER_MASTERS` behavior
1216

1317
### Deprecated
1418

opensearchtransport/discovery.go

Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,128 @@ import (
3434
"net"
3535
"net/http"
3636
"net/url"
37+
"slices"
3738
"strings"
3839
"sync"
3940
"time"
4041
)
4142

43+
// Node role constants to match upstream OpenSearch server definitions
44+
const (
45+
// RoleData nodes store and retrieve data, perform indexing, searching, and
46+
// aggregating operations on local shards. Available since OpenSearch 1.0.
47+
// See: https://docs.opensearch.org/latest/install-and-configure/configuring-opensearch/configuration-system/
48+
RoleData = "data"
49+
50+
// RoleIngest nodes pre-process data before storing via ingest pipelines.
51+
// Available since OpenSearch 1.0.
52+
// See: https://docs.opensearch.org/latest/install-and-configure/configuring-opensearch/configuration-system/
53+
RoleIngest = "ingest"
54+
55+
// RoleClusterManager nodes manage overall cluster operations, cluster state,
56+
// index creation/deletion, node health checks, and shard allocation.
57+
// Available since OpenSearch 1.0.
58+
// See: https://docs.opensearch.org/latest/install-and-configure/configuring-opensearch/configuration-system/
59+
RoleClusterManager = "cluster_manager"
60+
61+
// RoleRemoteClusterClient nodes can act as cross-cluster clients and connect
62+
// to remote clusters. Available since OpenSearch 1.0 (based on Elasticsearch 7.8.0).
63+
// See: https://docs.opensearch.org/latest/install-and-configure/configuring-opensearch/configuration-system/
64+
RoleRemoteClusterClient = "remote_cluster_client"
65+
66+
// RoleSearch nodes are dedicated to hosting search replica shards, allowing
67+
// separation of search workloads from indexing workloads. Added in OpenSearch 3.0.0-beta1.
68+
//
69+
// IMPORTANT: This role cannot be combined with any other node role. This restriction
70+
// has been enforced since OpenSearch 3.0.0-beta1.
71+
//
72+
// For searchable snapshots, use RoleWarm instead (recommended in OpenSearch 3.0+).
73+
// See: https://docs.opensearch.org/latest/tuning-your-cluster/separate-index-and-search-workloads/
74+
RoleSearch = "search"
75+
76+
// RoleWarm nodes provide access to warm indices and searchable snapshots.
77+
// Added in OpenSearch 2.4. In OpenSearch 3.0+, warm role replaces search role
78+
// for searchable snapshot functionality.
79+
// See: https://docs.opensearch.org/latest/tuning-your-cluster/index/
80+
RoleWarm = "warm"
81+
82+
// RoleML nodes are dedicated to running machine learning tasks and models.
83+
// This is a dynamic role added by the ML Commons plugin, not a built-in server role.
84+
// Available when ML Commons plugin is installed (typically OpenSearch 1.3+).
85+
// See: https://docs.opensearch.org/latest/ml-commons-plugin/cluster-settings/
86+
RoleML = "ml"
87+
88+
// RoleCoordinatingOnly represents nodes with no explicit roles (node.roles: []).
89+
// These nodes delegate client requests to shards on data nodes and aggregate results.
90+
// This is not a built-in role but a derived state when no roles are specified.
91+
// Available since OpenSearch 1.0 as a configuration pattern.
92+
// See: https://docs.opensearch.org/latest/install-and-configure/configuring-opensearch/configuration-system/
93+
RoleCoordinatingOnly = "coordinating_only"
94+
95+
// RoleMaster is Deprecated: Use RoleClusterManager instead for inclusive language.
96+
// Both roles are functionally identical but master role is deprecated.
97+
// See: https://docs.opensearch.org/latest/install-and-configure/configuring-opensearch/configuration-system/
98+
RoleMaster = "master"
99+
)
100+
101+
// roleSet represents a set of node roles for efficient O(1) role lookups.
102+
type roleSet map[string]struct{}
103+
104+
// newRoleSet creates a roleSet from a slice of role names.
105+
func newRoleSet(roles []string) roleSet {
106+
rs := make(roleSet, len(roles))
107+
for _, role := range roles {
108+
rs[role] = struct{}{}
109+
if role == RoleMaster {
110+
// Alias deprecated "master" role to "cluster_manager" for internal checks,
111+
// so we only need to perform a single check for "cluster_manager" elsewhere in the library.
112+
rs[RoleClusterManager] = struct{}{}
113+
}
114+
}
115+
return rs
116+
}
117+
118+
// has checks if the roleSet contains a specific role using O(1) map lookup.
119+
func (rs roleSet) has(roleName string) bool {
120+
_, exists := rs[roleName]
121+
return exists
122+
}
123+
124+
// isDedicatedClusterManager implements the logic from upstream Java client
125+
// NodeSelector.SKIP_DEDICATED_CLUSTER_MASTERS to determine if a node should be skipped.
126+
// It returns true for nodes that are cluster-manager eligible but have no "work" roles
127+
// (i.e., roles that actually process/store data or handle requests).
128+
// This matches OpenSearch server's SniffConnectionStrategy.DEFAULT_NODE_PREDICATE behavior.
129+
func (rs roleSet) isDedicatedClusterManager() bool {
130+
// Must be cluster manager eligible first
131+
if !rs.has(RoleClusterManager) {
132+
return false
133+
}
134+
135+
// Check if it has any "work" roles that make it non-dedicated
136+
workRoles := []string{
137+
RoleData, // stores and retrieves data
138+
RoleIngest, // processes incoming data
139+
RoleWarm, // handles warm/cold data storage
140+
RoleSearch, // dedicated search processing
141+
RoleML, // machine learning tasks
142+
}
143+
144+
return !slices.ContainsFunc(workRoles, rs.has)
145+
}
146+
42147
// Discoverable defines the interface for transports supporting node discovery.
43148
type Discoverable interface {
44149
DiscoverNodes() error
45150
}
46151

47152
// nodeInfo represents the information about node in a cluster.
48153
type nodeInfo struct {
49-
ID string `json:"id"`
50-
Name string `json:"name"`
51-
URL *url.URL `json:"url"`
52-
Roles []string `json:"roles"`
154+
ID string `json:"id"`
155+
Name string `json:"name"`
156+
URL *url.URL `json:"url"`
157+
Roles []string `json:"roles"`
158+
roleSet roleSet
53159
Attributes map[string]interface{} `json:"attributes"`
54160
HTTP struct {
55161
PublishAddress string `json:"publish_address"`
@@ -70,24 +176,23 @@ func (c *Client) DiscoverNodes() error {
70176
}
71177

72178
for _, node := range nodes {
73-
var isClusterManagerOnlyNode bool
179+
// Build role set for efficient O(1) lookups
180+
node.roleSet = newRoleSet(node.Roles)
74181

75-
if len(node.Roles) == 1 && (node.Roles[0] == "master" || node.Roles[0] == "cluster_manager") {
76-
isClusterManagerOnlyNode = true
77-
}
182+
// Skip this node if the user wants to exclude cluster managers (default) and this node is a dedicated cluster master.
183+
shouldSkip := !c.includeDedicatedClusterManagers && node.roleSet.isDedicatedClusterManager()
78184

79185
if debugLogger != nil {
80186
var skip string
81-
if isClusterManagerOnlyNode {
82-
skip = "; [SKIP]"
187+
if shouldSkip {
188+
skip = "; [SKIP: dedicated cluster manager]"
83189
}
84190

85-
debugLogger.Logf("Discovered node [%s]; %s; roles=%s%s\n", node.Name, node.URL, node.Roles, skip)
191+
debugLogger.Logf("Discovered node %q; %s; roles=%v%s\n", node.Name, node.URL, node.Roles, skip)
86192
}
87193

88-
// Skip cluster_manager only nodes
89-
// TODO: Move logic to Selector?
90-
if isClusterManagerOnlyNode {
194+
// Skip dedicated cluster managers (matching upstream Java client behavior)
195+
if shouldSkip {
91196
continue
92197
}
93198

0 commit comments

Comments
 (0)