2323-export ([info /0 ]).
2424-export ([info /1 ]).
2525-export ([info /2 ]).
26+ -export ([metrics /0 ]).
27+ -export ([metrics /2 ]).
2628-export ([await_up /1 ]).
2729-export ([await_up /2 ]).
2830-export ([checkout /2 ]). % % Use responsibly!
151153 conns :: #{pid () => down | {setup , any ()} | {up , http | http2 | ws | raw , map ()}},
152154 conns_meta = #{} :: meta (),
153155 await_up = [] :: [{pid (), any ()}],
154- lookup :: #{strategy := random , table := ets :tid ()}
156+ lookup :: #{strategy := random , table := ets :tid (),
157+ active := atomics :atomics_ref (), pool_key := {any (), binary ()}}
155158 | #{strategy := least_loaded , available := gb_trees :tree (),
156159 stream_counts := #{pid () => non_neg_integer ()},
157160 seqs := #{pid () => non_neg_integer ()}, next_seq := non_neg_integer (),
158- up_count := non_neg_integer ()}
161+ up_count := non_neg_integer (), manager_pid : = pid () }
159162}).
160163
161164% % Pool management.
@@ -210,6 +213,55 @@ info(Authority, Scope) ->
210213 gen_statem :call (ManagerPid , info )
211214 end .
212215
216+ % % Read pool metrics from ETS without calling the manager (random pools only).
217+ % % Counters may reflect a torn snapshot across connections but no call is made.
218+ -spec metrics () -> [map ()].
219+ metrics () ->
220+ [M || {PoolKey , Row } <- ets :tab2list (gun_pool_counters ),
221+ M <- [metrics_from_row (PoolKey , Row )], M =/= undefined ].
222+
223+ -spec metrics (binary (), any ()) -> undefined | map ().
224+ metrics (Authority , Scope ) ->
225+ metrics_by_key ({Scope , Authority }).
226+
227+ metrics_by_key (PoolKey ) ->
228+ case ets :lookup (gun_pool_counters , PoolKey ) of
229+ [] ->
230+ undefined ;
231+ [{_ , Row }] ->
232+ metrics_from_row (PoolKey , Row )
233+ end .
234+
235+ metrics_from_row ({Scope , _ }, #{table := Tid , max_streams := Max , active := Active , size := Size }) ->
236+ % % The per-pool table is owned by the manager and deleted on terminate.
237+ try
238+ ActiveN = atomics :get (Active , 1 ),
239+ StreamRows = ets :tab2list (Tid ),
240+ Streams = lists :sum ([C || {_ , C } <- StreamRows ]),
241+ {Full , High } = occupancy (StreamRows , Max ),
242+ #{
243+ scope => Scope ,
244+ size => Size ,
245+ active => ActiveN ,
246+ max_streams => Max ,
247+ streams => Streams ,
248+ full => Full ,
249+ high => High
250+ }
251+ catch _ :_ ->
252+ undefined
253+ end .
254+
255+ occupancy (_Rows , infinity ) ->
256+ {0 , 0 };
257+ occupancy (Rows , Max ) ->
258+ HighThreshold = (Max * 9 + 9 ) div 10 ,
259+ lists :foldl (fun ({_ , Count }, {F , H }) ->
260+ F1 = if Count >= Max -> F + 1 ; true -> F end ,
261+ H1 = if Count >= HighThreshold -> H + 1 ; true -> H end ,
262+ {F1 , H1 }
263+ end , {0 , 0 }, Rows ).
264+
213265-spec await_up (pid () | binary ()) -> ok | {error , pool_not_found , atom ()}.
214266await_up (ManagerPid ) when is_pid (ManagerPid ) ->
215267 gen_statem :call (ManagerPid , await_up , 5000 );
@@ -526,13 +578,21 @@ start_link(Host, Port, Opts) ->
526578
527579init ({Host , Port , Opts }) ->
528580 process_flag (trap_exit , true ),
529- true = ets :insert_new (gun_pools , {gun_pools_key (Host , Port , Opts ), self ()}),
581+ PoolKey = gun_pools_key (Host , Port , Opts ),
582+ true = ets :insert_new (gun_pools , {PoolKey , self ()}),
530583 Size = maps :get (size , Opts , 8 ),
531584 % % @todo Only start processes in static mode.
532585 Lookup = case maps :get (lookup_strategy , Opts , random ) of
533586 random ->
534587 Tid = ets :new (gun_pooled_conns , [ordered_set , public ]),
535- #{strategy => random , table => Tid };
588+ Active = atomics :new (1 , [{signed , false }]),
589+ ets :insert (gun_pool_counters , {PoolKey , #{
590+ table => Tid ,
591+ size => Size ,
592+ max_streams => infinity ,
593+ active => Active
594+ }}),
595+ #{strategy => random , table => Tid , active => Active , pool_key => PoolKey };
536596 least_loaded ->
537597 #{strategy => least_loaded , available => gb_trees :empty (),
538598 stream_counts => #{}, seqs => #{}, next_seq => 0 ,
@@ -630,6 +690,7 @@ degraded_setup(ConnPid, Msg, StateData0=#state{conns=Conns, conns_meta=ConnsMeta
630690 % % Websocket or tunnel stream refs.
631691 {up , Protocol , Meta } ->
632692 Settings = #{},
693+ metrics_conn_up (Lookup , Protocol ),
633694 StateData = StateData0 # state {
634695 conns = Conns #{ConnPid => {up , Protocol , Settings }},
635696 conns_meta = ConnsMeta #{ConnPid => Meta },
@@ -681,13 +742,15 @@ handle_common({call, From}, {checkout, _ReqOpts}, _,
681742 end ;
682743handle_common (cast , {release_stream , ConnPid }, _ , StateData ) ->
683744 {keep_state , adjust_stream_count (ConnPid , - 1 , StateData )};
684- handle_common (info , {gun_notify , ConnPid , settings_changed , Settings }, _ , StateData = # state {conns = Conns }) ->
745+ handle_common (info , {gun_notify , ConnPid , settings_changed , Settings }, _ , StateData = # state {conns = Conns , lookup = Lookup }) ->
685746 % % Assert that the state is correct.
686747 {up , http2 , _ } = maps :get (ConnPid , Conns ),
748+ metrics_update_max_streams (Lookup , Settings ),
687749 {keep_state , StateData # state {conns = Conns #{ConnPid => {up , http2 , Settings }}}};
688750handle_common (info , {gun_down , ConnPid , Protocol , _Reason , _KilledStreams }, _ ,
689- StateData = # state {lookup = #{strategy := random }, conns = Conns }) ->
751+ StateData = # state {lookup = #{strategy := random , active : = Active }, conns = Conns }) ->
690752 {up , Protocol , _ } = maps :get (ConnPid , Conns ),
753+ atomics :sub (Active , 1 , 1 ),
691754 {next_state , degraded , StateData # state {conns = Conns #{ConnPid => down }}};
692755handle_common (info , {gun_down , ConnPid , Protocol , _Reason , _KilledStreams }, _ ,
693756 StateData = # state {lookup = #{strategy := least_loaded , available := Available ,
@@ -706,6 +769,15 @@ handle_common(info, {gun_down, ConnPid, Protocol, _Reason, _KilledStreams}, _,
706769 }};
707770% % @todo We do not want to reconnect automatically when the pool is dynamic.
708771handle_common (info , {'DOWN' , _MRef , process , ConnPid0 , Reason }, _ , StateData0 ) ->
772+ % % gun_down fires before DOWN in normal cases and already decrements active.
773+ % % Guard against sudden process death (no gun_down) where the conn is still up.
774+ _ = case {maps :get (ConnPid0 , StateData0 # state .conns , undefined ),
775+ StateData0 # state .lookup } of
776+ {{up , _ , _ }, #{strategy := random , active := Active }} ->
777+ atomics :sub (Active , 1 , 1 );
778+ _ ->
779+ ok
780+ end ,
709781 StateData = # state {host = Host , port = Port , opts = Opts , lookup = Lookup , conns = Conns }
710782 = remove_down_conn (ConnPid0 , StateData0 ),
711783 case Reason of
@@ -873,5 +945,29 @@ terminate(Reason, StateName, #state{host=Host, port=Port, opts=Opts, await_up=Aw
873945 gen_statem :reply ([
874946 {reply , ReplyTo , {error , {terminate , StateName , Reason }}}
875947 || ReplyTo <- AwaitUp ]),
876- true = ets :delete (gun_pools , gun_pools_key (Host , Port , Opts )),
948+ PoolKey = gun_pools_key (Host , Port , Opts ),
949+ true = ets :delete (gun_pools , PoolKey ),
950+ ets :delete (gun_pool_counters , PoolKey ),
877951 ok .
952+
953+ metrics_conn_up (#{strategy := random , active := Active , pool_key := PoolKey }, http ) ->
954+ atomics :add (Active , 1 , 1 ),
955+ update_pool_counter_max_streams (PoolKey , 1 );
956+ metrics_conn_up (#{strategy := random , active := Active }, _Protocol ) ->
957+ atomics :add (Active , 1 , 1 );
958+ metrics_conn_up (_Lookup , _Protocol ) ->
959+ ok .
960+
961+ metrics_update_max_streams (#{strategy := random , pool_key := PoolKey }, Settings ) ->
962+ MaxStreams = maps :get (max_concurrent_streams , Settings , infinity ),
963+ update_pool_counter_max_streams (PoolKey , MaxStreams );
964+ metrics_update_max_streams (_Lookup , _Settings ) ->
965+ ok .
966+
967+ update_pool_counter_max_streams (PoolKey , MaxStreams ) ->
968+ case ets :lookup (gun_pool_counters , PoolKey ) of
969+ [{_ , Row }] ->
970+ ets :insert (gun_pool_counters , {PoolKey , Row #{max_streams => MaxStreams }});
971+ [] ->
972+ ok
973+ end .
0 commit comments