Packages
macula
0.7.16
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_service_registry.erl
%%%-------------------------------------------------------------------
%%% @doc
%%% Decentralized service advertisement registry using DHT.
%%%
%%% Provides service discovery via Kademlia DHT instead of centralized
%%% registration. Services advertise their capabilities to the DHT,
%%% and clients discover providers by querying the DHT.
%%%
%%% == Architecture ==
%%%
%%% - Services advertise: "I provide procedure X" → DHT stores node_id at key=hash(procedure)
%%% - Clients discover: "Who provides procedure X?" → DHT returns list of node_ids
%%% - Local cache: Recent discoveries cached with TTL for low-latency lookups
%%% - Re-advertisement: Periodic republish to DHT for TTL renewal (default: every 5 min)
%%%
%%% == Features ==
%%%
%%% - Fully decentralized (no central authority)
%%% - Multiple providers supported (DHT returns list)
%%% - Load balancing (client picks from list)
%%% - Fault tolerant (try another provider if one fails)
%%% - Low latency after first lookup (local cache)
%%%
%%% @end
%%%-------------------------------------------------------------------
-module(macula_service_registry).
%% API
-export([
new/0,
new/1,
%% Local service management
advertise_local/4,
unadvertise_local/2,
get_local_handler/2,
list_local_services/1,
%% Service discovery (with cache)
discover_service/2,
discover_service/3,
cache_service/4,
%% Subscriber discovery (pub/sub cache)
discover_subscribers/2,
cache_subscribers/4,
prune_expired_subscribers/1,
clear_subscriber_cache/1,
%% DHT integration
publish_to_dht/5,
query_dht_for_service/3,
remove_from_dht/3,
%% Cache management
prune_expired/1,
clear_cache/1,
%% Local service cleanup
prune_expired_local_services/1
]).
%% Types
-type service_id() :: binary().
%% Service identifier (procedure URI). Example: <<"energy.home.get">>.
-type node_id() :: binary().
%% 32-byte node identifier.
-type handler_fn() :: fun((map()) -> {ok, term()} | {error, term()}).
%% Handler function for local service implementations.
-type provider_info() :: #{
node_id := node_id(),
endpoint := binary(), % Connection endpoint
metadata := map(), % Custom metadata
advertised_at := integer() % Unix timestamp
}.
-type cache_entry() :: #{
service_id := service_id(),
providers := [provider_info()],
cached_at := integer(), % Unix timestamp
ttl := pos_integer() % Seconds
}.
-type local_service() :: #{
service_id := service_id(),
handler := handler_fn(),
metadata := map(),
advertised_at := integer()
}.
-type registry() :: #{
%% Local services this node provides
local_services := #{service_id() => local_service()},
%% Discovery cache: #{ServiceId => CacheEntry}
cache := #{service_id() => cache_entry()},
%% Subscriber cache: #{Topic => CacheEntry}
subscriber_cache := #{binary() => cache_entry()},
%% Configuration
default_ttl := pos_integer(), % Default TTL in seconds
cache_ttl := pos_integer(), % How long to cache discoveries
service_ttl := pos_integer() % TTL for local services (cleanup)
}.
-export_type([
service_id/0,
node_id/0,
handler_fn/0,
provider_info/0,
registry/0
]).
-include("macula_config.hrl").
%%%===================================================================
%%% API Functions
%%%===================================================================
%% @doc Create new empty service registry with default settings.
-spec new() -> registry().
new() ->
new(#{}).
%% @doc Create new service registry with custom options.
%%
%% Options:
%% - `default_ttl' - Default TTL for DHT advertisements (default: 300s)
%% - `cache_ttl' - How long to cache discovered services (default: 60s)
%% - `service_ttl' - TTL for local services before cleanup (default: 300s, 5 minutes)
-spec new(map()) -> registry().
new(Opts) ->
#{
local_services => #{},
cache => #{},
subscriber_cache => #{},
default_ttl => maps:get(default_ttl, Opts, ?DEFAULT_TTL),
cache_ttl => maps:get(cache_ttl, Opts, ?CACHE_TTL),
service_ttl => maps:get(service_ttl, Opts, 300) % 5 minutes default
}.
%% @doc Advertise a service locally (stores handler for incoming calls).
%%
%% This registers the service handler locally so this node can respond
%% to incoming RPC calls. The actual DHT advertisement must be done
%% separately (see `publish_to_dht/4').
-spec advertise_local(registry(), service_id(), handler_fn(), map()) -> registry().
advertise_local(#{local_services := Services} = Registry, ServiceId, Handler, Metadata) ->
LocalService = #{
service_id => ServiceId,
handler => Handler,
metadata => Metadata,
advertised_at => erlang:system_time(second)
},
NewServices = Services#{ServiceId => LocalService},
Registry#{local_services => NewServices}.
%% @doc Remove a local service advertisement.
-spec unadvertise_local(registry(), service_id()) -> registry().
unadvertise_local(#{local_services := Services} = Registry, ServiceId) ->
NewServices = maps:remove(ServiceId, Services),
Registry#{local_services => NewServices}.
%% @doc Get handler function for a locally advertised service.
-spec get_local_handler(registry(), service_id()) -> {ok, handler_fn()} | not_found.
get_local_handler(#{local_services := Services}, ServiceId) ->
get_handler_from_service(maps:get(ServiceId, Services, undefined)).
get_handler_from_service(undefined) ->
not_found;
get_handler_from_service(#{handler := Handler}) ->
{ok, Handler}.
%% @doc List all locally advertised services.
-spec list_local_services(registry()) -> [service_id()].
list_local_services(#{local_services := Services}) ->
maps:keys(Services).
%% @doc Discover service providers (checks cache first, returns cached if available).
-spec discover_service(registry(), service_id()) ->
{ok, [provider_info()], registry()} | {cache_miss, registry()}.
discover_service(Registry, ServiceId) ->
discover_service(Registry, ServiceId, #{}).
%% @doc Discover service providers with options.
%%
%% Checks local cache first. If found and not expired, returns cached providers.
%% If cache miss or expired, returns `{cache_miss, Registry}' so caller can
%% query DHT.
%%
%% Options:
%% - `force_refresh' - Skip cache, force DHT lookup (default: false)
-spec discover_service(registry(), service_id(), map()) ->
{ok, [provider_info()], registry()} | {cache_miss, registry()}.
discover_service(Registry, _ServiceId, #{force_refresh := true}) ->
{cache_miss, Registry};
discover_service(#{cache := Cache, cache_ttl := CacheTTL} = Registry, ServiceId, _Opts) ->
check_service_cache(maps:get(ServiceId, Cache, undefined), CacheTTL, Registry).
%% Check cache entry and return result based on expiry
check_service_cache(undefined, _CacheTTL, Registry) ->
{cache_miss, Registry};
check_service_cache(#{cached_at := CachedAt, providers := Providers}, CacheTTL, Registry) ->
Now = erlang:system_time(second),
Age = Now - CachedAt,
check_cache_expiry(Age >= CacheTTL, Providers, Registry).
%% Return result based on whether cache is expired
check_cache_expiry(true, _Providers, Registry) ->
{cache_miss, Registry};
check_cache_expiry(false, Providers, Registry) ->
{ok, Providers, Registry}.
%% @doc Cache discovered service providers.
%%
%% Stores providers in local cache with TTL. Subsequent `discover_service'
%% calls will return cached results until TTL expires.
-spec cache_service(registry(), service_id(), [provider_info()], pos_integer()) -> registry().
cache_service(#{cache := Cache} = Registry, ServiceId, Providers, TTL) ->
CacheEntry = #{
service_id => ServiceId,
providers => Providers,
cached_at => erlang:system_time(second),
ttl => TTL
},
NewCache = Cache#{ServiceId => CacheEntry},
Registry#{cache => NewCache}.
%% @doc Remove expired entries from discovery cache.
%%
%% Should be called periodically to prevent memory leaks.
%% Returns updated registry and count of removed entries.
-spec prune_expired(registry()) -> {registry(), non_neg_integer()}.
prune_expired(#{cache := Cache, cache_ttl := CacheTTL} = Registry) ->
Now = erlang:system_time(second),
{NewCache, Removed} = maps:fold(
fun(ServiceId, CacheEntry, {Acc, Count}) ->
CachedAt = maps:get(cached_at, CacheEntry),
Age = Now - CachedAt,
if
Age >= CacheTTL ->
%% Expired - don't include in new cache (>= allows 0-second TTL)
{Acc, Count + 1};
true ->
%% Still valid
{Acc#{ServiceId => CacheEntry}, Count}
end
end,
{#{}, 0},
Cache
),
{Registry#{cache => NewCache}, Removed}.
%% @doc Clear the entire discovery cache.
-spec clear_cache(registry()) -> registry().
clear_cache(Registry) ->
Registry#{cache => #{}}.
%% @doc Remove expired local services.
%%
%% Should be called periodically to prevent memory leaks from stale service
%% registrations. Returns updated registry and count of removed services.
-spec prune_expired_local_services(registry()) -> {registry(), non_neg_integer()}.
prune_expired_local_services(#{local_services := Services, service_ttl := ServiceTTL} = Registry) ->
Now = erlang:system_time(second),
{NewServices, Removed} = maps:fold(
fun(ServiceId, LocalService, {Acc, Count}) ->
AdvertisedAt = maps:get(advertised_at, LocalService),
Age = Now - AdvertisedAt,
prune_service_by_age(Age, ServiceTTL, ServiceId, LocalService, Acc, Count)
end,
{#{}, 0},
Services
),
{Registry#{local_services => NewServices}, Removed}.
%% Pattern match with guard for service expiry check
prune_service_by_age(Age, ServiceTTL, _ServiceId, _LocalService, Acc, Count)
when Age >= ServiceTTL ->
%% Expired - don't include (>= allows 0-second TTL for testing)
{Acc, Count + 1};
prune_service_by_age(_Age, _ServiceTTL, ServiceId, LocalService, Acc, Count) ->
%% Still valid
{Acc#{ServiceId => LocalService}, Count}.
%%%===================================================================
%%% Subscriber Cache Functions (Pub/Sub)
%%%===================================================================
%% @doc Discover subscribers for a topic (checks cache first).
%%
%% Similar to discover_service/2 but for pub/sub subscribers.
%% Returns cached subscribers if found and not expired, otherwise cache_miss.
-spec discover_subscribers(registry(), binary()) ->
{ok, [provider_info()], registry()} | {cache_miss, registry()}.
discover_subscribers(#{subscriber_cache := Cache, cache_ttl := CacheTTL} = Registry, Topic) ->
case maps:get(Topic, Cache, undefined) of
undefined ->
{cache_miss, Registry};
CacheEntry ->
check_subscriber_cache_expiry(CacheEntry, CacheTTL, Registry)
end.
%% Check if cached entry is expired (pattern matching)
check_subscriber_cache_expiry(#{cached_at := CachedAt, providers := Subscribers}, CacheTTL, Registry) ->
Now = erlang:system_time(second),
Age = Now - CachedAt,
check_expiry_by_age(Age, CacheTTL, Subscribers, Registry).
%% Guard-based expiry check
check_expiry_by_age(Age, CacheTTL, Subscribers, Registry) when Age < CacheTTL ->
{ok, Subscribers, Registry};
check_expiry_by_age(_Age, _CacheTTL, _Subscribers, Registry) ->
{cache_miss, Registry}.
%% @doc Cache discovered subscribers for a topic.
%%
%% Stores subscribers in local cache with TTL. Subsequent discover_subscribers/2
%% calls will return cached results until TTL expires.
-spec cache_subscribers(registry(), binary(), [provider_info()], pos_integer()) -> registry().
cache_subscribers(#{subscriber_cache := Cache} = Registry, Topic, Subscribers, TTL) ->
CacheEntry = #{
service_id => Topic,
providers => Subscribers,
cached_at => erlang:system_time(second),
ttl => TTL
},
NewCache = Cache#{Topic => CacheEntry},
Registry#{subscriber_cache => NewCache}.
%% @doc Remove expired subscriber cache entries.
%%
%% Should be called periodically to prevent memory leaks.
%% Returns updated registry and count of removed entries.
-spec prune_expired_subscribers(registry()) -> {registry(), non_neg_integer()}.
prune_expired_subscribers(#{subscriber_cache := Cache, cache_ttl := CacheTTL} = Registry) ->
Now = erlang:system_time(second),
{NewCache, Removed} = maps:fold(
fun(Topic, CacheEntry, {Acc, Count}) ->
prune_if_expired(Topic, CacheEntry, Now, CacheTTL, Acc, Count)
end,
{#{}, 0},
Cache
),
{Registry#{subscriber_cache => NewCache}, Removed}.
%% Pattern match with guard for expiry check
prune_if_expired(_Topic, #{cached_at := CachedAt}, Now, CacheTTL, Acc, Count)
when Now - CachedAt >= CacheTTL ->
%% Expired - don't include (>= allows 0-second TTL)
{Acc, Count + 1};
prune_if_expired(Topic, CacheEntry, _Now, _CacheTTL, Acc, Count) ->
%% Still valid
{Acc#{Topic => CacheEntry}, Count}.
%% @doc Clear the entire subscriber cache.
-spec clear_subscriber_cache(registry()) -> registry().
clear_subscriber_cache(Registry) ->
Registry#{subscriber_cache => #{}}.
%%%===================================================================
%%% DHT Integration Functions
%%%===================================================================
%% @doc Publish a service advertisement to the DHT.
%%
%% This function publishes a service's provider information to the DHT
%% so other nodes can discover it. The service_id is hashed to create
%% a DHT key, and the provider information is stored at that key.
%%
%% Parameters:
%% - DhtPid: Process ID or registered name of macula_routing_server
%% - ServiceId: The service identifier (procedure URI)
%% - ProviderInfo: Information about this provider (node_id, endpoint, metadata)
%% - TTL: Time-to-live in seconds for this advertisement
%% - K: Number of nodes to store at (typically 20 for Kademlia)
%%
%% Returns:
%% - ok if successful
%% - {error, Reason} if publication failed
%%
%% Example:
%% ```
%% ProviderInfo = #{
%% node_id => <<"my-node-123">>,
%% endpoint => <<"https://localhost:9443">>,
%% metadata => #{version => <<"1.0">>}
%% },
%% ok = publish_to_dht(DhtPid, <<"energy.home.get">>, ProviderInfo, 300, 20).
%% '''
-spec publish_to_dht(pid() | atom(), service_id(), provider_info(), pos_integer(), pos_integer()) ->
ok | {error, term()}.
publish_to_dht(DhtPid, ServiceId, ProviderInfo, TTL, _K) ->
%% Compute DHT key from service_id
Key = service_key(ServiceId),
%% Add TTL and timestamp to provider info
EnrichedProviderInfo = ProviderInfo#{
advertised_at => erlang:system_time(second),
ttl => TTL
},
%% Store in DHT - for now, use gen_server:call pattern
%% This will be replaced with actual DHT routing calls
%% Crashes if DHT not available or gen_server:call fails - exposes configuration issues
ResolvedPid = resolve_pid(DhtPid),
case ResolvedPid of
undefined ->
%% DHT not available - this is a configuration error
error(dht_not_available);
Pid when is_pid(Pid) ->
%% TODO: Replace with actual macula_routing_server:store_value call
%% For now, store locally in routing server (let it crash on errors)
ok = gen_server:call(Pid, {store_local, Key, EnrichedProviderInfo}),
ok
end.
%% @doc Query the DHT for service providers.
%%
%% This function queries the DHT to find nodes that provide a given service.
%% It returns a list of provider_info() maps, each containing node_id, endpoint,
%% and metadata for a provider.
%%
%% Parameters:
%% - DhtPid: Process ID or registered name of macula_routing_server
%% - ServiceId: The service identifier to query for
%% - K: Number of closest nodes to query (typically 20 for Kademlia)
%%
%% Returns:
%% - {ok, [ProviderInfo]} if providers found
%% - {ok, []} if no providers found
%% - {error, Reason} if query failed
%%
%% Example:
%% ```
%% {ok, Providers} = query_dht_for_service(DhtPid, <<"energy.home.get">>, 20),
%% %% Returns: [{ok, [#{node_id => ..., endpoint => ..., metadata => ...}]}]
%% '''
-spec query_dht_for_service(pid() | atom(), service_id(), pos_integer()) ->
{ok, [provider_info()]} | {error, term()}.
query_dht_for_service(DhtPid, ServiceId, K) ->
%% Compute DHT key from service_id
Key = service_key(ServiceId),
%% Query DHT using distributed find_value
%% Crashes if DHT not available or query fails - exposes configuration/routing issues
ResolvedPid = resolve_pid(DhtPid),
case ResolvedPid of
undefined ->
error(dht_not_available);
Pid when is_pid(Pid) ->
%% Use distributed DHT lookup (let it crash on errors)
case macula_routing_server:find_value(Pid, Key, K) of
{ok, Value} when is_map(Value) ->
%% Single provider stored
{ok, [Value]};
{ok, Values} when is_list(Values) ->
%% Multiple providers stored
{ok, Values};
{ok, []} ->
%% No providers found
{ok, []};
{nodes, _Nodes} ->
%% Value not found in DHT
{ok, []};
{error, Reason} ->
error({dht_query_failed, Reason});
Other ->
error({unexpected_dht_response, Other})
end
end.
%% @doc Remove a service advertisement from the DHT.
%%
%% This function removes a service advertisement when unadvertising.
%% Note: In practice, DHT entries expire naturally via TTL, so this
%% is optional and mainly useful for immediate cleanup.
%%
%% Parameters:
%% - DhtPid: Process ID or registered name of macula_routing_server
%% - ServiceId: The service identifier to remove
%% - NodeId: This node's identifier (to remove only this provider)
%%
%% Returns:
%% - ok if successful or entry not found
%% - {error, Reason} if removal failed
-spec remove_from_dht(pid() | atom(), service_id(), node_id()) ->
ok | {error, term()}.
remove_from_dht(DhtPid, ServiceId, NodeId) ->
%% Compute DHT key from service_id
Key = service_key(ServiceId),
%% Remove specific provider from DHT by node_id (best effort)
ResolvedPid = resolve_pid(DhtPid),
do_remove_from_dht(ResolvedPid, Key, NodeId).
%% Pattern match on whereis result for best-effort cleanup
do_remove_from_dht(undefined, _Key, _NodeId) ->
ok; % DHT not available, nothing to remove
do_remove_from_dht(Pid, Key, NodeId) when is_pid(Pid) ->
%% Remove this specific provider from the list
%% Pattern match on expected success, crash on unexpected errors
ok = gen_server:call(Pid, {delete_local, Key, NodeId}),
ok.
%%%===================================================================
%%% Internal Functions
%%%===================================================================
%% @doc Compute DHT key for a service.
%% Uses SHA-256 hash of service_id to create a 32-byte DHT key.
-spec service_key(service_id()) -> binary().
service_key(ServiceId) ->
crypto:hash(sha256, ServiceId).
%% @doc Resolve PID or atom to a PID.
%% Handles both direct PIDs and registered names.
-spec resolve_pid(pid() | atom()) -> pid() | undefined.
resolve_pid(Pid) when is_pid(Pid) ->
Pid;
resolve_pid(Name) when is_atom(Name) ->
whereis(Name);
resolve_pid(_) ->
undefined.