Skip to content

hb_http_client_sup.erl - HTTP Client Supervisor

Overview

Purpose: Supervise the HTTP client gen_server
Module: hb_http_client_sup
Behavior: supervisor
Strategy: one_for_one
Supervision: Single worker (hb_http_client)

This module implements a simple OTP supervisor for the HTTP client connection manager. It ensures the HTTP client gen_server is running and restarts it if it crashes, maintaining HTTP connectivity for the HyperBEAM node.

Supervision Strategy

Strategy: one_for_one
Max Restarts: 5
Time Window: 10 seconds
Shutdown Timeout: 10-30 seconds (depends on build mode)

{one_for_one, MaxRestarts, TimeWindow}

If hb_http_client crashes more than 5 times in 10 seconds, the supervisor terminates.

Dependencies

  • OTP: supervisor
  • Worker: hb_http_client

Module Defines

Shutdown Timeout

-ifdef(DEBUG).
-define(SHUTDOWN_TIMEOUT, 10000).  % 10 seconds in debug mode
-else.
-define(SHUTDOWN_TIMEOUT, 30000).  % 30 seconds in production
-endif.

Purpose: Allow sufficient time for graceful shutdown of Gun connections and pending requests.

Debug Mode: Faster shutdown for development Production Mode: Longer timeout to ensure clean connection closure


Child Specification Macro

-define(CHILD(I, Type, Opts), 
    {
        I,                          % Child ID
        {I, start_link, Opts},      % Start function
        permanent,                  % Restart policy
        ?SHUTDOWN_TIMEOUT,          % Shutdown timeout
        Type,                       % Child type (worker/supervisor)
        [I]                         % Modules
    }
).
Parameters:
  • I - Module name (used as ID)
  • Type - worker or supervisor
  • Opts - Start arguments

Public Functions Overview

%% Supervisor Callbacks
-spec start_link(Opts) -> {ok, pid()} | {error, Reason}.
-spec init(Opts) -> {ok, {SupervisorSpec, ChildSpecs}}.

Public Functions

1. start_link/1

-spec start_link(Opts) -> {ok, pid()} | {error, Reason}
    when
        Opts :: list(),
        Reason :: term().

Description: Start the HTTP client supervisor with given options.

Registration: Registered locally as hb_http_client_sup

Test Code:
-module(hb_http_client_sup_test).
-include_lib("eunit/include/eunit.hrl").
 
