Files
fluxer/fluxer_gateway/src/session/session_connection_guild_resolve.erl
T

259 lines
8.9 KiB
Erlang

%% SPDX-License-Identifier: AGPL-3.0-or-later
-module(session_connection_guild_resolve).
-typing([eqwalizer]).
-export([do_local_guild_connect/1]).
-export_type([guild_id/0, attempt/0, connect_ctx/0]).
-define(GUILD_CONNECT_ASYNC_TIMEOUT_MS, 120000).
-define(GUILD_MANAGER_START_TIMEOUT_MS, 20000).
-define(GUILD_MANAGER_LOOKUP_FALLBACK_TIMEOUT_MS, 200).
-define(MAX_GUILD_OWNER_REDIRECTS, 3).
-type guild_id() :: session:guild_id().
-type attempt() :: non_neg_integer().
-type connect_ctx() :: #{
session_pid := pid(),
guild_id := guild_id(),
attempt := attempt(),
session_id := binary(),
user_id := integer(),
bot := boolean(),
is_staff := boolean(),
initial_guild_id := guild_id() | undefined,
user_data := map()
}.
-spec do_local_guild_connect(connect_ctx()) ->
pending | {ok_cached_unavailable, map()} | {error, term()}.
do_local_guild_connect(Ctx) ->
case fluxer_gateway_sup:role_enabled(guilds) of
false -> try_remote_fallback(Ctx);
true -> do_local_lookup(Ctx)
end.
-spec try_remote_fallback(connect_ctx()) ->
pending | {ok_cached_unavailable, map()} | {error, term()}.
try_remote_fallback(#{guild_id := GuildId} = Ctx) ->
case resolve_owner_node(GuildId) of
{ok, OwnerNode} when OwnerNode =/= node() ->
do_remote_guild_connect(OwnerNode, Ctx);
_ ->
{error, {guild_manager_failed, {error, unavailable}}}
end.
-spec resolve_owner_node(guild_id()) ->
{ok, node()} | unavailable.
resolve_owner_node(GuildId) ->
try gateway_node_router:owner_node_result(GuildId, guilds) of
{ok, OwnerNode} when is_atom(OwnerNode) ->
{ok, OwnerNode};
{error, _Reason} ->
unavailable
catch
error:_Reason -> unavailable;
exit:_Reason -> unavailable
end.
-spec do_local_lookup(connect_ctx()) ->
pending | {ok_cached_unavailable, map()} | {error, term()}.
do_local_lookup(#{guild_id := GuildId} = Ctx) ->
Timeout = ?GUILD_MANAGER_LOOKUP_FALLBACK_TIMEOUT_MS,
case guild_manager:lookup(GuildId, Timeout) of
{ok, GuildPid} ->
start_connect_async(GuildPid, Ctx);
{error, {not_owner, OwnerNode}} when is_atom(OwnerNode) ->
do_remote_guild_connect(OwnerNode, Ctx);
{error, not_found} ->
handle_not_found(Ctx);
Error ->
{error, {guild_manager_failed, Error}}
end.
-spec handle_not_found(connect_ctx()) ->
pending | {ok_cached_unavailable, map()} | {error, term()}.
handle_not_found(#{guild_id := GuildId} = Ctx) ->
Timeout = ?GUILD_MANAGER_START_TIMEOUT_MS,
case guild_manager:start_or_lookup(GuildId, Timeout) of
{ok, GuildPid} when is_pid(GuildPid) ->
start_connect_async(GuildPid, Ctx);
{error, {not_owner, OwnerNode}} when is_atom(OwnerNode) ->
do_remote_guild_connect(OwnerNode, Ctx);
{error, timeout} ->
{error, {guild_manager_failed, {error, timeout}}};
{error, R} ->
{error, {guild_manager_failed, {error, R}}}
end.
-spec do_remote_guild_connect(node(), connect_ctx()) ->
pending | {ok_cached_unavailable, map()} | {error, term()}.
do_remote_guild_connect(OwnerNode, Ctx) ->
do_remote_guild_connect_loop(
OwnerNode, Ctx, ?MAX_GUILD_OWNER_REDIRECTS, []
).
-spec do_remote_guild_connect_loop(
node(), connect_ctx(), non_neg_integer(), [node()]
) -> pending | {ok_cached_unavailable, map()} | {error, term()}.
do_remote_guild_connect_loop(
OwnerNode, Ctx, RedirectsLeft, SeenOwnerNodes
) ->
case lists:member(OwnerNode, SeenOwnerNodes) of
true ->
LoopErr = {owner_redirect_loop, OwnerNode},
{error, {guild_manager_failed, {error, LoopErr}}};
false ->
do_remote_guild_connect_attempt(
OwnerNode, Ctx, RedirectsLeft, SeenOwnerNodes
)
end.
-spec do_remote_guild_connect_attempt(
node(), connect_ctx(), non_neg_integer(), [node()]
) -> pending | {ok_cached_unavailable, map()} | {error, term()}.
do_remote_guild_connect_attempt(
OwnerNode,
#{guild_id := GuildId} = Ctx,
RedirectsLeft,
SeenOwnerNodes
) ->
Timeout = ?GUILD_MANAGER_START_TIMEOUT_MS,
RemoteStartedAt = gateway_timings:start(),
Result =
try
gen_server:call(
{guild_manager, OwnerNode},
{start_or_lookup, GuildId},
Timeout
)
catch
exit:{timeout, _} -> {error, timeout};
exit:Reason -> {'EXIT', Reason};
error:Reason -> {'EXIT', Reason}
end,
notify_remote_timing(
Ctx,
guild_manager,
OwnerNode,
<<"session_connection_guild_resolve:do_remote_guild_connect_attempt/4">>,
RemoteStartedAt
),
Seen = [OwnerNode | SeenOwnerNodes],
handle_remote_result(
Result, Ctx, RedirectsLeft, Seen
).
-spec handle_remote_result(
term(), connect_ctx(), non_neg_integer(), [node()]
) -> pending | {ok_cached_unavailable, map()} | {error, term()}.
handle_remote_result({ok, GuildPid}, Ctx, _, _) when is_pid(GuildPid) ->
start_connect_async(GuildPid, Ctx);
handle_remote_result({error, {not_owner, Next}}, Ctx, RL, Seen) when
is_atom(Next), RL > 0
->
do_remote_guild_connect_loop(Next, Ctx, RL - 1, Seen);
handle_remote_result({error, timeout}, _, _, _) ->
{error, {guild_manager_failed, {error, timeout}}};
handle_remote_result({error, R}, _, _, _) ->
{error, {guild_manager_failed, {error, R}}};
handle_remote_result({'EXIT', {timeout, _}}, _, _, _) ->
{error, {guild_manager_failed, {error, timeout}}};
handle_remote_result({'EXIT', R}, _, _, _) ->
{error, {guild_manager_failed, {exit, R}}};
handle_remote_result(Other, _, _, _) ->
{error, {guild_manager_failed, Other}}.
-spec start_connect_async(pid(), connect_ctx()) ->
pending | {ok_cached_unavailable, map()} | {error, term()}.
start_connect_async(GuildPid, Ctx) ->
#{guild_id := GuildId, user_data := UserData} = Ctx,
UnavailCheck =
session_connection_unavailability:maybe_build_unavailable_response_from_cache(
GuildId, UserData
),
case UnavailCheck of
{ok, UnavailableResponse} ->
{ok_cached_unavailable, UnavailableResponse};
not_unavailable ->
do_start_connect_async(GuildPid, Ctx)
end.
-spec do_start_connect_async(pid(), connect_ctx()) -> pending | {error, term()}.
do_start_connect_async(GuildPid, Ctx) ->
#{
guild_id := GuildId,
attempt := Attempt,
session_id := SessionId,
user_id := UserId,
bot := Bot,
is_staff := IsStaff,
initial_guild_id := InitialGuildId,
session_pid := SessionPid
} = Ctx,
ActiveGuilds =
session_connection_unavailability:build_initial_active_guilds(
InitialGuildId, GuildId
),
Request = #{
session_id => SessionId,
user_id => UserId,
session_pid => SessionPid,
bot => Bot,
is_staff => IsStaff,
initial_guild_id => InitialGuildId,
active_guilds => ActiveGuilds
},
CastMsg =
{session_connect_async, #{
guild_id => GuildId, attempt => Attempt, request => Request
}},
send_connect_cast(GuildPid, CastMsg, GuildId, Attempt, SessionPid).
-spec send_connect_cast(pid(), term(), integer(), non_neg_integer(), pid()) ->
pending | {error, term()}.
send_connect_cast(GuildPid, CastMsg, GuildId, Attempt, SessionPid) ->
CastStartedAt = gateway_timings:start(),
Result = shard_utils:safe_cast(GuildPid, CastMsg),
notify_remote_timing(
SessionPid,
guild,
node(GuildPid),
<<"session_connection_guild_resolve:send_connect_cast/5">>,
CastStartedAt
),
case Result of
ok ->
TimeoutMsg = {guild_connect_timeout, GuildId, Attempt},
_ = erlang:send_after(?GUILD_CONNECT_ASYNC_TIMEOUT_MS, SessionPid, TimeoutMsg),
pending;
{error, overloaded} ->
{error, {guild_manager_failed, {error, overloaded}}}
end.
-spec notify_remote_timing(connect_ctx() | pid(), term(), node(), binary(), integer()) -> ok.
notify_remote_timing(#{session_pid := SessionPid}, Role, OwnerNode, FunctionName, StartedAt) ->
notify_remote_timing(SessionPid, Role, OwnerNode, FunctionName, StartedAt);
notify_remote_timing(SessionPid, Role, OwnerNode, FunctionName, StartedAt) when
is_pid(SessionPid), is_atom(OwnerNode)
->
_ =
case gateway_timings:remote_node(Role, OwnerNode) of
Remote when is_map(Remote) ->
Timings = gateway_timings:record_function(
remote_operation,
FunctionName,
StartedAt,
#{remote => Remote},
gateway_timings:new()
),
SessionPid ! {gateway_timing_update, Timings};
_ ->
ok
end,
ok;
notify_remote_timing(_, _, _, _, _) ->
ok.