Packages
macula
0.14.3
7.1.0
7.0.0
6.0.0
5.2.2
5.2.1
5.2.0
5.1.0
5.0.0
4.8.0
4.7.1
4.7.0
4.6.0
4.5.0
4.4.10
4.4.9
4.4.8
4.4.7
4.4.6
4.4.5
4.4.4
4.4.3
4.4.2
4.4.1
4.4.0
4.3.1
4.3.0
4.2.9
4.2.8
4.2.7
4.2.6
4.2.5
4.2.4
4.2.3
4.2.2
4.2.1
4.2.0
4.1.1
4.1.0
4.0.0
3.16.0
3.15.3
3.15.2
3.15.1
3.14.0
3.13.0
3.12.1
3.12.0
3.11.1
3.11.0
3.10.3
3.10.2
3.10.1
3.9.0
3.8.0
3.7.0
3.5.0
3.4.0
3.3.0
3.2.0
3.1.0
3.0.0
2.1.1
2.1.0
2.0.0
1.5.2
1.5.1
1.4.30
1.4.29
1.4.28
1.4.27
1.4.26
1.4.25
1.4.24
1.4.23
1.4.22
1.4.21
1.4.20
1.4.19
1.4.18
1.4.17
1.4.16
1.4.15
1.4.14
1.4.13
1.4.11
1.4.10
1.4.9
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.1
1.3.0
1.2.0
1.1.0
1.0.10
1.0.9
1.0.8
1.0.7
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.48.6
0.48.5
0.48.4
0.48.3
0.48.2
0.48.1
0.48.0
0.47.1
0.47.0
0.46.3
0.46.1
0.46.0
0.45.3
0.45.2
0.45.1
0.45.0
0.44.2
0.44.1
0.44.0
0.43.3
0.43.2
0.43.1
0.43.0
0.42.9
0.42.8
0.42.7
0.42.6
0.42.5
0.42.4
0.42.3
0.42.2
0.42.1
0.42.0
0.41.1
0.41.0
0.40.1
0.40.0
0.39.9
0.39.8
0.39.7
0.39.6
0.39.5
0.39.4
0.39.3
0.39.2
0.39.1
0.39.0
0.38.8
0.38.7
0.38.6
0.38.5
0.38.4
0.38.3
0.38.2
0.38.1
0.38.0
0.37.7
0.37.6
0.37.5
0.37.4
0.37.3
0.37.2
0.37.1
0.37.0
0.36.6
0.36.5
0.36.4
0.36.3
0.36.2
0.36.1
0.36.0
0.35.4
0.35.3
0.35.2
0.35.1
0.35.0
0.34.1
0.34.0
0.33.1
0.33.0
0.32.5
0.32.4
0.32.3
0.32.2
0.32.1
0.32.0
0.31.9
0.31.8
0.31.7
0.31.6
0.31.5
0.31.4
0.31.3
0.31.2
0.31.1
0.31.0
0.30.10
0.30.9
0.30.8
0.30.7
0.30.6
0.30.5
0.30.4
0.30.3
0.30.2
0.30.1
0.30.0
0.29.0
0.28.3
0.28.2
0.28.1
0.28.0
0.27.1
0.27.0
0.26.1
0.26.0
0.25.6
0.25.5
0.25.4
0.25.3
0.25.2
0.25.1
0.25.0
0.24.6
0.24.5
0.24.4
0.24.3
0.24.2
0.24.1
0.24.0
0.23.3
0.23.2
0.23.1
0.23.0
0.22.12
0.22.11
0.22.10
0.22.9
0.22.8
0.22.7
0.22.6
0.22.5
0.22.4
0.22.3
0.22.2
0.22.1
0.22.0
0.21.7
0.21.6
0.21.5
0.21.4
0.21.2
0.21.1
0.21.0
0.20.25
0.20.24
0.20.23
0.20.22
0.20.21
0.20.20
0.20.19
0.20.18
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.3
0.20.2
0.20.1
0.20.0
0.19.2
0.19.1
0.19.0
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.6
0.16.5
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.1
0.15.0
0.14.3
0.14.2
0.14.1
0.14.0
0.12.6
0.12.5
0.12.3
0.11.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.30
0.7.29
0.7.28
0.7.27
0.7.26
0.7.25
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.4
0.3.3
0.3.2
0.3.1
Macula HTTP/3 Mesh SDK — connect, subscribe, publish, call, advertise
Current section
Files
Jump to
Current section
Files
src/macula_gateway_system/macula_gateway_system.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% Gateway Root Supervisor - top-level supervisor for gateway subsystem.
%%%
%%% Supervision Strategy:
%%% - rest_for_one: Dependency-based restart ordering
%%% - Child order reflects dependencies:
%%% 1. quic_server (owns QUIC listener, no dependencies)
%%% 2. gateway (depends on quic_server PID)
%%% 3. workers_sup (depends on gateway PID)
%%%
%%% Fault Isolation:
%%% - quic_server crash → restart quic_server, gateway, workers_sup
%%% - gateway crash → restart gateway, workers_sup (quic_server continues)
%%% - workers_sup crash → restart workers_sup only (quic_server and gateway continue)
%%%
%%% Architecture:
%%% <pre>
%%% macula_gateway_system (this module)
%%% ├── macula_gateway_health - Health check HTTP server
%%% ├── macula_gateway_diagnostics - Diagnostics service
%%% ├── macula_gateway_quic_server - QUIC transport layer
%%% ├── macula_gateway - Message routing coordinator
%%% └── macula_gateway_workers_sup - Business logic workers
%%% ├── macula_gateway_clients - Client tracking
%%% ├── macula_gateway_pubsub - Pub/Sub routing
%%% ├── macula_gateway_rpc - RPC handling
%%% └── macula_gateway_mesh - Mesh connections
%%% </pre>
%%%
%%% Circular Dependency Resolution:
%%% - quic_server starts first (without gateway PID)
%%% - gateway starts second (receives quic_server PID)
%%% - Supervisor calls quic_server:set_gateway/1 to complete link
%%% - workers_sup starts last (receives gateway PID)
%%%
%%% Created during Phase 2 QUIC refactoring to enable proper OTP supervision.
%%% @end
%%%-------------------------------------------------------------------
-module(macula_gateway_system).
-behaviour(supervisor).
-include_lib("kernel/include/logger.hrl").
%% API
-export([start_link/1]).
%% Supervisor callbacks
-export([init/1]).
%%%===================================================================
%%% API
%%%===================================================================
%% @doc Start the root gateway supervisor with configuration.
-spec start_link(proplists:proplist()) -> {ok, pid()} | {error, term()}.
start_link(Opts) ->
supervisor:start_link(?MODULE, Opts).
%%%===================================================================
%%% Supervisor callbacks
%%%===================================================================
init(Opts) when is_list(Opts) ->
%% Called with proplist (legacy tests, application startup)
Port = proplists:get_value(port, Opts, 9443),
Realm = proplists:get_value(realm, Opts, <<"macula.default">>),
HealthPort = proplists:get_value(health_port, Opts, 8080),
CertFile = get_cert_file(proplists:get_value(cert_file, Opts)),
KeyFile = get_key_file(proplists:get_value(key_file, Opts)),
init_supervisor(Port, Realm, HealthPort, CertFile, KeyFile);
init(Opts) when is_map(Opts) ->
%% Called with map (new style)
Port = maps:get(port, Opts, 9443),
Realm = maps:get(realm, Opts, <<"macula.default">>),
HealthPort = maps:get(health_port, Opts, 8080),
CertFile = get_cert_file(maps:get(cert_file, Opts, undefined)),
KeyFile = get_key_file(maps:get(key_file, Opts, undefined)),
init_supervisor(Port, Realm, HealthPort, CertFile, KeyFile).
%% @private
%% @doc Initialize the supervisor with extracted configuration.
init_supervisor(Port, Realm, HealthPort, CertFile, KeyFile) ->
?LOG_INFO("Initializing gateway supervisor for realm ~s on port ~p",
[Realm, Port]),
?LOG_INFO("Certificate file: ~p", [CertFile]),
?LOG_INFO("Key file: ~p", [KeyFile]),
%% Compute node_id and url early so workers can use them
NodeId = get_node_id(Realm, Port),
Url = get_url(Port),
?LOG_INFO("NodeId: ~s", [binary:encode_hex(NodeId)]),
?LOG_INFO("Endpoint URL: ~s", [Url]),
%% Supervision strategy: rest_for_one
%% - health fails → restart health, diagnostics, quic_server, gateway, workers_sup
%% - diagnostics fails → restart diagnostics, quic_server, gateway, workers_sup
%% - quic_server fails → restart quic_server, gateway, workers_sup
%% - gateway fails → restart gateway, workers_sup
%% - workers_sup fails → restart workers_sup only
SupFlags = #{
strategy => rest_for_one,
intensity => 10,
period => 60
},
%% Child 1: Health Check Server
HealthSpec = #{
id => macula_gateway_health,
start => {macula_gateway_health, start_link, [[{health_port, HealthPort}]]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [macula_gateway_health]
},
%% Child 2: Diagnostics Service
DiagnosticsSpec = #{
id => macula_gateway_diagnostics,
start => {macula_gateway_diagnostics, start_link, [[{realm, Realm}]]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [macula_gateway_diagnostics]
},
%% Child 3: QUIC Server (starts without gateway PID)
QuicServerSpec = #{
id => macula_gateway_quic_server,
start => {macula_gateway_quic_server, start_link, [[
{port, Port},
{realm, Realm},
{cert_file, CertFile},
{key_file, KeyFile}
%% Note: NO gateway PID yet!
]]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [macula_gateway_quic_server]
},
%% Child 4: Gateway (receives quic_server PID after it starts)
GatewaySpec = #{
id => macula_gateway,
start => {macula_gateway, start_link, [[
{port, Port},
{realm, Realm}
%% Note: Gateway will find quic_server via supervisor
]]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [macula_gateway]
},
%% Child 5: Workers Supervisor (supervises business logic workers)
WorkersSupSpec = #{
id => macula_gateway_workers_sup,
start => {macula_gateway_workers_sup, start_link, [#{
port => Port,
realm => Realm,
node_id => NodeId,
url => Url
}]},
restart => permanent,
shutdown => infinity, % supervisor shutdown
type => supervisor,
modules => [macula_gateway_workers_sup]
},
Children = [HealthSpec, DiagnosticsSpec, QuicServerSpec, GatewaySpec, WorkersSupSpec],
{ok, {SupFlags, Children}}.
%%%===================================================================
%%% Private helper functions
%%%===================================================================
%% @private
%% @doc Get certificate file path from opts or OS environment variable.
%% Falls back to TLS_CERT_FILE environment variable when not provided in opts.
get_cert_file(undefined) ->
case os:getenv("TLS_CERT_FILE") of
false ->
%% Use macula_tls default path (auto-generated if missing)
{CertPath, _KeyPath} = macula_tls:get_cert_paths(),
CertPath;
CertFile -> CertFile
end;
get_cert_file(CertFile) when is_list(CertFile) ->
CertFile.
%% @private
%% @doc Get key file path from opts or OS environment variable.
%% Falls back to TLS_KEY_FILE environment variable when not provided in opts.
get_key_file(undefined) ->
case os:getenv("TLS_KEY_FILE") of
false ->
%% Use macula_tls default path (auto-generated if missing)
{_CertPath, KeyPath} = macula_tls:get_cert_paths(),
KeyPath;
KeyFile -> KeyFile
end;
get_key_file(KeyFile) when is_list(KeyFile) ->
KeyFile.
%% @private
%% @doc Get node ID from HOSTNAME env var (set by Docker) or generate from {Realm, Port}.
%% Returns a 32-byte binary (raw binary for Kademlia, never hex-encoded).
%% MUST match macula_gateway:get_node_id/2 exactly!
%%
%% Priority:
%% 1. NODE_NAME env var (explicit, highest priority)
%% 2. HOSTNAME env var (Docker sets this to container hostname - unique per container)
%% 3. Fallback to {Realm, Port} only (NO MAC - MAC is shared across Docker containers)
get_node_id(Realm, Port) ->
case os:getenv("NODE_NAME") of
false ->
%% No NODE_NAME, try HOSTNAME (Docker sets this to container hostname)
case os:getenv("HOSTNAME") of
false ->
%% No HOSTNAME either, use {Realm, Port} as last resort
%% Note: This WILL collide if multiple nodes share same realm+port
?LOG_WARNING("No HOSTNAME or NODE_NAME set, using realm+port only"),
?LOG_WARNING("This may cause node_id collisions in Docker!"),
crypto:hash(sha256, term_to_binary({Realm, Port}));
Hostname when is_list(Hostname) ->
%% Use HOSTNAME from Docker - unique per container
?LOG_INFO("Using HOSTNAME-based node ID: ~s, Realm=~s, Port=~p",
[Hostname, Realm, Port]),
crypto:hash(sha256, term_to_binary({Realm, list_to_binary(Hostname), Port}))
end;
NodeName when is_list(NodeName) ->
%% Use NODE_NAME from environment - hash it to get 32-byte binary
?LOG_INFO("Using NODE_NAME from environment: ~s", [NodeName]),
crypto:hash(sha256, list_to_binary(NodeName))
end.
%% @private
%% @doc Get endpoint URL for this gateway.
%% Constructs "https://hostname:port" using HOSTNAME env var or "localhost".
get_url(Port) ->
Hostname = case os:getenv("HOSTNAME") of
false -> "localhost";
Host when is_list(Host) -> Host
end,
iolist_to_binary([<<"https://">>, list_to_binary(Hostname), <<":">>, integer_to_binary(Port)]).