@@ -42,6 +42,7 @@ import (
4242 "slices"
4343 "strconv"
4444 "strings"
45+ "sync"
4546 "testing"
4647 "time"
4748
@@ -2259,3 +2260,198 @@ func TestGetNodesInfoNodesMeta(t *testing.T) {
22592260 })
22602261 }
22612262}
2263+
2264+ // gatedNodesHandler returns a /_nodes/http handler that signals entered when
2265+ // the request arrives, then blocks until gate is closed before responding with
2266+ // a single data node. The handler also selects on t.Context().Done() so that
2267+ // test cleanup can unblock it.
2268+ func gatedNodesHandler (t * testing.T , entered chan <- struct {}, gate <- chan struct {}) http.HandlerFunc {
2269+ t .Helper ()
2270+ return func (w http.ResponseWriter , _ * http.Request ) {
2271+ select {
2272+ case entered <- struct {}{}:
2273+ default :
2274+ }
2275+ select {
2276+ case <- gate :
2277+ case <- t .Context ().Done ():
2278+ http .Error (w , "test context cancelled" , http .StatusServiceUnavailable )
2279+ return
2280+ }
2281+ w .Header ().Set ("Content-Type" , "application/json" )
2282+ fmt .Fprint (w , `{
2283+ "_nodes":{"total":1,"successful":1,"failed":0},
2284+ "cluster_name":"test",
2285+ "nodes":{
2286+ "n1":{
2287+ "name":"n1",
2288+ "roles":["data","ingest"],
2289+ "http":{"publish_address":"127.0.0.1:9200"}
2290+ }
2291+ }
2292+ }` )
2293+ }
2294+ }
2295+
2296+ // gatedErrorNodesHandler returns a /_nodes/http handler that signals entered
2297+ // when the request arrives, then blocks until gate is closed before responding
2298+ // with an HTTP 503 to trigger a discovery error. The handler also selects on
2299+ // t.Context().Done() so that test cleanup can unblock it.
2300+ func gatedErrorNodesHandler (t * testing.T , entered chan <- struct {}, gate <- chan struct {}) http.HandlerFunc {
2301+ t .Helper ()
2302+ return func (w http.ResponseWriter , _ * http.Request ) {
2303+ select {
2304+ case entered <- struct {}{}:
2305+ default :
2306+ }
2307+ select {
2308+ case <- gate :
2309+ case <- t .Context ().Done ():
2310+ }
2311+ http .Error (w , "unavailable" , http .StatusServiceUnavailable )
2312+ }
2313+ }
2314+
2315+ // newGatedDiscoverClient creates a transport Client wired to the given
2316+ // handler routes, with discoverMu.cond properly initialised.
2317+ func newGatedDiscoverClient (t * testing.T , routes mockhttp.HandlerMap ) * Client {
2318+ t .Helper ()
2319+ transport := mockhttp .NewTransportFromRoutes (t , routes )
2320+ u , _ := url .Parse ("http://127.0.0.1:9200" )
2321+ tp , err := New (Config {URLs : []* url.URL {u }, Transport : transport })
2322+ require .NoError (t , err )
2323+ tp .discoverMu .cond = sync .NewCond (& tp .discoverMu )
2324+ return tp
2325+ }
2326+
2327+ func TestDiscoverNodesBlocking (t * testing.T ) {
2328+ entered := make (chan struct {}, 1 )
2329+ gate := make (chan struct {})
2330+
2331+ routes := mockhttp .GetDefaultHandlers (t )
2332+ routes ["/_nodes/http" ] = gatedNodesHandler (t , entered , gate )
2333+ tp := newGatedDiscoverClient (t , routes )
2334+
2335+ // Goroutine A: start discovery (blocks in handler on gate).
2336+ var wg sync.WaitGroup
2337+ wg .Add (1 )
2338+ go func () {
2339+ defer wg .Done ()
2340+ tp .DiscoverNodes (t .Context ())
2341+ }()
2342+
2343+ // Wait for handler to be entered — discovery is now in-flight.
2344+ <- entered
2345+
2346+ // Goroutine B: should block in DiscoverNodes until A finishes.
2347+ bDone := make (chan error , 1 )
2348+ go func () {
2349+ bDone <- tp .DiscoverNodes (t .Context ())
2350+ }()
2351+
2352+ // B should not have returned yet (gate still closed, A still blocked).
2353+ select {
2354+ case <- bDone :
2355+ t .Fatal ("goroutine B returned before discovery finished" )
2356+ default :
2357+ }
2358+
2359+ // Release A — A finishes, B wakes up.
2360+ close (gate )
2361+ wg .Wait ()
2362+
2363+ err := <- bDone
2364+ require .NoError (t , err , "goroutine B should succeed after waiting" )
2365+ }
2366+
2367+ func TestDiscoverNodesBlockingPropagatesError (t * testing.T ) {
2368+ entered := make (chan struct {}, 1 )
2369+ gate := make (chan struct {})
2370+
2371+ routes := mockhttp .GetDefaultHandlers (t )
2372+ routes ["/_nodes/http" ] = gatedErrorNodesHandler (t , entered , gate )
2373+ tp := newGatedDiscoverClient (t , routes )
2374+
2375+ // Goroutine A: start discovery that will fail.
2376+ aDone := make (chan error , 1 )
2377+ go func () {
2378+ aDone <- tp .DiscoverNodes (t .Context ())
2379+ }()
2380+
2381+ <- entered // A is in handler, discovery in-flight.
2382+
2383+ // Goroutine B: waits for A, receives the same error via lastErr.
2384+ bDone := make (chan error , 1 )
2385+ go func () {
2386+ bDone <- tp .DiscoverNodes (t .Context ())
2387+ }()
2388+
2389+ close (gate )
2390+
2391+ errA := <- aDone
2392+ errB := <- bDone
2393+
2394+ require .Error (t , errA , "runner should report discovery error" )
2395+ require .Error (t , errB , "waiter should receive the same error" )
2396+ require .Equal (t , errA , errB )
2397+ }
2398+
2399+ func TestDiscoverNodesBlockingContextCancel (t * testing.T ) {
2400+ entered := make (chan struct {}, 1 )
2401+ gate := make (chan struct {})
2402+ defer close (gate ) // prevent goroutine leak
2403+
2404+ routes := mockhttp .GetDefaultHandlers (t )
2405+ routes ["/_nodes/http" ] = gatedNodesHandler (t , entered , gate )
2406+ tp := newGatedDiscoverClient (t , routes )
2407+
2408+ // Goroutine A: start slow discovery.
2409+ go func () {
2410+ tp .DiscoverNodes (t .Context ())
2411+ }()
2412+
2413+ <- entered // A is in handler, discovery in-flight.
2414+
2415+ // B: call DiscoverNodes with an already-cancelled context.
2416+ ctx , cancel := context .WithCancel (t .Context ())
2417+ cancel ()
2418+
2419+ err := tp .DiscoverNodes (ctx )
2420+ require .ErrorIs (t , err , context .Canceled )
2421+ }
2422+
2423+ func TestTryDiscoverNodesNonBlocking (t * testing.T ) {
2424+ entered := make (chan struct {}, 1 )
2425+ gate := make (chan struct {})
2426+ defer close (gate ) // prevent goroutine leak
2427+
2428+ routes := mockhttp .GetDefaultHandlers (t )
2429+ routes ["/_nodes/http" ] = gatedNodesHandler (t , entered , gate )
2430+ tp := newGatedDiscoverClient (t , routes )
2431+
2432+ // Start discovery in a goroutine.
2433+ go func () {
2434+ tp .DiscoverNodes (t .Context ())
2435+ }()
2436+
2437+ <- entered // discovery is in-flight.
2438+
2439+ // tryDiscoverNodes should return nil immediately without blocking.
2440+ err := tp .tryDiscoverNodes (t .Context ())
2441+ require .NoError (t , err )
2442+ }
2443+
2444+ func TestDiscoverNodesSequential (t * testing.T ) {
2445+ routes := mockhttp .GetDefaultHandlersWithNodes (t , map [string ][]string {
2446+ "node1" : {"data" , "ingest" },
2447+ })
2448+ tp := newGatedDiscoverClient (t , routes )
2449+
2450+ // First call succeeds.
2451+ err := tp .DiscoverNodes (t .Context ())
2452+ require .NoError (t , err )
2453+
2454+ // Second call also succeeds (starts a fresh discovery).
2455+ err = tp .DiscoverNodes (t .Context ())
2456+ require .NoError (t , err )
2457+ }
0 commit comments