Skip to content

Commit 9d8d490

Browse files
committed
merge commit
Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
1 parent 3e1e620 commit 9d8d490

6 files changed

Lines changed: 47 additions & 87 deletions

File tree

CHANGELOG.md

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

77
### Added
8+
- Add connection pool health probes with cluster-aware resurrection timing ([#786](https://github.com/opensearch-project/opensearch-go/pull/786))
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))

opensearchtransport/connection.go

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -336,7 +336,7 @@ func (cp *statusConnectionPool) resurrectWithLock(c *Connection, removeDead bool
336336
debugLogger.Logf("Health check failed for %q: %s; will retry later\n", c.URL, err)
337337
}
338338
// Health check failed - schedule another resurrection attempt
339-
cp.scheduleResurrect(c)
339+
cp.scheduleResurrect(c, c.mu.deadSince)
340340
return
341341
}
342342
if debugLogger != nil {
@@ -398,6 +398,7 @@ func (cp *statusConnectionPool) scheduleResurrect(c *Connection, deadSince time.
398398
clusterTimeout := time.Duration(float64(baseTimeout) * clusterFactor)
399399

400400
// Add random jitter (0 to clusterTimeout range)
401+
// #nosec G404 - Non-cryptographic randomness is acceptable for connection timing jitter
401402
jitter := time.Duration(rand.Float64() * float64(clusterTimeout))
402403
finalTimeout = jitter
403404

@@ -436,34 +437,6 @@ func (cp *statusConnectionPool) scheduleResurrect(c *Connection, deadSince time.
436437
return
437438
}
438439

439-
cp.resurrectWithLock(c, true)
440-
})
441-
c.URL,
442-
failures,
443-
factor,
444-
liveNodes,
445-
len(cp.dead),
446-
totalNodes,
447-
baseTimeout,
448-
finalTimeout,
449-
c.DeadSince.Add(finalTimeout).Sub(time.Now().UTC()).Truncate(time.Millisecond),
450-
)
451-
}
452-
453-
time.AfterFunc(finalTimeout, func() {
454-
cp.Lock()
455-
defer cp.Unlock()
456-
457-
c.mu.Lock()
458-
defer c.mu.Unlock()
459-
460-
if !c.mu.isDead {
461-
if debugLogger != nil {
462-
debugLogger.Logf("Already resurrected %q\n", c.URL)
463-
}
464-
return
465-
}
466-
467440
cp.resurrectWithLock(c, true)
468441
})
469442
}

opensearchtransport/connection_integration_test.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,28 @@ func TestStatusConnectionPool(t *testing.T) {
4949
serverHosts []string
5050
numServers = 3
5151

52-
defaultHandler = func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "OK") }
52+
defaultHandler = func(w http.ResponseWriter, r *http.Request) {
53+
// Return proper OpenSearch root endpoint JSON response
54+
w.Header().Set("Content-Type", "application/json")
55+
response := `{
56+
"name": "test-node",
57+
"cluster_name": "test-cluster",
58+
"cluster_uuid": "test-cluster-uuid",
59+
"version": {
60+
"number": "3.4.0",
61+
"build_type": "tar",
62+
"build_hash": "test-hash",
63+
"build_date": "2024-01-01T00:00:00.000Z",
64+
"build_snapshot": false,
65+
"lucene_version": "9.11.0",
66+
"minimum_wire_compatibility_version": "7.10.0",
67+
"minimum_index_compatibility_version": "7.0.0",
68+
"distribution": "opensearch"
69+
},
70+
"tagline": "The OpenSearch Project: https://opensearch.org/"
71+
}`
72+
fmt.Fprint(w, response)
73+
}
5374
)
5475

