Packages

macula

0.36.5
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
macula src macula_gateway_system macula_gateway_system.erl
Raw

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 MACULA_TLS_CERTFILE environment variable when not provided in opts.
%% Note: v0.16.6 changed from TLS_CERT_FILE to MACULA_TLS_CERTFILE for consistency.
get_cert_file(undefined) ->
case os:getenv("MACULA_TLS_CERTFILE") 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 MACULA_TLS_KEYFILE environment variable when not provided in opts.
%% Note: v0.16.6 changed from TLS_KEY_FILE to MACULA_TLS_KEYFILE for consistency.
get_key_file(undefined) ->
case os:getenv("MACULA_TLS_KEYFILE") 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 ->
Host = macula_connection:hostname_from_node(),
?LOG_INFO("Using node()-derived hostname for node ID: ~s", [Host]),
crypto:hash(sha256, term_to_binary({Realm, Host, 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 the same hostname resolution
%% as macula_gateway:build_gateway_endpoint/1.
%% Priority: MACULA_HOSTNAME > HOSTNAME > net_adm:localhost() > "localhost".
%% Also respects MACULA_ADVERTISE_PORT for Docker port mapping.
get_url(Port) ->
Hostname = get_reachable_hostname(),
AdvertisePort = case os:getenv("MACULA_ADVERTISE_PORT") of
false -> Port;
PortStr -> list_to_integer(PortStr)
end,
iolist_to_binary([<<"https://">>, Hostname, <<":">>, integer_to_binary(AdvertisePort)]).
%% @private
%% @doc Get a network-reachable hostname for this node.
get_reachable_hostname() ->
case os:getenv("MACULA_HOSTNAME") of
false ->
case os:getenv("HOSTNAME") of
false ->
list_to_binary(net_adm:localhost());
Host ->
list_to_binary(Host)
end;
MaculaHostname ->
list_to_binary(MaculaHostname)
end.