mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
582 lines
23 KiB
Erlang
582 lines
23 KiB
Erlang
%% SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
-module(session).
|
|
-typing([eqwalizer]).
|
|
-behaviour(gen_server).
|
|
|
|
-export([start_link/1]).
|
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
|
|
|
|
-export_type([
|
|
session_state/0, session_id/0, user_id/0, guild_id/0, channel_id/0, seq/0, status/0
|
|
]).
|
|
|
|
-type session_id() :: binary().
|
|
-type user_id() :: integer().
|
|
-type guild_id() :: integer().
|
|
-type channel_id() :: integer().
|
|
-type seq() :: non_neg_integer().
|
|
-type status() :: online | offline | idle | dnd | invisible.
|
|
-type guild_ref() :: {pid(), reference()} | undefined | cached_unavailable | unavailable.
|
|
-type call_ref() :: {pid(), reference()} | undefined.
|
|
-type replay_buffer() :: eqwalizer:dynamic(limited_deque:deque() | [map()]).
|
|
-type pending_presence_buffer() :: eqwalizer:dynamic([map()] | queue:queue(map())).
|
|
-type reaction_buffer() :: eqwalizer:dynamic([map()] | queue:queue(map())).
|
|
|
|
-type session_state() :: #{
|
|
id => session_id(),
|
|
user_id => user_id(),
|
|
user_data => map(),
|
|
custom_status => map() | null,
|
|
version => non_neg_integer(),
|
|
token_hash => binary(),
|
|
auth_session_id_hash => binary(),
|
|
buffer => replay_buffer(),
|
|
buffer_bytes => non_neg_integer(),
|
|
seq => seq(),
|
|
ack_seq => seq(),
|
|
properties => map(),
|
|
status => status(),
|
|
resume_status => status(),
|
|
afk => boolean(),
|
|
mobile => boolean(),
|
|
presence_pid => pid() | undefined,
|
|
presence_mref => reference() | undefined,
|
|
socket_pid => pid() | undefined,
|
|
socket_mref => reference() | undefined,
|
|
resume_timer => {reference(), reference()} | undefined,
|
|
offline_timer => {reference(), reference()} | undefined,
|
|
guilds => #{guild_id() => guild_ref()},
|
|
active_guilds => sets:set(guild_id()),
|
|
calls => #{channel_id() => call_ref()},
|
|
channels => #{channel_id() => map()},
|
|
ready => map() | undefined,
|
|
bot => boolean(),
|
|
shard => gateway_sharding:shard() | undefined,
|
|
e2ee_capable => boolean(),
|
|
ignored_events => #{binary() => true},
|
|
initial_guild_id => guild_id() | undefined,
|
|
collected_guild_states => [map()],
|
|
collected_sessions => [map()],
|
|
collected_presences => [map()],
|
|
guild_subscription_state => #{guild_id() => map()},
|
|
relationships => #{user_id() => integer()},
|
|
suppress_presence_updates => boolean(),
|
|
pending_presences => pending_presence_buffer(),
|
|
guild_connect_inflight => #{guild_id() => non_neg_integer()},
|
|
guild_connect_workers => #{reference() => {guild_id(), non_neg_integer(), pid()}},
|
|
guild_connect_timers => #{guild_id() => {reference(), reference()}},
|
|
voice_queue => queue:queue(map()),
|
|
voice_queue_timer => reference() | undefined,
|
|
debounce_reactions => boolean(),
|
|
reaction_buffer => reaction_buffer(),
|
|
reaction_buffer_timer => reference() | undefined,
|
|
_ => _
|
|
}.
|
|
|
|
-spec start_link(map()) -> {ok, pid()} | ignore | {error, term()}.
|
|
start_link(SessionData) ->
|
|
gen_server:start_link(?MODULE, SessionData, []).
|
|
|
|
-spec init(map()) -> {ok, session_state()}.
|
|
init(SessionData) ->
|
|
process_flag(trap_exit, true),
|
|
erlang:process_flag(fullsweep_after, 10),
|
|
State0 = session_init:build_state(SessionData),
|
|
ScheduleStartedAt = gateway_timings:start(),
|
|
session_init:schedule_timers(State0),
|
|
GwTimings = gateway_timings:record_function(
|
|
schedule_initial_timers,
|
|
<<"session_init:schedule_timers/1">>,
|
|
ScheduleStartedAt,
|
|
gateway_timings:from_state(State0)
|
|
),
|
|
{ok, gateway_timings:put_state(GwTimings, State0)}.
|
|
|
|
-spec handle_call(term(), gen_server:from(), session_state()) ->
|
|
{reply, term(), session_state()} | {stop, normal, term(), session_state()}.
|
|
handle_call({token_verify, Token}, _From, State) when is_binary(Token) ->
|
|
session_lifecycle:handle_token_verify(Token, State);
|
|
handle_call({is_staff}, _From, State) ->
|
|
session_lifecycle:handle_is_staff(State);
|
|
handle_call({heartbeat_ack, Seq}, _From, State) when is_integer(Seq), Seq >= 0 ->
|
|
session_lifecycle:handle_heartbeat_ack(Seq, State);
|
|
handle_call({resume, Seq, SocketPid}, _From, State) when
|
|
is_integer(Seq), Seq >= 0, is_pid(SocketPid)
|
|
->
|
|
session_lifecycle:handle_resume(Seq, SocketPid, State);
|
|
handle_call({get_state}, _From, State) ->
|
|
{reply, session_lifecycle:serialize_state(State), State};
|
|
handle_call(export_state, _From, State) ->
|
|
{reply, {ok, session_lifecycle:serialize_transfer_state(State)}, State};
|
|
handle_call({terminate, SessionIdHashes}, _From, State) ->
|
|
case binary_list(SessionIdHashes) of
|
|
{ok, Hashes} -> session_lifecycle:handle_terminate_call(Hashes, State);
|
|
error -> {reply, ok, State}
|
|
end;
|
|
handle_call({voice_state_update, Data}, _From, State) when is_map(Data) ->
|
|
session_voice:handle_voice_state_update(Data, State);
|
|
handle_call(_, _From, State) ->
|
|
{reply, ok, State}.
|
|
|
|
-spec handle_cast(term(), session_state()) ->
|
|
{noreply, session_state()} | {stop, normal, session_state()}.
|
|
handle_cast({presence_update, Update}, State) when is_map(Update) ->
|
|
session_lifecycle:handle_presence_update_cast(Update, State);
|
|
handle_cast({dispatch, Event, {pre_encoded, EncodedData} = Data}, State) when
|
|
is_atom(Event), is_binary(EncodedData)
|
|
->
|
|
session_dispatch:handle_dispatch(Event, Data, State);
|
|
handle_cast({dispatch, Event, {pre_encoded, EncodedData} = Data}, State) when
|
|
is_binary(Event), is_binary(EncodedData)
|
|
->
|
|
session_dispatch:handle_dispatch(Event, Data, State);
|
|
handle_cast({dispatch, Event, Data}, State) when
|
|
is_atom(Event), is_map(Data);
|
|
is_atom(Event), is_list(Data)
|
|
->
|
|
session_dispatch:handle_dispatch(Event, Data, State);
|
|
handle_cast({dispatch, Event, Data}, State) when
|
|
is_binary(Event), is_map(Data);
|
|
is_binary(Event), is_list(Data)
|
|
->
|
|
session_dispatch:handle_dispatch(Event, Data, State);
|
|
handle_cast({initial_global_presences, Presences}, State) ->
|
|
handle_cast_presences(Presences, State);
|
|
handle_cast(Msg, State) ->
|
|
handle_cast_guild_or_lifecycle(Msg, State).
|
|
|
|
-spec handle_cast_presences(term(), session_state()) ->
|
|
{noreply, session_state()}.
|
|
handle_cast_presences(Presences, State) ->
|
|
case map_list(Presences) of
|
|
{ok, PresenceMaps} ->
|
|
session_lifecycle:handle_initial_global_presences(
|
|
PresenceMaps, State
|
|
);
|
|
error ->
|
|
{noreply, State}
|
|
end.
|
|
|
|
-spec handle_cast_guild_or_lifecycle(term(), session_state()) ->
|
|
{noreply, session_state()} | {stop, normal, session_state()}.
|
|
handle_cast_guild_or_lifecycle(handoff_fence, State) ->
|
|
session_lifecycle:handle_handoff_fence(State);
|
|
handle_cast_guild_or_lifecycle({reconnect_drain, SocketPid}, State) when
|
|
is_pid(SocketPid); SocketPid =:= undefined
|
|
->
|
|
session_lifecycle:handle_reconnect_drain(SocketPid, State);
|
|
handle_cast_guild_or_lifecycle({guild_join, GuildId}, State) when is_integer(GuildId) ->
|
|
self() ! {guild_connect, GuildId, 0},
|
|
{noreply, State};
|
|
handle_cast_guild_or_lifecycle({store_guild_subscriptions, Data}, State) when is_map(Data) ->
|
|
{noreply, session_guilds:store_guild_subscriptions(Data, State)};
|
|
handle_cast_guild_or_lifecycle({guild_leave, GuildId, forced_unavailable, true}, State) when
|
|
is_integer(GuildId)
|
|
->
|
|
session_guilds:handle_forced_unavailable_guild_leave(GuildId, true, State);
|
|
handle_cast_guild_or_lifecycle({guild_leave, GuildId, forced_unavailable}, State) when
|
|
is_integer(GuildId)
|
|
->
|
|
session_guilds:handle_forced_unavailable_guild_leave(GuildId, false, State);
|
|
handle_cast_guild_or_lifecycle({guild_leave, GuildId}, State) when is_integer(GuildId) ->
|
|
session_guilds:handle_guild_leave(GuildId, State);
|
|
handle_cast_guild_or_lifecycle({terminate, SessionIdHashes}, State) ->
|
|
case binary_list(SessionIdHashes) of
|
|
{ok, Hashes} -> session_lifecycle:handle_terminate_cast(Hashes, State);
|
|
error -> {noreply, State}
|
|
end;
|
|
handle_cast_guild_or_lifecycle({terminate_force}, State) ->
|
|
{stop, normal, State};
|
|
handle_cast_guild_or_lifecycle(reconnect_drain, State) ->
|
|
session_lifecycle:handle_reconnect_drain(State);
|
|
handle_cast_guild_or_lifecycle(Msg, State) ->
|
|
do_handle_cast_call(Msg, State).
|
|
|
|
-spec do_handle_cast_call(term(), session_state()) ->
|
|
{noreply, session_state()}.
|
|
do_handle_cast_call({call_monitor, ChannelId, CallPid}, State) when
|
|
is_integer(ChannelId), is_pid(CallPid)
|
|
->
|
|
session_lifecycle:handle_call_monitor(ChannelId, CallPid, State);
|
|
do_handle_cast_call({call_unmonitor, ChannelId}, State) when
|
|
is_integer(ChannelId)
|
|
->
|
|
session_lifecycle:handle_call_unmonitor(ChannelId, State);
|
|
do_handle_cast_call({call_force_disconnect, ChannelId, ConnId}, State) when
|
|
is_integer(ChannelId), is_binary(ConnId)
|
|
->
|
|
{noreply, session_lifecycle:force_disconnect_dm_call(ChannelId, ConnId, State)};
|
|
do_handle_cast_call({call_force_disconnect, ChannelId, undefined}, State) when
|
|
is_integer(ChannelId)
|
|
->
|
|
{noreply, session_lifecycle:force_disconnect_dm_call(ChannelId, undefined, State)};
|
|
do_handle_cast_call(_, State) ->
|
|
{noreply, State}.
|
|
|
|
-spec handle_info(term(), session_state()) ->
|
|
{noreply, session_state()} | {stop, normal, session_state()}.
|
|
handle_info({presence_connect, Attempt}, #{presence_pid := undefined} = State) when
|
|
is_integer(Attempt), Attempt >= 0
|
|
->
|
|
session_connection:handle_presence_connect(Attempt, State);
|
|
handle_info({presence_connect, _Attempt}, State) ->
|
|
{noreply, State};
|
|
handle_info({guild_connect, GuildId, Attempt}, State) when
|
|
is_integer(GuildId), is_integer(Attempt), Attempt >= 0
|
|
->
|
|
session_connection:handle_guild_connect(GuildId, Attempt, State);
|
|
handle_info({guild_connect_result, _, _, _} = Msg, State) ->
|
|
handle_info_guild_connect_result(Msg, State);
|
|
handle_info({guild_connect_timeout, GuildId, Attempt, Token}, State) when
|
|
is_integer(GuildId), is_integer(Attempt), Attempt >= 0, is_reference(Token)
|
|
->
|
|
session_connection:handle_guild_connect_timeout(GuildId, Attempt, Token, State);
|
|
handle_info({call_reconnect, ChannelId, Attempt}, State) when
|
|
is_integer(ChannelId), is_integer(Attempt), Attempt >= 0
|
|
->
|
|
session_connection:handle_call_reconnect(ChannelId, Attempt, State);
|
|
handle_info({gateway_timing_update, Timings}, State) ->
|
|
{noreply, gateway_timings:merge_state(Timings, State)};
|
|
handle_info(Msg, State) ->
|
|
handle_info_lifecycle(Msg, State).
|
|
|
|
-spec handle_info_guild_connect_result(tuple(), session_state()) ->
|
|
{noreply, session_state()} | {stop, normal, session_state()}.
|
|
handle_info_guild_connect_result(
|
|
{guild_connect_result, GuildId, Attempt, Result}, State
|
|
) when is_integer(GuildId), is_integer(Attempt), Attempt >= 0 ->
|
|
session_connection:handle_guild_connect_result(
|
|
GuildId, Attempt, Result, State
|
|
);
|
|
handle_info_guild_connect_result(_, State) ->
|
|
{noreply, State}.
|
|
|
|
-spec handle_info_lifecycle(term(), session_state()) ->
|
|
{noreply, session_state()} | {stop, normal, session_state()}.
|
|
handle_info_lifecycle(enable_presence_updates, State) ->
|
|
Flushed = session_dispatch:flush_all_pending_presences(State),
|
|
{noreply, Flushed#{suppress_presence_updates => false}};
|
|
handle_info_lifecycle(premature_readiness, #{ready := undefined} = State) ->
|
|
{noreply, State};
|
|
handle_info_lifecycle(premature_readiness, State) ->
|
|
session_ready:dispatch_ready_data(State);
|
|
handle_info_lifecycle(bot_initial_ready, #{ready := undefined} = State) ->
|
|
{noreply, State};
|
|
handle_info_lifecycle(bot_initial_ready, State) ->
|
|
session_ready:dispatch_ready_data(State);
|
|
handle_info_lifecycle({resume_timeout, _} = Msg, State) ->
|
|
do_handle_resume_timeout(Msg, State);
|
|
handle_info_lifecycle(resume_timeout, State) ->
|
|
do_handle_resume_timeout(resume_timeout, State);
|
|
handle_info_lifecycle({resume_offline_timeout, _} = Msg, State) ->
|
|
session_lifecycle:handle_resume_offline_timeout(Msg, State);
|
|
handle_info_lifecycle(Msg, State) ->
|
|
do_handle_info_misc(Msg, State).
|
|
|
|
-spec do_handle_resume_timeout(term(), session_state()) ->
|
|
{noreply, session_state()} | {stop, normal, session_state()}.
|
|
do_handle_resume_timeout({resume_timeout, Token}, #{socket_pid := undefined} = State) ->
|
|
case maps:get(resume_timer, State, undefined) of
|
|
{Token, _TimerRef} ->
|
|
{stop, normal, State#{resume_timer => undefined}};
|
|
_ ->
|
|
{noreply, State}
|
|
end;
|
|
do_handle_resume_timeout({resume_timeout, _Token}, State) ->
|
|
{noreply, State};
|
|
do_handle_resume_timeout(resume_timeout, #{socket_pid := undefined} = State) ->
|
|
case maps:get(resume_timer, State, undefined) of
|
|
undefined -> {stop, normal, State};
|
|
_ -> {noreply, State}
|
|
end;
|
|
do_handle_resume_timeout(resume_timeout, State) ->
|
|
{noreply, State}.
|
|
|
|
-spec do_handle_info_misc(term(), session_state()) ->
|
|
{noreply, session_state()} | {stop, normal, session_state()}.
|
|
do_handle_info_misc({process_voice_queue}, State) ->
|
|
handle_voice_queue(State);
|
|
do_handle_info_misc(flush_reaction_buffer, State) ->
|
|
{noreply, session_dispatch:flush_reaction_buffer(State)};
|
|
do_handle_info_misc({'DOWN', Ref, process, _Pid, Reason}, State) when
|
|
is_reference(Ref)
|
|
->
|
|
session_monitor:handle_process_down(Ref, Reason, State);
|
|
do_handle_info_misc(check_ack_lag, State) ->
|
|
erlang:send_after(60000, self(), check_ack_lag),
|
|
State1 = session_connection_guild:repair_stalled_guild_connects(State),
|
|
{noreply, session_connection:repair_presence_connection(State1)};
|
|
do_handle_info_misc({presence_rejoin_check}, State) ->
|
|
handle_presence_rejoin_check(State);
|
|
do_handle_info_misc(_Info, State) ->
|
|
{noreply, State}.
|
|
|
|
-spec handle_presence_rejoin_check(session_state()) -> {noreply, session_state()}.
|
|
handle_presence_rejoin_check(State) ->
|
|
RepairedState = session_connection:repair_presence_connection(State),
|
|
{noreply, session_dispatch_presence:sync_presence_targets(RepairedState)}.
|
|
|
|
-spec terminate(term(), session_state()) -> ok.
|
|
terminate(Reason, State) ->
|
|
session_lifecycle:terminate(Reason, State).
|
|
|
|
-spec code_change(term(), session_state(), term()) -> {ok, session_state()}.
|
|
code_change(_OldVsn, State, _Extra) ->
|
|
erlang:process_flag(fullsweep_after, 10),
|
|
erlang:garbage_collect(),
|
|
{ok, State}.
|
|
|
|
-spec handle_voice_queue(session_state()) -> {noreply, session_state()}.
|
|
handle_voice_queue(State) ->
|
|
NewState = session_voice:process_voice_queue(State),
|
|
VoiceQueue = maps:get(voice_queue, NewState, queue:new()),
|
|
case queue:is_empty(VoiceQueue) of
|
|
false ->
|
|
Timer = erlang:send_after(100, self(), {process_voice_queue}),
|
|
{noreply, NewState#{voice_queue_timer => Timer}};
|
|
true ->
|
|
{noreply, NewState}
|
|
end.
|
|
|
|
-spec binary_list(term()) -> {ok, [binary()]} | error.
|
|
binary_list(Value) when is_list(Value) ->
|
|
binary_list(Value, []);
|
|
binary_list(_) ->
|
|
error.
|
|
|
|
-spec binary_list([term()], [binary()]) -> {ok, [binary()]} | error.
|
|
binary_list([], Acc) ->
|
|
{ok, lists:reverse(Acc)};
|
|
binary_list([Value | Rest], Acc) when is_binary(Value) ->
|
|
binary_list(Rest, [Value | Acc]);
|
|
binary_list(_, _) ->
|
|
error.
|
|
|
|
-spec map_list(term()) -> {ok, [map()]} | error.
|
|
map_list(Value) when is_list(Value) ->
|
|
map_list(Value, []);
|
|
map_list(_) ->
|
|
error.
|
|
|
|
-spec map_list([term()], [map()]) -> {ok, [map()]} | error.
|
|
map_list([], Acc) ->
|
|
{ok, lists:reverse(Acc)};
|
|
map_list([Value | Rest], Acc) when is_map(Value) ->
|
|
map_list(Rest, [Value | Acc]);
|
|
map_list(_, _) ->
|
|
error.
|
|
|
|
-ifdef(TEST).
|
|
-include_lib("eunit/include/eunit.hrl").
|
|
|
|
test_base_state(GuildId) ->
|
|
Buf = limited_deque:new(4096, 16777216),
|
|
maps:merge(test_base_core(GuildId, Buf), test_base_extra()).
|
|
|
|
test_base_core(GuildId, Buffer) ->
|
|
#{
|
|
id => <<"test-session">>,
|
|
user_id => 1,
|
|
user_data => #{},
|
|
custom_status => null,
|
|
version => 1,
|
|
token_hash => <<>>,
|
|
auth_session_id_hash => <<>>,
|
|
buffer => Buffer,
|
|
seq => 0,
|
|
ack_seq => 0,
|
|
properties => #{},
|
|
status => online,
|
|
resume_status => online,
|
|
afk => false,
|
|
mobile => false,
|
|
presence_pid => undefined,
|
|
presence_mref => undefined,
|
|
socket_pid => undefined,
|
|
socket_mref => undefined,
|
|
resume_timer => undefined,
|
|
offline_timer => undefined,
|
|
guilds => #{GuildId => {self(), make_ref()}},
|
|
calls => #{},
|
|
channels => #{},
|
|
ready => undefined,
|
|
bot => false
|
|
}.
|
|
|
|
test_base_extra() ->
|
|
#{
|
|
ignored_events => #{},
|
|
initial_guild_id => undefined,
|
|
collected_guild_states => [],
|
|
collected_sessions => [],
|
|
collected_presences => [],
|
|
relationships => #{},
|
|
suppress_presence_updates => false,
|
|
pending_presences => [],
|
|
guild_connect_inflight => #{},
|
|
voice_queue => queue:new(),
|
|
voice_queue_timer => undefined,
|
|
debounce_reactions => false,
|
|
reaction_buffer => [],
|
|
reaction_buffer_timer => undefined
|
|
}.
|
|
|
|
handle_cast_forced_unavailable_guild_leave_schedules_retry_test() ->
|
|
GuildId = 123,
|
|
State0 = test_base_state(GuildId),
|
|
{noreply, State1} = handle_cast({guild_leave, GuildId, forced_unavailable}, State0),
|
|
?assertEqual(cached_unavailable, maps:get(GuildId, maps:get(guilds, State1))),
|
|
[LastEvent] = limited_deque:to_list(maps:get(buffer, State1)),
|
|
LastData = assert_guild_delete_event_data(LastEvent),
|
|
?assertEqual(true, maps:get(<<"unavailable">>, LastData)),
|
|
receive
|
|
{guild_connect, GuildId, 0} -> ok
|
|
after 100 -> ?assert(false, forced_unavailable_retry_not_scheduled)
|
|
end.
|
|
|
|
handle_cast_forced_unavailable_hidden_guild_leave_includes_hidden_flag_test() ->
|
|
GuildId = 124,
|
|
State0 = test_base_state(GuildId),
|
|
{noreply, State1} = handle_cast({guild_leave, GuildId, forced_unavailable, true}, State0),
|
|
?assertEqual(cached_unavailable, maps:get(GuildId, maps:get(guilds, State1))),
|
|
[LastEvent] = limited_deque:to_list(maps:get(buffer, State1)),
|
|
EventData = assert_guild_delete_event_data(LastEvent),
|
|
?assertEqual(true, maps:get(<<"unavailable">>, EventData)),
|
|
?assertEqual(true, maps:get(<<"unavailable_hidden">>, EventData)),
|
|
receive
|
|
{guild_connect, GuildId, 0} -> ok
|
|
after 100 -> ?assert(false, forced_unavailable_hidden_retry_not_scheduled)
|
|
end.
|
|
|
|
-spec assert_guild_delete_event_data(term()) -> map().
|
|
assert_guild_delete_event_data(Event) when is_map(Event) ->
|
|
?assertEqual(guild_delete, maps:get(event, Event)),
|
|
ensure_map(maps:get(data, Event)).
|
|
|
|
ensure_map(V) when is_map(V) -> V.
|
|
|
|
handle_cast_reconnect_drain_signals_socket_test() ->
|
|
SocketPid = spawn_msg_forwarder(self(), socket_message),
|
|
State = #{socket_pid => SocketPid},
|
|
{noreply, State} = handle_cast(reconnect_drain, State),
|
|
receive
|
|
{socket_message, session_reconnect} -> ok
|
|
after 200 -> ?assert(false, reconnect_signal_not_received)
|
|
end.
|
|
|
|
handle_cast_targeted_reconnect_drain_ignores_replaced_socket_test() ->
|
|
OldSocketPid = spawn_msg_forwarder(self(), old_socket_message),
|
|
NewSocketPid = spawn_msg_forwarder(self(), new_socket_message),
|
|
State = #{socket_pid => NewSocketPid},
|
|
{noreply, State} = handle_cast({reconnect_drain, OldSocketPid}, State),
|
|
receive
|
|
{old_socket_message, session_reconnect} -> ?assert(false, old_socket_signaled);
|
|
{new_socket_message, session_reconnect} -> ?assert(false, new_socket_signaled)
|
|
after 50 ->
|
|
ok
|
|
end.
|
|
|
|
handle_cast_targeted_reconnect_drain_signals_matching_socket_test() ->
|
|
SocketPid = spawn_msg_forwarder(self(), socket_message),
|
|
State = #{socket_pid => SocketPid},
|
|
{noreply, State} = handle_cast({reconnect_drain, SocketPid}, State),
|
|
receive
|
|
{socket_message, session_reconnect} -> ok
|
|
after 200 -> ?assert(false, targeted_reconnect_signal_not_received)
|
|
end.
|
|
|
|
spawn_msg_forwarder(TargetPid, Tag) ->
|
|
spawn(fun() -> msg_forwarder_loop(TargetPid, Tag) end).
|
|
|
|
msg_forwarder_loop(TargetPid, Tag) ->
|
|
receive
|
|
Msg -> TargetPid ! {Tag, Msg}
|
|
after 30000 -> ok
|
|
end.
|
|
|
|
handle_cast_reconnect_drain_with_missing_socket_is_noop_test() ->
|
|
State = #{socket_pid => undefined},
|
|
{noreply, State} = handle_cast(reconnect_drain, State),
|
|
receive
|
|
session_reconnect -> ?assert(false, unexpected_reconnect)
|
|
after 50 -> ok
|
|
end.
|
|
|
|
handle_cast_handoff_fence_signals_socket_and_stops_test() ->
|
|
SocketPid = spawn_msg_forwarder(self(), socket_message),
|
|
State0 = #{socket_pid => SocketPid},
|
|
{stop, normal, State1} = handle_cast(handoff_fence, State0),
|
|
?assertEqual(true, maps:get(fenced, State1)),
|
|
receive
|
|
{socket_message, session_reconnect} -> ok
|
|
after 200 -> ?assert(false, fence_signal_not_received)
|
|
end.
|
|
|
|
handle_cast_handoff_fence_with_missing_socket_stops_test() ->
|
|
State0 = #{socket_pid => undefined},
|
|
{stop, normal, State1} = handle_cast(handoff_fence, State0),
|
|
?assertEqual(true, maps:get(fenced, State1)).
|
|
|
|
handle_info_ignores_stale_resume_timeout_test() ->
|
|
CurrentToken = make_ref(),
|
|
State = #{socket_pid => undefined, resume_timer => {CurrentToken, make_ref()}},
|
|
{noreply, State} = handle_info({resume_timeout, make_ref()}, State).
|
|
|
|
handle_info_stops_on_current_resume_timeout_test() ->
|
|
CurrentToken = make_ref(),
|
|
State = #{socket_pid => undefined, resume_timer => {CurrentToken, make_ref()}},
|
|
?assertMatch({stop, normal, _}, handle_info({resume_timeout, CurrentToken}, State)).
|
|
|
|
handle_info_merges_gateway_timing_update_test() ->
|
|
BaseTimings = gateway_timings:record_function(
|
|
base,
|
|
<<"base/0">>,
|
|
gateway_timings:start() - 10,
|
|
gateway_timings:new()
|
|
),
|
|
UpdateTimings = gateway_timings:record_function(
|
|
update,
|
|
<<"update/0">>,
|
|
gateway_timings:start() - 5,
|
|
gateway_timings:new()
|
|
),
|
|
{noreply, State1} = handle_info(
|
|
{gateway_timing_update, UpdateTimings},
|
|
#{gw_timings => BaseTimings}
|
|
),
|
|
Timings = gateway_timings:finalize(maps:get(gw_timings, State1)),
|
|
TraceNames = [maps:get(<<"name">>, Span) || Span <- maps:get(<<"trace">>, Timings)],
|
|
?assert(lists:member(<<"base/0">>, TraceNames)),
|
|
?assert(lists:member(<<"update/0">>, TraceNames)).
|
|
|
|
handle_presence_rejoin_check_reconnects_when_unattached_test() ->
|
|
{noreply, State1} = handle_presence_rejoin_check(#{presence_pid => undefined}),
|
|
?assertEqual(undefined, maps:get(presence_pid, State1)),
|
|
?assert(received_presence_connect()).
|
|
|
|
handle_presence_rejoin_check_keeps_healthy_attachment_test() ->
|
|
State0 = #{presence_pid => self(), presence_mref => undefined},
|
|
?assertEqual({noreply, State0}, handle_presence_rejoin_check(State0)),
|
|
?assertNot(received_presence_connect()).
|
|
|
|
check_ack_lag_repairs_unattached_presence_test() ->
|
|
State0 = #{
|
|
guilds => #{},
|
|
guild_connect_inflight => #{},
|
|
guild_connect_workers => #{},
|
|
guild_connect_last_repair_at => 0,
|
|
presence_pid => undefined
|
|
},
|
|
{noreply, State1} = do_handle_info_misc(check_ack_lag, State0),
|
|
?assertEqual(undefined, maps:get(presence_pid, State1)),
|
|
?assert(received_presence_connect()).
|
|
|
|
received_presence_connect() ->
|
|
receive
|
|
{presence_connect, 0} -> true
|
|
after 200 -> false
|
|
end.
|
|
|
|
-endif.
|