From dcd5f88d651e36f52653dec3aebc644708ff0fc2 Mon Sep 17 00:00:00 2001 From: Hampus Date: Tue, 1 Sep 2026 00:16:26 +0200 Subject: [PATCH] fix(gateway): stop rate limit tables dying with their creator (#2299) --- .../gateway/gateway_handler_rate_limit.erl | 48 ++++++++++++++++--- .../src/gateway/session_abuse_protection.erl | 45 ++++++++++++++--- fluxer_gateway/src/guild/guild_ets_utils.erl | 9 +++- 3 files changed, 87 insertions(+), 15 deletions(-) diff --git a/fluxer_gateway/src/gateway/gateway_handler_rate_limit.erl b/fluxer_gateway/src/gateway/gateway_handler_rate_limit.erl index 46452b3de..10d125ffd 100644 --- a/fluxer_gateway/src/gateway/gateway_handler_rate_limit.erl +++ b/fluxer_gateway/src/gateway/gateway_handler_rate_limit.erl @@ -320,18 +320,17 @@ create_counter_table(Table) -> -spec create_table(atom()) -> created | exists. create_table(Table) -> try - _ = ets:new(Table, [ - named_table, - public, - set, - {write_concurrency, true}, - {read_concurrency, true} - ]), + _ = ets:new(Table, rate_table_options()), created catch error:badarg -> exists end. +-spec rate_table_options() -> list(). +rate_table_options() -> + [named_table, public, set, {write_concurrency, true}, {read_concurrency, true}] ++ + guild_ets_utils:heir_options(). + -spec rate_limits_disabled() -> boolean(). rate_limits_disabled() -> case os:getenv("FLUXER_DISABLE_RATE_LIMITS") of @@ -502,6 +501,41 @@ await_table_size(Table, Max, Attempts) -> await_table_size(Table, Max, Attempts - 1) end. +shared_ip_bucket_survives_creating_process_death_test() -> + with_rate_limits_enabled(fun assert_shared_ip_bucket_survives_creator/0). + +assert_shared_ip_bucket_survives_creator() -> + drop_table(?SHARED_IP_RATE_TABLE), + {ok, _} = guild_ets_owner:start_link(), + try + assert_shared_ip_bucket_outlives_creator() + after + gen_server:stop(guild_ets_owner) + end. + +assert_shared_ip_bucket_outlives_creator() -> + IP = <<"198.51.100.70">>, + stop_table_owner(start_shared_ip_bucket_creator(IP)), + ?assertNotEqual(undefined, ets:whereis(?SHARED_IP_RATE_TABLE)), + Key = {IP, erlang:system_time(millisecond) div ?SHARED_IP_RATE_WINDOW_MS}, + ?assertEqual([{Key, 1}], ets:lookup(?SHARED_IP_RATE_TABLE, Key)). + +start_shared_ip_bucket_creator(IP) -> + Parent = self(), + Pid = spawn(fun() -> create_shared_ip_bucket(Parent, IP) end), + receive + {owner_ready, Pid} -> Pid + after 1000 -> error(owner_start_timeout) + end. + +create_shared_ip_bucket(Parent, IP) -> + ok = check_shared_ip_rate(IP), + Parent ! {owner_ready, self()}, + receive + stop -> ok + after 30000 -> ok + end. + window_cleanup_loops_do_not_outlive_their_table_test() -> with_rate_limits_enabled(fun() -> with_fast_cleanup_timer(fun assert_window_cleanup_loops_do_not_leak/0) diff --git a/fluxer_gateway/src/gateway/session_abuse_protection.erl b/fluxer_gateway/src/gateway/session_abuse_protection.erl index 1bda8eb52..12596b47e 100644 --- a/fluxer_gateway/src/gateway/session_abuse_protection.erl +++ b/fluxer_gateway/src/gateway/session_abuse_protection.erl @@ -151,18 +151,17 @@ create_session_user_counts_table() -> -spec create_rate_table(atom()) -> created | exists. create_rate_table(Table) -> try - _ = ets:new(Table, [ - named_table, - public, - set, - {write_concurrency, true}, - {read_concurrency, true} - ]), + _ = ets:new(Table, rate_table_options()), created catch error:badarg -> exists end. +-spec rate_table_options() -> list(). +rate_table_options() -> + [named_table, public, set, {write_concurrency, true}, {read_concurrency, true}] ++ + guild_ets_utils:heir_options(). + -spec ensure_identify_table() -> ok. ensure_identify_table() -> case ets:whereis(?IDENTIFY_TABLE) of @@ -313,6 +312,38 @@ prune_old_identify_entries_keeps_recent_buckets_test() -> ?assertNotEqual([], ets:lookup(?IDENTIFY_TABLE, RecentKey)), ets:delete(?IDENTIFY_TABLE, RecentKey). +identify_bucket_survives_creating_process_death_test() -> + drop_identify_table(), + {ok, _} = guild_ets_owner:start_link(), + try + assert_identify_bucket_outlives_creator() + after + gen_server:stop(guild_ets_owner) + end. + +assert_identify_bucket_outlives_creator() -> + IP = <<"192.0.2.220">>, + stop_table_owner(start_identify_bucket_creator(IP)), + ?assertNotEqual(undefined, ets:whereis(?IDENTIFY_TABLE)), + Key = {IP, erlang:system_time(second) div ?IDENTIFY_WINDOW_SECS}, + ?assertEqual([{Key, 1}], ets:lookup(?IDENTIFY_TABLE, Key)). + +start_identify_bucket_creator(IP) -> + Parent = self(), + Pid = spawn(fun() -> create_identify_bucket(Parent, IP) end), + receive + {owner_ready, Pid} -> Pid + after 1000 -> error(owner_start_timeout) + end. + +create_identify_bucket(Parent, IP) -> + ok = check_identify_rate(IP), + Parent ! {owner_ready, self()}, + receive + stop -> ok + after 30000 -> ok + end. + identify_cleanup_loops_do_not_outlive_their_table_test() -> meck:new(gateway_retry_timer, [passthrough]), meck:expect(gateway_retry_timer, wait, fun(_) -> timer:sleep(5) end), diff --git a/fluxer_gateway/src/guild/guild_ets_utils.erl b/fluxer_gateway/src/guild/guild_ets_utils.erl index 1a09712fb..93eccadaf 100644 --- a/fluxer_gateway/src/guild/guild_ets_utils.erl +++ b/fluxer_gateway/src/guild/guild_ets_utils.erl @@ -3,7 +3,7 @@ -module(guild_ets_utils). -typing([eqwalizer]). --export([ensure_table/2]). +-export([ensure_table/2, heir_options/0]). -spec ensure_table(atom(), list()) -> ok. ensure_table(TableName, Options) -> @@ -14,6 +14,13 @@ ensure_table(TableName, Options) -> ensure_table_local(TableName, Options) end. +-spec heir_options() -> list(). +heir_options() -> + case whereis(guild_ets_owner) of + Pid when is_pid(Pid), Pid =/= self() -> [{heir, Pid, inherited}]; + _ -> [] + end. + -spec ensure_table_local(atom(), list()) -> ok. ensure_table_local(TableName, Options) -> case ets:whereis(TableName) of