mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-18 21:45:31 +09:00
259 lines
8.9 KiB
Erlang
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.
|