start_link_test() ->
    %% Stop if already running
    case whereis(hb_http_client_sup) of
        undefined -> ok;
        ExistingPid -> 
            unlink(ExistingPid),
            exit(ExistingPid, shutdown),
            timer:sleep(100)
    end,
    
    {ok, Pid} = hb_http_client_sup:start_link([#{}]),
    ?assert(is_pid(Pid)),
    ?assertEqual(Pid, whereis(hb_http_client_sup)),
    
    %% Verify child is running
    Children = supervisor:which_children(hb_http_client_sup),
    ?assertMatch([{hb_http_client, _, worker, [hb_http_client]}], Children),
    
    %% Cleanup - unlink before killing to avoid test crash
    unlink(Pid),
    exit(Pid, shutdown),
    timer:sleep(50).

2. init/1

-spec init(Opts) -> {ok, {SupervisorSpec, ChildSpecs}}
    when
        Opts :: list(),
        SupervisorSpec :: {RestartStrategy, MaxRestarts, TimeWindow},
        RestartStrategy :: one_for_one,
        MaxRestarts :: non_neg_integer(),
        TimeWindow :: non_neg_integer(),
        ChildSpecs :: [ChildSpec].

Description: Initialize supervisor with child specification for hb_http_client.

Test Code:
init_spec_test() ->
    Opts = [#{}],
    {ok, {SupSpec, ChildSpecs}} = hb_http_client_sup:init(Opts),
    
    %% Verify supervisor spec
    ?assertMatch({one_for_one, 5, 10}, SupSpec),
    
    %% Verify child specs
    ?assertEqual(1, length(ChildSpecs)),
    [ChildSpec] = ChildSpecs,
    ?assertMatch({hb_http_client, {hb_http_client, start_link, _}, permanent, _, worker, [hb_http_client]}, ChildSpec).
 
child_restart_test() ->
    %% Stop if already running
    case whereis(hb_http_client_sup) of
        undefined -> ok;
        ExistingPid -> 
            unlink(ExistingPid),
            exit(ExistingPid, shutdown),
            timer:sleep(100)
    end,
    
    {ok, SupPid} = hb_http_client_sup:start_link([#{}]),
    
    %% Get original child PID
    [{_, ChildPid1, _, _}] = supervisor:which_children(hb_http_client_sup),
    ?assert(is_pid(ChildPid1)),
    
    %% Kill child
    exit(ChildPid1, kill),
    timer:sleep(100),
    
    %% Verify new child started
    [{_, ChildPid2, _, _}] = supervisor:which_children(hb_http_client_sup),
    ?assertNotEqual(ChildPid1, ChildPid2),
    
    %% Cleanup
    unlink(SupPid),
    exit(SupPid, shutdown),
    timer:sleep(50).
Implementation:
init(Opts) ->
    {ok, {{one_for_one, 5, 10}, [?CHILD(hb_http_client, worker, Opts)]}}.
Supervisor Spec:
{
    one_for_one,  % Strategy: restart only failed child
    5,            % Max 5 restarts
    10            % Within 10 seconds
}
Child Spec:
{
    hb_http_client,                      % Child ID
    {hb_http_client, start_link, Opts},  % Start: hb_http_client:start_link(Opts)
    permanent,                           % Restart: always restart if terminated
    ?SHUTDOWN_TIMEOUT,                   % Shutdown: 10s or 30s
    worker,                              % Type: worker process
    [hb_http_client]                     % Modules: for code upgrades
}

Supervision Tree

hb_http_client_sup (one_for_one)
    └── hb_http_client (gen_server, permanent)
Characteristics:
  • Single child worker
  • Automatic restart on crash
  • Clean shutdown with timeout
  • Connection pool managed by child

Restart Policies

Child Restart: permanent

The hb_http_client child uses permanent restart policy:

  • Normal Termination: Supervisor restarts child
  • Shutdown: Supervisor restarts child
  • Crash: Supervisor restarts child

This ensures HTTP client is always available.


Supervisor Restart Intensity

{one_for_one, 5, 10}

If the child crashes more than 5 times in 10 seconds:

  1. Supervisor terminates itself
  2. Parent supervisor (if any) handles supervisor failure
  3. Prevents restart loop on persistent errors

Shutdown Behavior

Graceful Shutdown

  1. Supervisor sends shutdown signal to hb_http_client
  2. Child has ?SHUTDOWN_TIMEOUT to clean up:
    • Close Gun connections
    • Reply to pending requests with errors
    • Save state if needed
  3. If timeout expires, supervisor kills child forcefully

Timeout Values

Build ModeTimeoutUse Case
Debug10sFast iteration during development
Production30sEnsure all connections close cleanly

Common Patterns

%% Start supervisor (typically in application supervisor)
-module(my_app_sup).
-behaviour(supervisor).
 
init([]) ->
    Children = [
        #{
            id => hb_http_client_sup,
            start => {hb_http_client_sup, start_link, [[#{port => 8080}]]},
            restart => permanent,
            type => supervisor
        }
        % ... other children
    ],
    {ok, {{one_for_one, 10, 60}, Children}}.
 
%% Check if HTTP client is running
IsRunning = whereis(hb_http_client) =/= undefined.
 
%% Get supervisor info
supervisor:which_children(hb_http_client_sup).
% Returns: [{hb_http_client, Pid, worker, [hb_http_client]}]
 
%% Get supervisor status
supervisor:count_children(hb_http_client_sup).
% Returns: [{specs, 1}, {active, 1}, {supervisors, 0}, {workers, 1}]

Configuration

The supervisor passes options directly to hb_http_client:start_link/1:

Opts = [#{
    % HTTP client options
    http_client => gun | httpc,
    port => 8734,
    prometheus => true,
    
    % Connection options
    connect_timeout => 5000,
    http_request_send_timeout => 30000,
    
    % Any other options for hb_http_client
}].
 
{ok, Pid} = hb_http_client_sup:start_link(Opts).

Monitoring & Debugging

Check Supervisor Status

% Get supervisor PID
SupPid = whereis(hb_http_client_sup).
 
% Check children
Children = supervisor:which_children(hb_http_client_sup).
% [{hb_http_client, <0.123.0>, worker, [hb_http_client]}]
 
% Count children
Counts = supervisor:count_children(hb_http_client_sup).
% [{specs,1},{active,1},{supervisors,0},{workers,1}]
 
% Restart child (for debugging)
supervisor:terminate_child(hb_http_client_sup, hb_http_client).
supervisor:restart_child(hb_http_client_sup, hb_http_client).

Check Restart Statistics

% Check if supervisor restarted recently
sys:get_status(hb_http_client_sup).
 
% Check child's restart history via supervisor
% (OTP tracks this internally)

Error Handling

Child Crashes

% Scenario: hb_http_client crashes
1. Supervisor detects child exit
2. Increments restart counter
3. If under restart intensity limit:
   - Starts new hb_http_client process
   - Passes same Opts
   - New process initializes fresh state
4. If over restart intensity limit:
   - Supervisor terminates itself
   - Parent supervisor handles failure

Max Restart Intensity Reached

% When child crashes > 5 times in 10 seconds:
% 
% 1. Supervisor logs: "Too many restarts"
% 2. Supervisor terminates
% 3. If under parent supervisor:
%    - Parent restarts hb_http_client_sup
%    - Fresh restart counter
% 4. If no parent:
%    - Application may crash

Best Practices

  1. Always Start Under Supervision: Never start hb_http_client directly
  2. Handle Restart Loops: Monitor restart intensity in production
  3. Graceful Shutdown: Ensure child respects shutdown timeout
  4. Configuration: Pass all options through supervisor
  5. Monitoring: Use observer or metrics to track restarts
  6. Testing: Test both normal operation and crash scenarios
  7. Logging: Log supervisor events for debugging

Integration

Application Supervisor Integration

-module(my_app_sup).
-behaviour(supervisor).
 
init([]) ->
    SupFlags = #{
        strategy => one_for_one,
        intensity => 10,
        period => 60
    },
    
    Children = [
        #{
            id => hb_http_client_sup,
            start => {hb_http_client_sup, start_link, [[#{
                prometheus => true,
                port => 8734
            }]]},
            restart => permanent,
            shutdown => infinity,  % Supervisor shutdown
            type => supervisor,
            modules => [hb_http_client_sup]
        }
        % ... other children
    ],
    
    {ok, {SupFlags, Children}}.

References


Notes

  1. Simple Design: Single-child supervisor for HTTP client
  2. Restart Policy: Child always restarted (permanent)
  3. Shutdown Timeout: Longer in production for clean connection closure
  4. Debug Mode: Faster shutdown for development iteration
  5. Restart Intensity: 5 restarts in 10 seconds before supervisor terminates
  6. One-For-One: Only failed child restarted (N/A with single child)
  7. Child Type: Worker process (not supervisor)
  8. Registration: Supervisor registered as hb_http_client_sup
  9. Options: Passed through to child's start_link
  10. No State: Supervisor is stateless; child maintains connection pool
  11. Monitoring: OTP supervisor automatically monitors child
  12. Upgrade: Supports code upgrades via modules specification
  13. Parent: Typically under application supervisor tree
  14. Testing: Easy to test with EUnit (start/stop supervisor)
  15. Simplicity: Minimal supervisor for maximal reliability