From d05f6c9aaaa2747aade986c3d553e6cd015113c7 Mon Sep 17 00:00:00 2001 From: Hampus Date: Mon, 28 Sep 2026 01:47:55 +0200 Subject: [PATCH] fix(gateway): push held users whose sessions end during grace (#3006) --- .../src/guild/guild_dispatch_push.erl | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/fluxer_gateway/src/guild/guild_dispatch_push.erl b/fluxer_gateway/src/guild/guild_dispatch_push.erl index 0c37d2984..cdd44e842 100644 --- a/fluxer_gateway/src/guild/guild_dispatch_push.erl +++ b/fluxer_gateway/src/guild/guild_dispatch_push.erl @@ -1077,9 +1077,15 @@ deliver_after_grace(Params, Sessions, RecheckAt, Presences) -> ok = apply_push_worker_priority(), LiveSessions = await_grace_recheck(monitor_grace_sessions(Sessions), RecheckAt), send_push_now( - users_left_offline(lists:usort(maps:values(LiveSessions)), Presences), Params + grace_recipients(maps:keys(Sessions), maps:values(LiveSessions), Presences), Params ). +-spec grace_recipients([user_id()], [user_id()], term()) -> [user_id()]. +grace_recipients(HeldUserIds, LiveUserIds, Presences) -> + Live = lists:usort(LiveUserIds), + Gone = lists:usort(HeldUserIds) -- Live, + lists:usort(Gone ++ users_left_offline(Live, Presences)). + -spec monitor_grace_sessions(grace_sessions()) -> #{reference() => user_id()}. monitor_grace_sessions(Sessions) -> maps:fold(fun monitor_user_sessions/3, #{}, Sessions). @@ -1315,6 +1321,57 @@ presence_eligibility_allows_when_presence_table_missing_test() -> build_push_presence_eligibility(#{<<"s1">> => #{user_id => 1}}, #{}) ). +grace_recipients_push_a_user_whose_held_sessions_all_ended_test() -> + Presences = #{1 => #{<<"status">> => <<"online">>}}, + ?assertEqual([1], grace_recipients([1], [], Presences)). + +grace_recipients_skip_a_user_still_online_on_a_held_session_test() -> + Presences = #{1 => #{<<"status">> => <<"online">>}}, + ?assertEqual([], grace_recipients([1], [1], Presences)). + +grace_recipients_push_a_user_whose_live_session_went_offline_test() -> + Presences = #{1 => #{<<"status">> => <<"offline">>}}, + ?assertEqual([1], grace_recipients([1], [1], Presences)). + +grace_recipients_skip_a_user_with_one_ended_and_one_live_online_session_test() -> + Presences = #{1 => #{<<"status">> => <<"online">>}}, + ?assertEqual([], grace_recipients([1], [1], Presences)). + +grace_recipients_mix_users_test() -> + Presences = #{ + 1 => #{<<"status">> => <<"online">>}, + 2 => #{<<"status">> => <<"online">>}, + 3 => #{<<"status">> => <<"offline">>} + }, + ?assertEqual([1, 3], grace_recipients([1, 2, 3], [2, 3, 3], Presences)). + +deliver_after_grace_pushes_a_user_whose_session_exits_inside_the_window_test() -> + Self = self(), + ok = meck:new(push, [passthrough, non_strict]), + try + ok = meck:expect(push, handle_message_create, fun(Params) -> + Self ! {pushed, maps:get(user_ids, Params)}, + ok + end), + Session = spawn(fun() -> + receive + stop -> ok + end + end), + Held = #{42 => [Session]}, + Presences = #{42 => #{<<"status">> => <<"online">>}}, + RecheckAt = erlang:monotonic_time(millisecond) + 300, + _ = spawn(fun() -> deliver_after_grace(#{}, Held, RecheckAt, Presences) end), + timer:sleep(50), + Session ! stop, + receive + {pushed, UserIds} -> ?assertEqual([42], UserIds) + after 2000 -> erlang:error(no_push_after_session_exit) + end + after + meck:unload(push) + end. + legacy_session_eligibility_suppresses_every_real_session_test() -> RealSession = #{ session_id => <<"s1">>,