5576
for i := 1; i <= numServers; i++ {

opensearchtransport/connection_internal_test.go

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -29,53 +29,12 @@
2929
package opensearchtransport
3030

3131
import (
32-
"bytes"
33-
"io"
34-
"net/http"
3532
"net/url"
3633
"regexp"
3734
"testing"
3835
"time"
3936
)
4037

41-
// mockTransport provides a mock HTTP transport that responds to health checks with valid OpenSearch responses
42-
type mockTransport struct{}
43-
44-
func (t *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
45-
// Return a valid OpenSearch GET / response for health checks
46-
if req.Method == "GET" && req.URL.Path == "/" {
47-
body := `{
48-
"name": "test-node",
49-
"cluster_name": "test-cluster",
50-
"cluster_uuid": "test-uuid",
51-
"version": {
52-
"number": "2.0.0",
53-
"distribution": "opensearch",
54-
"build_type": "tar",
55-
"build_hash": "test-hash",
56-
"build_date": "2024-01-01T00:00:00Z",
57-
"build_snapshot": false,
58-
"lucene_version": "9.0.0",
59-
"minimum_wire_compatibility_version": "7.10.0",
60-
"minimum_index_compatibility_version": "7.0.0"
61-
},
62-
"tagline": "The OpenSearch Project: https://opensearch.org/"
63-
}`
64-
return &http.Response{
65-
StatusCode: 200,
66-
Body: io.NopCloser(bytes.NewBufferString(body)),
67-
Header: make(http.Header),
68-
}, nil
69-
}
70-
71-
// For other requests, return a basic response
72-
return &http.Response{
73-
StatusCode: 200,
74-
Body: io.NopCloser(bytes.NewBufferString("{}")),
75-
Header: make(http.Header),
76-
}, nil
77-
}
78-
7938
func TestSingleConnectionPoolNext(t *testing.T) {
8039
t.Run("Single URL", func(t *testing.T) {
8140
pool := &singleConnectionPool{
@@ -429,6 +388,10 @@ func TestStatusConnectionPoolResurrect(t *testing.T) {
429388
// Channel to signal when resurrection is complete
430389
done := make(chan struct{})
431390

391+
// Create round-robin selector
392+
s := &roundRobinSelector{}
393+
s.curr.Store(-1)
394+
432395
pool := &statusConnectionPool{
433396
selector: s,
434397
resurrectTimeoutInitial: 0,
@@ -479,7 +442,6 @@ func TestStatusConnectionPoolResurrect(t *testing.T) {
479442
if len(pool.mu.dead) != 0 {
480443
t.Errorf("Expected no dead connections, got: %d", len(pool.mu.dead))
481444
}
482-
}
483445
})
484446
}
485447

opensearchtransport/discovery.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ func (c *Client) DiscoverNodes() error {
140140
}
141141

142142
// Set up health check function for pools that support it
143-
if pool, ok := c.pool.(*statusConnectionPool); ok {
143+
if pool, ok := c.mu.pool.(*statusConnectionPool); ok {
144144
pool.healthCheck = c.isHealthyOpenSearchNode
145145
}
146146

@@ -168,7 +168,8 @@ func (c *Client) getNodesInfo() ([]nodeInfo, error) {
168168
}
169169

170170
// Use round-robin selector to pick a startup URL
171-
selector := &roundRobinSelector{curr: -1}
171+
selector := &roundRobinSelector{}
172+
selector.curr.Store(-1)
172173
conn, err = selector.Select(startupConns)
173174
if err != nil {
174175
return nil, fmt.Errorf("failed to select startup URL: %w", err)

opensearchtransport/opensearchtransport.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ const (
5656
)
5757

5858
var (
59-
reGoVersion = regexp.MustCompile(`go(\d+\.\d+\..+)`)
60-
errHealthCheckFailed = errors.New("connection health check error")
59+
reGoVersion = regexp.MustCompile(`go(\d+\.\d+\..+)`)
60+
errHealthCheckFailed = errors.New("connection health check error")
6161
)
6262

6363
// Interface defines the interface for HTTP client.
@@ -177,9 +177,6 @@ func New(cfg Config) (*Client, error) {
177177
conns := make([]*Connection, len(cfg.URLs))
178178
for idx, u := range cfg.URLs {
179179
conn := &Connection{URL: u}
180-
// Mark initial connections as dead to trigger health validation via resurrection workflow
181-
// Discovery will use fallback to startup URLs when pool has no live connections
182-
conn.markAsDead()
183180
conns[idx] = conn
184181
}
185182

@@ -226,7 +223,7 @@ func New(cfg Config) (*Client, error) {
226223
}
227224

228225
// Set up health check function for pools that support it
229-
if pool, ok := client.pool.(*statusConnectionPool); ok {
226+
if pool, ok := client.mu.pool.(*statusConnectionPool); ok {
230227
pool.healthCheck = client.isHealthyOpenSearchNode
231228
}
232229

@@ -563,12 +560,12 @@ type OpenSearchInfo struct {
563560
Version struct {
564561
// Permanent fields - guaranteed since OpenSearch 1.3.0
565562
Number string `json:"number"` // Version number, e.g. "1.3.0"
566-
BuildType string `json:"build_type"` // Build type: "tar", "docker", etc.
567-
BuildHash string `json:"build_hash"` // Git commit hash
568-
BuildDate string `json:"build_date"` // Build timestamp
569-
BuildSnapshot bool `json:"build_snapshot"` // Is snapshot build
570-
LuceneVersion string `json:"lucene_version"` // Underlying Lucene version
571-
MinimumWireCompatibilityVersion string `json:"minimum_wire_compatibility_version"` // Minimum wire protocol version
563+
BuildType string `json:"build_type"` // Build type: "tar", "docker", etc.
564+
BuildHash string `json:"build_hash"` // Git commit hash
565+
BuildDate string `json:"build_date"` // Build timestamp
566+
BuildSnapshot bool `json:"build_snapshot"` // Is snapshot build
567+
LuceneVersion string `json:"lucene_version"` // Underlying Lucene version
568+
MinimumWireCompatibilityVersion string `json:"minimum_wire_compatibility_version"` // Minimum wire protocol version
572569
MinimumIndexCompatibilityVersion string `json:"minimum_index_compatibility_version"` // Minimum index compatibility version
573570

574571
// Conditional fields - may be missing in specific configurations
@@ -601,13 +598,18 @@ func (c *Client) isHealthyOpenSearchNode(url *url.URL) (string, error) {
601598
if res == nil {
602599
return "", fmt.Errorf("%w: nil response", errHealthCheckFailed)
603600
}
604-
defer res.Body.Close()
601+
if res.Body != nil {
602+
defer res.Body.Close()
603+
}
605604

606605
if res.StatusCode != http.StatusOK {
607606
return "", fmt.Errorf("%w: status %d", errHealthCheckFailed, res.StatusCode)
608607
}
609608

610609
// Read and parse the response
610+
if res.Body == nil {
611+
return "", fmt.Errorf("%w: nil response body", errHealthCheckFailed)
612+
}
611613
body, err := io.ReadAll(res.Body)
612614
if err != nil {
613615
return "", fmt.Errorf("%w: %w", errHealthCheckFailed, err)

0 commit comments

Comments
 (0)