Skip to content

Commit 605a12c

Browse files
committed
Optimize Connection role lookups with roleSet for O(1) performance
Replace Connection.Roles []string with roleSet (map[string]struct{}) to enable O(1) role lookups instead of O(n) iterations in filterByRoles(). While most nodes have 1-3 roles where sequential comparison might be faster, this future-proofs performance for any role count and provides consistent algorithmic behavior. Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
1 parent 27db04a commit 605a12c

4 files changed

Lines changed: 57 additions & 27 deletions

File tree

opensearchtransport/connection.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import (
3131
"fmt"
3232
"math"
3333
"net/url"
34-
"slices"
3534
"sort"
3635
"strings"
3736
"sync"
@@ -99,7 +98,7 @@ type Connection struct {
9998
URL *url.URL
10099
ID string
101100
Name string
102-
Roles []string
101+
Roles roleSet
103102
Attributes map[string]any
104103

105104
failures atomic.Int64
@@ -580,8 +579,8 @@ func (s *RoleBasedSelector) filterByRoles(connections []*Connection) []*Connecti
580579
// Check if connection has at least one required role
581580
hasRequiredRole := len(s.requiredRoles) == 0 // If no required roles, all nodes qualify
582581
if !hasRequiredRole {
583-
for _, role := range conn.Roles {
584-
if slices.Contains(s.requiredRoles, role) {
582+
for _, role := range s.requiredRoles {
583+
if conn.Roles.has(role) {
585584
hasRequiredRole = true
586585
break
587586
}
@@ -594,8 +593,8 @@ func (s *RoleBasedSelector) filterByRoles(connections []*Connection) []*Connecti
594593

595594
// Check if connection has any excluded roles
596595
hasExcludedRole := false
597-
for _, role := range conn.Roles {
598-
if slices.Contains(s.excludedRoles, role) {
596+
for _, role := range s.excludedRoles {
597+
if conn.Roles.has(role) {
599598
hasExcludedRole = true
600599
break
601600
}

opensearchtransport/discovery.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,30 @@ func (rs roleSet) has(roleName string) bool {
121121
return exists
122122
}
123123

124+
// toSlice converts the roleSet back to a []string slice for compatibility.
125+
// The roles are sorted alphabetically for consistent ordering.
126+
func (rs roleSet) toSlice() []string {
127+
if len(rs) == 0 {
128+
return nil
129+
}
130+
131+
roles := make([]string, 0, len(rs))
132+
for role := range rs {
133+
// Skip the internal cluster_manager alias added for deprecated master role
134+
if role == RoleClusterManager {
135+
// Check if this was added as an alias for deprecated master role
136+
if _, hasMaster := rs[RoleMaster]; hasMaster {
137+
continue // Skip the alias, keep only the original master role
138+
}
139+
}
140+
roles = append(roles, role)
141+
}
142+
143+
// Sort roles alphabetically for consistent ordering
144+
slices.Sort(roles)
145+
return roles
146+
}
147+
124148
// isDedicatedClusterManager implements the logic from upstream Java client
125149
// NodeSelector.SKIP_DEDICATED_CLUSTER_MASTERS to determine if a node should be skipped.
126150
// It returns true for nodes that are cluster-manager eligible but have no "work" roles
@@ -199,7 +223,7 @@ func (c *Client) DiscoverNodes() error {
199223
URL: node.URL,
200224
ID: node.ID,
201225
Name: node.Name,
202-
Roles: node.Roles,
226+
Roles: newRoleSet(node.Roles),
203227
Attributes: node.Attributes,
204228
})
205229
}

opensearchtransport/discovery_internal_test.go

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import (
3939
"net/url"
4040
"os"
4141
"reflect"
42+
"slices"
4243
"testing"
4344
"time"
4445

@@ -553,8 +554,14 @@ func TestDiscovery(t *testing.T) {
553554
}
554555

555556
for _, conn := range pool.mu.live {
556-
if !reflect.DeepEqual(tt.args.Nodes[conn.ID].Roles, conn.Roles) {
557-
t.Errorf("Unexpected roles for node %s, want=%s, got=%s", conn.Name, tt.args.Nodes[conn.ID], conn.Roles)
557+
expectedRoles := make([]string, len(tt.args.Nodes[conn.ID].Roles))
558+
copy(expectedRoles, tt.args.Nodes[conn.ID].Roles)
559+
slices.Sort(expectedRoles)
560+
561+
actualRoles := conn.Roles.toSlice()
562+
563+
if !reflect.DeepEqual(expectedRoles, actualRoles) {
564+
t.Errorf("Unexpected roles for node %s, want=%s, got=%s", conn.Name, expectedRoles, actualRoles)
558565
}
559566
}
560567

@@ -1023,11 +1030,11 @@ func TestIncludeDedicatedClusterManagersConfiguration(t *testing.T) {
10231030
// TestGenericRoleBasedSelector tests the new generic role-based selector
10241031
func TestGenericRoleBasedSelector(t *testing.T) {
10251032
connections := []*Connection{
1026-
{Name: "data-node", Roles: []string{RoleData}},
1027-
{Name: "ingest-node", Roles: []string{RoleIngest}},
1028-
{Name: "data-ingest-node", Roles: []string{RoleData, RoleIngest}},
1029-
{Name: "cluster-manager-node", Roles: []string{RoleClusterManager}},
1030-
{Name: "coordinating-node", Roles: []string{}}, // No specific roles
1033+
{Name: "data-node", Roles: newRoleSet([]string{RoleData})},
1034+
{Name: "ingest-node", Roles: newRoleSet([]string{RoleIngest})},
1035+
{Name: "data-ingest-node", Roles: newRoleSet([]string{RoleData, RoleIngest})},
1036+
{Name: "cluster-manager-node", Roles: newRoleSet([]string{RoleClusterManager})},
1037+
{Name: "coordinating-node", Roles: newRoleSet([]string{})}, // No specific roles
10311038
}
10321039

10331040
fallback := &mockSelector{}
@@ -1096,13 +1103,13 @@ func TestGenericRoleBasedSelector(t *testing.T) {
10961103
func TestRoleBasedSelectors(t *testing.T) {
10971104
// Create test connections with different roles
10981105
connections := []*Connection{
1099-
{Name: "data-node", Roles: []string{RoleData}},
1100-
{Name: "ingest-node", Roles: []string{RoleIngest}},
1101-
{Name: "data-ingest-node", Roles: []string{RoleData, RoleIngest}},
1102-
{Name: "cluster-manager-node", Roles: []string{RoleClusterManager}},
1103-
{Name: "warm-node", Roles: []string{RoleWarm}},
1104-
{Name: "search-node", Roles: []string{RoleSearch}},
1105-
{Name: "coordinating-node", Roles: []string{}}, // No specific roles
1106+
{Name: "data-node", Roles: newRoleSet([]string{RoleData})},
1107+
{Name: "ingest-node", Roles: newRoleSet([]string{RoleIngest})},
1108+
{Name: "data-ingest-node", Roles: newRoleSet([]string{RoleData, RoleIngest})},
1109+
{Name: "cluster-manager-node", Roles: newRoleSet([]string{RoleClusterManager})},
1110+
{Name: "warm-node", Roles: newRoleSet([]string{RoleWarm})},
1111+
{Name: "search-node", Roles: newRoleSet([]string{RoleSearch})},
1112+
{Name: "coordinating-node", Roles: newRoleSet([]string{})}, // No specific roles
11061113
}
11071114

11081115
// Mock fallback selector that just returns the first connection
@@ -1180,9 +1187,9 @@ func TestRoleBasedSelectors(t *testing.T) {
11801187
func TestSmartSelector(t *testing.T) {
11811188
// Create test connections
11821189
connections := []*Connection{
1183-
{Name: "data-node", Roles: []string{RoleData}},
1184-
{Name: "ingest-node", Roles: []string{RoleIngest}},
1185-
{Name: "data-ingest-node", Roles: []string{RoleData, RoleIngest}},
1190+
{Name: "data-node", Roles: newRoleSet([]string{RoleData})},
1191+
{Name: "ingest-node", Roles: newRoleSet([]string{RoleIngest})},
1192+
{Name: "data-ingest-node", Roles: newRoleSet([]string{RoleData, RoleIngest})},
11861193
}
11871194

11881195
fallback := &mockSelector{}
@@ -1235,8 +1242,8 @@ func TestSmartSelector(t *testing.T) {
12351242
// TestRequestAwareConnectionPool tests the enhanced connection pool
12361243
func TestRequestAwareConnectionPool(t *testing.T) {
12371244
connections := []*Connection{
1238-
{Name: "data-node", Roles: []string{RoleData}},
1239-
{Name: "ingest-node", Roles: []string{RoleIngest}},
1245+
{Name: "data-node", Roles: newRoleSet([]string{RoleData})},
1246+
{Name: "ingest-node", Roles: newRoleSet([]string{RoleIngest})},
12401247
}
12411248

12421249
smartSelector := NewSmartSelector(&mockSelector{})

opensearchtransport/metrics.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ func (c *Client) Metrics() (Metrics, error) {
140140
}
141141

142142
if len(c.Roles) > 0 {
143-
cm.Meta.Roles = c.Roles
143+
cm.Meta.Roles = c.Roles.toSlice()
144144
}
145145

146146
m.Connections = append(m.Connections, cm)

0 commit comments

Comments
 (0)