fix(gateway): always clear expired custom statuses (#2807)

This commit is contained in:
Hampus
2026-09-16 16:52:22 +02:00
committed by GitHub
parent 17292fd6a5
commit 3bc5dd8e0f
3 changed files with 52 additions and 159 deletions
@@ -269,60 +269,45 @@ dispatch_to_sessions(Payload, State) ->
-ifdef(TEST). -ifdef(TEST).
-include_lib("eunit/include/eunit.hrl"). -include_lib("eunit/include/eunit.hrl").
expiry_timer_not_armed_while_disabled_test() ->
application:unset_env(fluxer_gateway, custom_status_expiry_enabled),
State = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}),
?assertNot(maps:is_key(?CUSTOM_STATUS_EXPIRY_TIMER, State)).
expiry_timer_not_armed_without_expires_at_test() -> expiry_timer_not_armed_without_expires_at_test() ->
with_expiry_enabled(fun() -> ?assertNot(
?assertNot( maps:is_key(
maps:is_key( ?CUSTOM_STATUS_EXPIRY_TIMER,
?CUSTOM_STATUS_EXPIRY_TIMER, refresh_custom_status_expiry_timer(#{custom_status => null})
refresh_custom_status_expiry_timer(#{custom_status => null})
)
),
?assertNot(
maps:is_key(
?CUSTOM_STATUS_EXPIRY_TIMER,
refresh_custom_status_expiry_timer(#{
custom_status => #{<<"text">> => <<"hi">>}
})
)
) )
end). ),
?assertNot(
maps:is_key(
?CUSTOM_STATUS_EXPIRY_TIMER,
refresh_custom_status_expiry_timer(#{
custom_status => #{<<"text">> => <<"hi">>}
})
)
).
expiry_timer_armed_for_a_future_expiry_test() -> expiry_timer_armed_for_a_future_expiry_test() ->
with_expiry_enabled(fun() -> State = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}),
State = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}), ?assert(is_reference(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, State))),
?assert(is_reference(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, State))), ok = cancel_expiry_timer(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, State)).
ok = cancel_expiry_timer(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, State))
end).
expiry_timer_not_armed_for_an_already_expired_status_test() -> expiry_timer_not_armed_for_an_already_expired_status_test() ->
with_expiry_enabled(fun() -> Expired = #{<<"expires_at">> => <<"2020-01-01T00:00:00.000Z">>},
Expired = #{<<"expires_at">> => <<"2020-01-01T00:00:00.000Z">>}, State = refresh_custom_status_expiry_timer(#{custom_status => Expired}),
State = refresh_custom_status_expiry_timer(#{custom_status => Expired}), ?assertNot(maps:is_key(?CUSTOM_STATUS_EXPIRY_TIMER, State)).
?assertNot(maps:is_key(?CUSTOM_STATUS_EXPIRY_TIMER, State))
end).
expiry_timer_replaces_the_previous_one_test() -> expiry_timer_replaces_the_previous_one_test() ->
with_expiry_enabled(fun() -> First = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}),
First = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}), Second = refresh_custom_status_expiry_timer(First),
Second = refresh_custom_status_expiry_timer(First), ?assertNotEqual(
?assertNotEqual( maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, First),
maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, First), maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, Second)
maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, Second) ),
), ok = cancel_expiry_timer(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, Second)).
ok = cancel_expiry_timer(maps:get(?CUSTOM_STATUS_EXPIRY_TIMER, Second))
end).
expiry_timer_is_dropped_when_the_status_is_cleared_test() -> expiry_timer_is_dropped_when_the_status_is_cleared_test() ->
with_expiry_enabled(fun() -> Armed = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}),
Armed = refresh_custom_status_expiry_timer(#{custom_status => future_custom_status()}), Cleared = refresh_custom_status_expiry_timer(Armed#{custom_status => null}),
Cleared = refresh_custom_status_expiry_timer(Armed#{custom_status => null}), ?assertNot(maps:is_key(?CUSTOM_STATUS_EXPIRY_TIMER, Cleared)).
?assertNot(maps:is_key(?CUSTOM_STATUS_EXPIRY_TIMER, Cleared))
end).
expiry_timer_delivers_a_reconcile_cast_test() -> expiry_timer_delivers_a_reconcile_cast_test() ->
State = arm_expiry_timer({ok, 1}, #{}), State = arm_expiry_timer({ok, 1}, #{}),
@@ -343,19 +328,4 @@ future_custom_status() ->
), ),
#{<<"text">> => <<"brb">>, <<"expires_at">> => list_to_binary(ExpiresAt)}. #{<<"text">> => <<"brb">>, <<"expires_at">> => list_to_binary(ExpiresAt)}.
with_expiry_enabled(Fun) ->
Key = custom_status_expiry_enabled,
Previous = application:get_env(fluxer_gateway, Key),
application:set_env(fluxer_gateway, Key, true),
try
Fun()
after
restore_expiry_env(Key, Previous)
end.
restore_expiry_env(Key, undefined) ->
application:unset_env(fluxer_gateway, Key);
restore_expiry_env(Key, {ok, Value}) ->
application:set_env(fluxer_gateway, Key, Value).
-endif. -endif.
@@ -96,46 +96,30 @@ build_invisible_binary_normalized_to_offline_test() ->
?assertEqual(null, maps:get(<<"custom_status">>, Result)). ?assertEqual(null, maps:get(<<"custom_status">>, Result)).
build_clears_expired_custom_status_test() -> build_clears_expired_custom_status_test() ->
with_expiry_enabled(fun() -> Result = build(test_user(), online, false, false, expired_custom_status()),
Result = build(test_user(), online, false, false, expired_custom_status()), ?assertEqual(<<"online">>, maps:get(<<"status">>, Result)),
?assertEqual(<<"online">>, maps:get(<<"status">>, Result)), ?assertEqual(null, maps:get(<<"custom_status">>, Result)).
?assertEqual(null, maps:get(<<"custom_status">>, Result))
end).
build_coalesces_an_expired_status_with_no_status_test() -> build_coalesces_an_expired_status_with_no_status_test() ->
with_expiry_enabled(fun() -> ?assertEqual(
?assertEqual( build(test_user(), online, false, false, null),
build(test_user(), online, false, false, null), build(test_user(), online, false, false, expired_custom_status())
build(test_user(), online, false, false, expired_custom_status()) ).
)
end).
build_keeps_unexpired_custom_status_test() -> build_keeps_unexpired_custom_status_test() ->
with_expiry_enabled(fun() -> Live = future_custom_status(),
Live = future_custom_status(), Result = build(test_user(), dnd, false, false, Live),
Result = build(test_user(), dnd, false, false, Live), ?assertEqual(Live, maps:get(<<"custom_status">>, Result)).
?assertEqual(Live, maps:get(<<"custom_status">>, Result))
end).
build_keeps_custom_status_without_expires_at_test() -> build_keeps_custom_status_without_expires_at_test() ->
with_expiry_enabled(fun() -> Live = #{<<"text">> => <<"forever">>},
Live = #{<<"text">> => <<"forever">>}, Result = build(test_user(), idle, false, false, Live),
Result = build(test_user(), idle, false, false, Live), ?assertEqual(Live, maps:get(<<"custom_status">>, Result)).
?assertEqual(Live, maps:get(<<"custom_status">>, Result))
end).
build_keeps_malformed_expires_at_test() -> build_keeps_malformed_expires_at_test() ->
with_expiry_enabled(fun() -> Live = #{<<"text">> => <<"hi">>, <<"expires_at">> => <<"not-a-date">>},
Live = #{<<"text">> => <<"hi">>, <<"expires_at">> => <<"not-a-date">>}, Result = build(test_user(), online, false, false, Live),
Result = build(test_user(), online, false, false, Live), ?assertEqual(Live, maps:get(<<"custom_status">>, Result)).
?assertEqual(Live, maps:get(<<"custom_status">>, Result))
end).
build_leaves_expired_status_alone_while_disabled_test() ->
application:unset_env(fluxer_gateway, custom_status_expiry_enabled),
Expired = expired_custom_status(),
Result = build(test_user(), online, false, false, Expired),
?assertEqual(Expired, maps:get(<<"custom_status">>, Result)).
test_user() -> test_user() ->
#{<<"id">> => <<"1">>, <<"username">> => <<"Test">>}. #{<<"id">> => <<"1">>, <<"username">> => <<"Test">>}.
@@ -153,19 +137,4 @@ future_custom_status() ->
erlang:system_time(millisecond) + 3600000, [{unit, millisecond}, {offset, "Z"}] erlang:system_time(millisecond) + 3600000, [{unit, millisecond}, {offset, "Z"}]
), ),
#{<<"text">> => <<"brb">>, <<"expires_at">> => list_to_binary(ExpiresAt)}. #{<<"text">> => <<"brb">>, <<"expires_at">> => list_to_binary(ExpiresAt)}.
with_expiry_enabled(Fun) ->
Key = custom_status_expiry_enabled,
Previous = application:get_env(fluxer_gateway, Key),
application:set_env(fluxer_gateway, Key, true),
try
Fun()
after
restore_expiry_env(Key, Previous)
end.
restore_expiry_env(Key, undefined) ->
application:unset_env(fluxer_gateway, Key);
restore_expiry_env(Key, {ok, Value}) ->
application:set_env(fluxer_gateway, Key, Value).
-endif. -endif.
@@ -4,7 +4,6 @@
-typing([eqwalizer]). -typing([eqwalizer]).
-export([ -export([
enabled/0,
clear_if_expired/1, clear_if_expired/1,
clear_if_expired/2, clear_if_expired/2,
remaining_ms/2, remaining_ms/2,
@@ -20,26 +19,13 @@
-type wakeup() :: {ok, pos_integer()} | none. -type wakeup() :: {ok, pos_integer()} | none.
-type offset() :: {ok, integer()} | none. -type offset() :: {ok, integer()} | none.
-define(ENABLED_KEY, custom_status_expiry_enabled).
-define(MAX_REPAIR_WINDOW_SECONDS, 86400). -define(MAX_REPAIR_WINDOW_SECONDS, 86400).
-define(MAX_TIMER_MS, 86400000). -define(MAX_TIMER_MS, 86400000).
-define(WAKEUP_JITTER_MS, 5000). -define(WAKEUP_JITTER_MS, 5000).
-spec enabled() -> boolean().
enabled() ->
application:get_env(fluxer_gateway, ?ENABLED_KEY, false) =:= true.
-spec clear_if_expired(term()) -> custom_status(). -spec clear_if_expired(term()) -> custom_status().
clear_if_expired(CustomStatus) -> clear_if_expired(CustomStatus) ->
clear_when_enabled(enabled(), CustomStatus). clear_if_expired(CustomStatus, erlang:system_time(millisecond)).
-spec clear_when_enabled(boolean(), term()) -> custom_status().
clear_when_enabled(true, CustomStatus) ->
clear_if_expired(CustomStatus, erlang:system_time(millisecond));
clear_when_enabled(false, CustomStatus) when is_map(CustomStatus) ->
CustomStatus;
clear_when_enabled(false, _CustomStatus) ->
null.
-spec clear_if_expired(term(), term()) -> custom_status(). -spec clear_if_expired(term(), term()) -> custom_status().
clear_if_expired(CustomStatus, NowMs) when is_map(CustomStatus), is_integer(NowMs) -> clear_if_expired(CustomStatus, NowMs) when is_map(CustomStatus), is_integer(NowMs) ->
@@ -88,12 +74,6 @@ parse_rfc3339(Chars) ->
-spec next_wakeup_ms(term()) -> wakeup(). -spec next_wakeup_ms(term()) -> wakeup().
next_wakeup_ms(CustomStatus) -> next_wakeup_ms(CustomStatus) ->
next_wakeup_ms(enabled(), CustomStatus).
-spec next_wakeup_ms(boolean(), term()) -> wakeup().
next_wakeup_ms(false, _CustomStatus) ->
none;
next_wakeup_ms(true, CustomStatus) ->
wakeup_delay(remaining_ms(CustomStatus, erlang:system_time(millisecond))). wakeup_delay(remaining_ms(CustomStatus, erlang:system_time(millisecond))).
-spec wakeup_delay(offset()) -> wakeup(). -spec wakeup_delay(offset()) -> wakeup().
@@ -125,8 +105,7 @@ repair(WindowSeconds) when
#{ #{
scheduled => Total, scheduled => Total,
considered => length(Considered), considered => length(Considered),
window_seconds => WindowSeconds, window_seconds => WindowSeconds
enabled => enabled()
}; };
repair(_WindowSeconds) -> repair(_WindowSeconds) ->
#{error => invalid_window_seconds}. #{error => invalid_window_seconds}.
@@ -334,21 +313,11 @@ wakeup_clamps_to_the_timer_limit_test() ->
?assert(DelayMs > ?MAX_TIMER_MS), ?assert(DelayMs > ?MAX_TIMER_MS),
?assert(DelayMs =< ?MAX_TIMER_MS + ?WAKEUP_JITTER_MS). ?assert(DelayMs =< ?MAX_TIMER_MS + ?WAKEUP_JITTER_MS).
disabled_by_default_test() -> clears_expired_and_arms_future_test() ->
application:unset_env(fluxer_gateway, ?ENABLED_KEY),
Status = status(?LIVE_EXPIRES_AT),
?assertEqual(false, enabled()),
?assertEqual(Status, clear_if_expired(Status)),
?assertEqual(none, next_wakeup_ms(future_status())).
enabled_clears_expired_and_arms_future_test() ->
Live = future_status(), Live = future_status(),
with_expiry_enabled(fun() -> ?assertEqual(null, clear_if_expired(status(?LIVE_EXPIRES_AT))),
?assertEqual(true, enabled()), ?assertEqual(Live, clear_if_expired(Live)),
?assertEqual(null, clear_if_expired(status(?LIVE_EXPIRES_AT))), ?assertMatch({ok, _}, next_wakeup_ms(Live)).
?assertEqual(Live, clear_if_expired(Live)),
?assertMatch({ok, _}, next_wakeup_ms(Live))
end).
future_status() -> future_status() ->
status(rfc3339(erlang:system_time(millisecond) + 3600000)). status(rfc3339(erlang:system_time(millisecond) + 3600000)).
@@ -371,8 +340,7 @@ repair_reports_what_it_scheduled_test() ->
Result = repair(0), Result = repair(0),
?assert(is_integer(maps:get(scheduled, Result))), ?assert(is_integer(maps:get(scheduled, Result))),
?assert(maps:get(scheduled, Result) >= 0), ?assert(maps:get(scheduled, Result) >= 0),
?assertEqual(0, maps:get(window_seconds, Result)), ?assertEqual(0, maps:get(window_seconds, Result)).
?assertEqual(enabled(), maps:get(enabled, Result)).
schedule_reconciles_delivers_the_cast_test() -> schedule_reconciles_delivers_the_cast_test() ->
flush_mailbox(), flush_mailbox(),
@@ -388,18 +356,4 @@ flush_mailbox() ->
after 0 -> ok after 0 -> ok
end. end.
with_expiry_enabled(Fun) ->
Previous = application:get_env(fluxer_gateway, ?ENABLED_KEY),
application:set_env(fluxer_gateway, ?ENABLED_KEY, true),
try
Fun()
after
restore_expiry_env(Previous)
end.
restore_expiry_env(undefined) ->
application:unset_env(fluxer_gateway, ?ENABLED_KEY);
restore_expiry_env({ok, Value}) ->
application:set_env(fluxer_gateway, ?ENABLED_KEY, Value).
-endif. -endif.