Packages
hackney
4.6.0
4.7.2
4.7.1
4.7.0
4.6.1
4.6.0
4.5.2
4.5.1
4.5.0
4.4.5
4.4.3
4.4.2
4.4.1
4.4.0
4.3.0
4.2.3
4.2.2
4.2.1
4.2.0
4.1.0
4.0.3
4.0.2
4.0.1
4.0.0
3.2.1
3.2.0
3.1.2
3.1.1
3.1.0
3.0.3
3.0.2
3.0.1
3.0.0
retired
2.0.1
2.0.0
2.0.0-beta.1
1.25.0
1.24.1
1.24.0
1.23.0
1.22.0
1.21.0
1.20.1
1.20.0
1.19.1
1.19.0
1.18.2
1.18.1
1.18.0
1.17.4
1.17.3
1.17.2
1.17.1
1.17.0
1.16.0
1.15.2
1.15.1
1.15.0
1.14.3
1.14.2
1.14.0
1.13.0
1.12.1
1.12.0
1.11.0
1.10.1
1.10.0
1.9.0
1.8.6
1.8.5
1.8.4
1.8.3
1.8.2
1.8.0
1.7.1
1.7.0
1.6.6
retired
1.6.5
1.6.4
retired
1.6.3
1.6.2
1.6.1
1.6.0
1.5.7
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.0
1.1.0
1.0.6
1.0.5
1.0.2
1.0.1
0.15.2
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.13.1
Simple HTTP client with HTTP/1.1, HTTP/2, and HTTP/3 support
Current section
Files
Jump to
Current section
Files
src/hackney_load_regulation.erl
%%% -*- erlang -*-
%%%
%%% This file is part of hackney released under the Apache 2 license.
%%% See the NOTICE for more information.
%%%
%%% Copyright (c) 2024-2026 Benoit Chesneau
%%%
%%% @doc Per-host connection load regulation using ETS counting semaphore.
%%%
%%% This module provides per-host connection limits using an atomic
%%% counting semaphore pattern. It tracks the number of active connections
%%% per {Host, Port} and blocks new requests when the limit is reached.
%%%
%%% Usage:
%%% ```
%%% case hackney_load_regulation:acquire(Host, Port, MaxPerHost, Timeout) of
%%% ok ->
%%% try
%%% %% Do work with connection
%%% after
%%% hackney_load_regulation:release(Host, Port)
%%% end;
%%% {error, timeout} ->
%%% {error, checkout_timeout}
%%% end.
%%% '''
-module(hackney_load_regulation).
%% API
-export([
init/0,
acquire/4,
release/2,
current/2,
reset/2
]).
-define(TABLE, hackney_host_limits).
-define(BACKOFF_MS, 10).
%%====================================================================
%% API
%%====================================================================
%% @doc Initialize the load regulation ETS table.
%% Should be called once during application startup.
-spec init() -> ok.
init() ->
case ets:whereis(?TABLE) of
undefined ->
?TABLE = ets:new(?TABLE, [
public,
set,
named_table,
{write_concurrency, true},
{read_concurrency, true}
]),
ok;
_Tid ->
ok
end.
%% @doc Acquire a slot for the given host.
%% Blocks with exponential backoff until a slot is available or timeout.
%% Returns ok if slot acquired, {error, timeout} otherwise.
-spec acquire(Host :: string() | binary(), Port :: inet:port_number(),
MaxPerHost :: pos_integer(), Timeout :: timeout()) ->
ok | {error, timeout}.
acquire(Host, Port, MaxPerHost, Timeout) ->
Key = normalize_key(Host, Port),
Deadline = deadline(Timeout),
acquire_loop(Key, MaxPerHost, Deadline).
%% @doc Release a slot for the given host.
%% Should always be called after acquire, typically in an after block.
-spec release(Host :: string() | binary(), Port :: inet:port_number()) -> ok.
release(Host, Port) ->
Key = normalize_key(Host, Port),
try
_ = ets:update_counter(?TABLE, Key, {2, -1, 0, 0}),
ok
catch
error:badarg ->
%% Key doesn't exist, nothing to release
ok
end.
%% @doc Get the current number of active connections for a host.
-spec current(Host :: string() | binary(), Port :: inet:port_number()) ->
non_neg_integer().
current(Host, Port) ->
Key = normalize_key(Host, Port),
case ets:lookup(?TABLE, Key) of
[{_, Count}] -> max(0, Count);
[] -> 0
end.
%% @doc Reset the counter for a host (for testing).
-spec reset(Host :: string() | binary(), Port :: inet:port_number()) -> ok.
reset(Host, Port) ->
Key = normalize_key(Host, Port),
ets:delete(?TABLE, Key),
ok.
%%====================================================================
%% Internal functions
%%====================================================================
%% @private Normalize host to lowercase binary for consistent keys.
normalize_key(Host, Port) when is_list(Host) ->
normalize_key(list_to_binary(Host), Port);
normalize_key(Host, Port) when is_binary(Host) ->
{string:lowercase(Host), Port}.
%% @private Calculate deadline from timeout.
deadline(infinity) ->
infinity;
deadline(Timeout) when is_integer(Timeout), Timeout >= 0 ->
erlang:monotonic_time(millisecond) + Timeout.
%% @private Check if deadline has passed.
check_deadline(infinity) ->
ok;
check_deadline(Deadline) ->
case erlang:monotonic_time(millisecond) < Deadline of
true -> ok;
false -> timeout
end.
%% @private Main acquire loop with backoff.
acquire_loop(Key, Max, Deadline) ->
%% Atomically increment counter, creating entry if needed
Count = ets:update_counter(?TABLE, Key, {2, 1}, {Key, 0}),
case Count =< Max of
true ->
%% Got a slot
ok;
false ->
%% Over limit - decrement back and retry
_ = ets:update_counter(?TABLE, Key, {2, -1}),
case check_deadline(Deadline) of
ok ->
timer:sleep(?BACKOFF_MS),
acquire_loop(Key, Max, Deadline);
timeout ->
{error, timeout}
end
end.