Skip to content

Commit 0ab5947

Browse files
committed
feat(opensearchtransport): make DiscoverNodes blocking, add *bool DiscoverNodesOnStart
DiscoverNodes now waits for an in-flight discovery to complete (or for the context to be cancelled) instead of returning nil immediately. This lets callers block until topology data is available after client construction. - Auto-enable DiscoverNodesOnStart when OPENSEARCH_GO_ROUTER=true and the caller did not set the field Signed-off-by: Sean Chittenden <sean.chittenden@crowdstrike.com>
1 parent ff87a7d commit 0ab5947

5 files changed

Lines changed: 482 additions & 20 deletions

File tree

opensearch_internal_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ import (
5050

5151
var called int
5252

53+
func boolPtr(v bool) *bool { return &v }
54+
5355
var defaultRoundTripFunc = func(req *http.Request) (*http.Response, error) {
5456
response := &http.Response{Header: http.Header{}}
5557

@@ -78,8 +80,6 @@ type testReq struct {
7880
Headers http.Header
7981
}
8082

81-
func boolPtr(v bool) *bool { return &v }
82-
8383
func (r testReq) GetRequest(method string) (*http.Request, error) {
8484
if r.Error {
8585
return nil, fmt.Errorf("test error")

opensearchtransport/discovery.go

Lines changed: 92 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ import (
4848
// connection pool is left untouched.
4949
var errDiscoveryEmpty = errors.New("discovery returned zero successful nodes")
5050

51+
// errDiscoveryInterrupted is returned to a waiting caller when the discovery
52+
// it blocked on was aborted by the cancellation of the goroutine that ran it,
53+
// rather than by the waiter's own context. The waiter's context is still
54+
// healthy, so it must not inherit the runner's context.Canceled (which would
55+
// look like the waiter's own cancellation and could suppress a legitimate
56+
// retry); it receives this sentinel instead and may retry discovery.
57+
var errDiscoveryInterrupted = errors.New("discovery interrupted by another caller's cancellation")
58+
5159
// Node role constants matching upstream OpenSearch server definitions.
5260
const (
5361
// RoleData nodes store and retrieve data, perform indexing, searching, and
@@ -252,34 +260,101 @@ func (m *_NodesMeta) formatFailures() string {
252260
return string(b)
253261
}
254262

255-
// DiscoverNodes reloads the client connections by fetching information from the cluster.
263+
// DiscoverNodes reloads the client connections by fetching information from
264+
// the cluster. If another discovery is already in progress, DiscoverNodes
265+
// blocks until that discovery completes (or ctx is cancelled) and returns
266+
// its result.
256267
func (c *Client) DiscoverNodes(ctx context.Context) error {
257-
// Bail out early if the context is already cancelled (e.g. client shutting down).
258268
if ctx.Err() != nil {
259269
return ctx.Err()
260270
}
261271

262-
// Prevent concurrent discovery operations
263-
c.mu.Lock()
264-
if c.mu.discoveryInProgress {
265-
c.mu.Unlock()
272+
c.discoverMu.Lock()
273+
274+
if c.discoverMu.inProgress {
275+
// Another goroutine is running discovery. Wait for it using
276+
// sync.Cond + context.AfterFunc so that context cancellation
277+
// wakes us even though Cond.Wait is not context-aware.
278+
stopf := context.AfterFunc(ctx, func() {
279+
c.discoverMu.Lock()
280+
defer c.discoverMu.Unlock()
281+
c.discoverMu.cond.Broadcast()
282+
})
283+
defer stopf()
284+
285+
for c.discoverMu.inProgress {
286+
c.discoverMu.cond.Wait()
287+
if ctx.Err() != nil {
288+
c.discoverMu.Unlock()
289+
return ctx.Err()
290+
}
291+
}
292+
err := c.discoverMu.lastErr
293+
c.discoverMu.Unlock()
294+
// If our own context was cancelled (it can race the final
295+
// Broadcast and skip the in-loop check), that takes precedence.
296+
if ctx.Err() != nil {
297+
return ctx.Err()
298+
}
299+
// Otherwise the discovery we waited on may have been aborted by the
300+
// runner's context, not ours. Don't inherit a cancellation we never
301+
// initiated -- report a retryable sentinel instead.
302+
if err != nil && (errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)) {
303+
return errDiscoveryInterrupted
304+
}
305+
return err
306+
}
307+
308+
// We won the race: start discovery.
309+
// Lock is held — doDiscoverNodes takes ownership and releases it.
310+
return c.doDiscoverNodes(ctx)
311+
}
312+
313+
// tryDiscoverNodes attempts to start a discovery cycle. If discovery is
314+
// already in progress it returns nil immediately without waiting.
315+
//
316+
// This is used by the internal discoveryLoop, which must never block on
317+
// another discovery. It could be exported in the future if callers need
318+
// fire-and-forget semantics on the public API.
319+
func (c *Client) tryDiscoverNodes(ctx context.Context) error {
320+
if ctx.Err() != nil {
321+
return ctx.Err()
322+
}
323+
324+
c.discoverMu.Lock()
325+
if c.discoverMu.inProgress {
326+
c.discoverMu.Unlock()
266327
return nil
267328
}
268-
c.mu.discoveryInProgress = true
269-
c.mu.Unlock()
329+
// Lock is held — doDiscoverNodes takes ownership and releases it.
330+
return c.doDiscoverNodes(ctx)
331+
}
270332

333+
// doDiscoverNodes performs the discovery work.
334+
//
335+
// Called with c.discoverMu held. It sets inProgress = true, releases the
336+
// lock for I/O, then re-acquires it on completion to clear inProgress,
337+
// store the result, and wake any waiters.
338+
func (c *Client) doDiscoverNodes(ctx context.Context) error {
339+
c.discoverMu.inProgress = true
340+
c.discoverMu.Unlock()
341+
342+
var discoverErr error
271343
defer func() {
272-
c.mu.Lock()
273-
c.mu.discoveryInProgress = false
274-
c.mu.Unlock()
344+
c.discoverMu.Lock()
345+
c.discoverMu.inProgress = false
346+
c.discoverMu.lastErr = discoverErr
347+
c.discoverMu.cond.Broadcast()
348+
c.discoverMu.Unlock()
275349
}()
276350

277351
discovered, err := c.getNodesInfo(ctx)
278352
if err != nil {
279353
if dl := loadDebugLogger(); dl != nil {
280354
dl.Logf("Error getting nodes info: %s\n", err)
281355
}
282-
return fmt.Errorf("discovery: get nodes: %w", err)
356+
discoverErr = fmt.Errorf("discovery: get nodes: %w", err)
357+
return discoverErr
283358
}
284359

285360
c.mu.RLock()
@@ -289,11 +364,13 @@ func (c *Client) DiscoverNodes(ctx context.Context) error {
289364

290365
if isColdStart {
291366
if err := c.nodeDiscoveryAsyncStart(ctx, discovered); err != nil {
292-
return err
367+
discoverErr = err
368+
return discoverErr
293369
}
294370
} else {
295371
if err := c.nodeDiscovery(ctx, discovered); err != nil {
296-
return err
372+
discoverErr = err
373+
return discoverErr
297374
}
298375
}
299376

@@ -1217,7 +1294,7 @@ func (c *Client) discoveryLoop() {
12171294
switch {
12181295
case !now.Before(nextNodes):
12191296
// Full node + shard discovery.
1220-
c.DiscoverNodes(c.ctx) //nolint:errcheck // errors logged inside
1297+
c.tryDiscoverNodes(c.ctx) //nolint:errcheck // errors logged inside
12211298

12221299
nextNodes = time.Now().Add(c.discoverNodesInterval)
12231300
nextCat = time.Time{}

0 commit comments

Comments
 (0)