@@ -62,7 +62,8 @@ end_per_suite(_) ->
6262 max_streams_h2_size_1_retry_least_loaded ,
6363 max_streams_h2_size_2_least_loaded ,
6464 max_streams_h2_size_2_retry_least_loaded ,
65- reconnect_h1_least_loaded
65+ reconnect_h1_least_loaded ,
66+ least_loaded_routing_least_loaded
6667 ],
6768 _ = [cowboy :stop_listener (Listener ) || Listener <- ExtraListeners ],
6869 ok .
@@ -491,7 +492,14 @@ do_degraded_configuration_error(Config) ->
491492least_loaded_routing (Config ) ->
492493 doc (" Confirm the least_loaded strategy always routes to the connection "
493494 " with the fewest active streams." ),
494- Port = config (port , Config ),
495+ % % Use a dedicated listener with a 30s delay so the busy connection
496+ % % cannot free up before the test completes on slow CI runners.
497+ Listener = listener_name (? FUNCTION_NAME , Config ),
498+ Routes = [{" /" , hello_h , []}, {" /delay" , delayed_hello_h , 30000 }],
499+ {ok , _ } = cowboy :start_clear (Listener , [], #{
500+ env => #{dispatch => cowboy_router :compile ([{'_' , Routes }])}
501+ }),
502+ Port = ranch :get_port (Listener ),
495503 Authority = [" localhost:" , integer_to_binary (Port )],
496504 {ok , ManagerPid } = gun_pool :start_pool (" localhost" , Port , #{
497505 conn_opts => #{protocols => [http2 ]},
@@ -504,15 +512,19 @@ least_loaded_routing(Config) ->
504512 {async , {BusyConn , _ }} = gun_pool :get (" /delay" ,
505513 #{<<" host" >> => Authority }, #{scope => scope (? FUNCTION_NAME , Config )}),
506514 % % Send requests one at a time and await each before the next.
507- % % This keeps the idle connection at 0-1 streams, always below the
508- % % busy connection, so least_loaded must consistently pick it.
515+ % % The stream is released asynchronously (the event handler casts
516+ % % {release_stream, _} to the manager), so wait until the idle
517+ % % connection is back to 0 streams before the next request. It is
518+ % % then strictly below the busy connection, so least_loaded must
519+ % % consistently pick it.
509520 % % With random selection this would pass with probability ~0.1%.
510521 _ = [begin
511522 {async , {ConnPid , _ } = PoolStreamRef } = gun_pool :get (" /" ,
512523 #{<<" host" >> => Authority }, #{scope => scope (? FUNCTION_NAME , Config )}),
513524 true = ConnPid =/= BusyConn ,
514525 {response , nofin , 200 , _ } = gun_pool :await (PoolStreamRef ),
515- {ok , <<" Hello world!" >>} = gun_pool :await_body (PoolStreamRef )
526+ {ok , <<" Hello world!" >>} = gun_pool :await_body (PoolStreamRef ),
527+ ok = wait_streams_released_except (ManagerPid , BusyConn )
516528 end || _ <- lists :seq (1 , 10 )].
517529
518530least_loaded_round_robin (Config ) ->
@@ -622,6 +634,25 @@ wait_for_metrics(Authority, Scope, Expected, N) ->
622634 wait_for_metrics (Authority , Scope , Expected , N - 1 )
623635 end .
624636
637+ % % Poll the manager until every connection except Excluded has 0 streams.
638+ wait_streams_released_except (ManagerPid , Excluded ) ->
639+ wait_streams_released_except (ManagerPid , Excluded , 100 ).
640+
641+ wait_streams_released_except (_ManagerPid , _Excluded , 0 ) ->
642+ {error , timeout };
643+ wait_streams_released_except (ManagerPid , Excluded , N ) ->
644+ {_ , #{lookup := #{stream_counts := StreamCounts }}} = gun_pool :info (ManagerPid ),
645+ Released = lists :all (fun ({ConnPid , Count }) ->
646+ ConnPid =:= Excluded orelse Count =:= 0
647+ end , maps :to_list (StreamCounts )),
648+ case Released of
649+ true ->
650+ ok ;
651+ false ->
652+ timer :sleep (10 ),
653+ wait_streams_released_except (ManagerPid , Excluded , N - 1 )
654+ end .
655+
625656% % Poll the manager until every connection's stream count is back to 0.
626657wait_all_streams_released (ManagerPid ) ->
627658 wait_all_streams_released (ManagerPid , 100 ).
0 commit comments