refactor(gateway): share shard helpers and test fixtures (#3381)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-10 23:04:30 +02:00
1 parent f22261f218
commit e7b13fd19f
25 files changed
+101 -2021

No files matched your search

@@ -153,26 +153,12 @@ select_shard(Key, Count) when Count > 0 ->
-spec find_shard_by_ref(reference(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_shard_by_ref(Ref, Shards) ->
maps:fold(
fun
(Index, #{ref := R}, _) when R =:= Ref -> {ok, Index};
(_, _, Acc) -> Acc
end,
not_found,
Shards
).
shard_utils:find_shard_by_ref(Ref, Shards).
-spec find_shard_by_pid(pid(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_shard_by_pid(Pid, Shards) ->
maps:fold(
fun
(Index, #{pid := P}, _) when P =:= Pid -> {ok, Index};
(_, _, Acc) -> Acc
end,
not_found,
Shards
).
shard_utils:find_shard_by_pid(Pid, Shards).
-spec rebalance_retry_timer_ref(map()) -> reference() | undefined.
rebalance_retry_timer_ref(State) ->
@@ -25,19 +25,11 @@
-spec determine_shard_count() -> {pos_integer(), configured | auto}.
determine_shard_count() ->
case fluxer_gateway_env:get(guild_shards) of
Value when is_integer(Value), Value > 0 ->
{Value, configured};
_ ->
{default_shard_count(), auto}
end.
shard_utils:determine_shard_count(guild_shards).
-spec default_shard_count() -> pos_integer().
default_shard_count() ->
shard_utils:max_positive([
erlang:system_info(logical_processors_available),
erlang:system_info(schedulers_online)
]).
clustered_ets_cache:default_shard_count().
-spec start_shards(pos_integer()) -> #{non_neg_integer() => shard_map()}.
start_shards(Count) ->
@@ -109,12 +101,12 @@ normalize_grouped_ids(Groups) ->
-spec find_shard_by_ref(reference(), #{non_neg_integer() => shard_map()}) ->
{ok, non_neg_integer()} | not_found.
find_shard_by_ref(Ref, Shards) ->
find_shard_by(fun(#{ref := R}) -> R =:= Ref end, Shards).
shard_utils:find_shard_by_ref(Ref, Shards).
-spec find_shard_by_pid(pid(), #{non_neg_integer() => shard_map()}) ->
{ok, non_neg_integer()} | not_found.
find_shard_by_pid(Pid, Shards) ->
find_shard_by(fun(#{pid := P}) -> P =:= Pid end, Shards).
shard_utils:find_shard_by_pid(Pid, Shards).
-spec start_shard_into_map(non_neg_integer(), #{non_neg_integer() => shard_map()}) ->
#{non_neg_integer() => shard_map()}.
@@ -132,26 +124,3 @@ ensure_live_shard(Index, Pid, State) ->
false ->
restart_shard_or_fail(Index, State)
end.
-spec find_shard_by(fun((shard_map()) -> boolean()), #{non_neg_integer() => shard_map()}) ->
{ok, non_neg_integer()} | not_found.
find_shard_by(Pred, Shards) ->
maps:fold(
fun(Index, ShardMap, Acc) -> find_matching_shard(Pred, Index, ShardMap, Acc) end,
not_found,
Shards
).
-spec find_matching_shard(
fun((shard_map()) -> boolean()),
non_neg_integer(),
shard_map(),
{ok, non_neg_integer()} | not_found
) -> {ok, non_neg_integer()} | not_found.
find_matching_shard(_Pred, _Index, _ShardMap, {ok, _Found} = Acc) ->
Acc;
find_matching_shard(Pred, Index, ShardMap, not_found) ->
case Pred(ShardMap) of
true -> {ok, Index};
false -> not_found
end.
@@ -323,7 +323,7 @@ maybe_add_last_message_id(RawId, RawMsgId, UserId, Member, State, Acc) ->
binary(), binary(), term(), integer(), map(), guild_state(), map()
) -> map().
maybe_add_last_message_for_channel(IdBin, MsgIdBin, RawId, UserId, Member, State, Acc) ->
case parse_snowflake(<<"id">>, RawId) of
case guild_dispatch_decorate:parse_snowflake(<<"id">>, RawId) of
ChId when is_integer(ChId) ->
maybe_add_visible_last_message(IdBin, MsgIdBin, UserId, ChId, Member, State, Acc);
undefined ->
@@ -345,13 +345,6 @@ maybe_add_visible_last_message(IdBin, MsgId, UserId, ChId, Member, State, Acc) -
compute_voice_state_updates(Current, Previous, GuildId) ->
guild_passive_sync_voice:compute_voice_state_updates(Current, Previous, GuildId).
-spec parse_snowflake(binary(), term()) -> integer() | undefined.
parse_snowflake(FieldName, Value) ->
case validation:validate_snowflake(FieldName, Value) of
{ok, Id} -> Id;
{error, _, _} -> undefined
end.
-spec snowflake_binary(binary(), term()) -> binary() | undefined.
snowflake_binary(FieldName, Value) ->
case validation:validate_snowflake(FieldName, Value) of
@@ -150,7 +150,7 @@ moderator_rules_test() ->
).
shared_case_table_test() ->
{ok, Contents} = file:read_file("test/thread_permission_cases.json"),
{ok, Contents} = file:read_file("../packages/constants/src/ThreadPermissionCases.json"),
Cases = [
Case
|| #{<<"fn">> := Fn} = Case <- json:decode(Contents),
@@ -39,7 +39,7 @@ ensure_tables() ->
-spec upsert_voice_state(voice_state()) -> ok.
upsert_voice_state(VoiceState) when is_map(VoiceState) ->
ensure_tables(),
ConnectionId = normalize_optional_binary(
ConnectionId = guild_voice_connection_normalize:normalize_optional_binary(
get_optional_field(VoiceState, <<"connection_id">>, connection_id)
),
upsert_by_connection(ConnectionId, VoiceState);
@@ -51,17 +51,17 @@ upsert_by_connection(undefined, _VoiceState) ->
ok;
upsert_by_connection(ConnectionId, VoiceState) ->
case
normalize_optional_binary(
guild_voice_connection_normalize:normalize_optional_binary(
get_optional_field(VoiceState, <<"channel_id">>, channel_id, null)
)
of
undefined ->
remove_connection(ConnectionId);
_ChannelId ->
RegionId = normalize_optional_binary(
RegionId = guild_voice_connection_normalize:normalize_optional_binary(
get_optional_field(VoiceState, <<"region_id">>, region_id)
),
ServerId = normalize_optional_binary(
ServerId = guild_voice_connection_normalize:normalize_optional_binary(
get_optional_field(VoiceState, <<"server_id">>, server_id)
),
upsert_connection_metadata(ConnectionId, RegionId, ServerId)
@@ -70,7 +70,7 @@ upsert_by_connection(ConnectionId, VoiceState) ->
-spec remove_connection(binary() | term()) -> ok.
remove_connection(ConnectionId) ->
ensure_tables(),
case normalize_optional_binary(ConnectionId) of
case guild_voice_connection_normalize:normalize_optional_binary(ConnectionId) of
undefined ->
ok;
NormalizedConnectionId ->
@@ -214,19 +214,6 @@ compare_count_rows({LeftId, LeftCount}, {RightId, RightCount}) ->
false -> LeftCount > RightCount
end.
-spec normalize_optional_binary(term()) -> binary() | undefined.
normalize_optional_binary(undefined) ->
undefined;
normalize_optional_binary(null) ->
undefined;
normalize_optional_binary(Value) when is_binary(Value), byte_size(Value) > 0 -> Value;
normalize_optional_binary(Value) when is_binary(Value) -> undefined;
normalize_optional_binary(Value) when is_integer(Value) -> integer_to_binary(Value);
normalize_optional_binary(Value) when is_list(Value) ->
guild_voice_connection_normalize:normalize_optional_binary(Value);
normalize_optional_binary(_) ->
undefined.
-spec get_optional_field(map(), term(), term()) -> term().
get_optional_field(Map, PrimaryKey, FallbackKey) ->
get_optional_field(Map, PrimaryKey, FallbackKey, undefined).
@@ -159,7 +159,7 @@ maybe_put_live_voice_state(FallbackConnId, DefaultChanId, VoiceState, Acc) ->
VoiceState,
maps:get(connection_id, VoiceState, FallbackConnId)
),
ConnectionId = normalize_optional_binary(RawConnId),
ConnectionId = guild_voice_connection_normalize:normalize_optional_binary(RawConnId),
case ConnectionId of
undefined ->
Acc;
@@ -175,7 +175,7 @@ maybe_put_default_channel_id(VoiceState, DefaultChannelId) ->
VoiceState,
maps:get(channel_id, VoiceState, undefined)
),
case normalize_optional_binary(ExistingId) of
case guild_voice_connection_normalize:normalize_optional_binary(ExistingId) of
undefined ->
apply_default_channel_id(VoiceState, DefaultChannelId);
ChannelId ->
@@ -184,27 +184,11 @@ maybe_put_default_channel_id(VoiceState, DefaultChannelId) ->
-spec apply_default_channel_id(voice_state(), term()) -> voice_state().
apply_default_channel_id(VoiceState, DefaultChannelId) ->
case normalize_optional_binary(DefaultChannelId) of
case guild_voice_connection_normalize:normalize_optional_binary(DefaultChannelId) of
undefined -> VoiceState;
ChannelId -> VoiceState#{<<"channel_id">> => ChannelId}
end.
-spec normalize_optional_binary(term()) -> binary() | undefined.
normalize_optional_binary(undefined) ->
undefined;
normalize_optional_binary(null) ->
undefined;
normalize_optional_binary(Value) when is_binary(Value), byte_size(Value) > 0 ->
Value;
normalize_optional_binary(Value) when is_binary(Value) ->
undefined;
normalize_optional_binary(Value) when is_integer(Value) ->
integer_to_binary(Value);
normalize_optional_binary(Value) when is_list(Value) ->
guild_voice_connection_normalize:normalize_optional_binary(Value);
normalize_optional_binary(_) ->
undefined.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
+3 -33
View File
@@ -44,7 +44,7 @@ diagnostic_info() ->
init([]) ->
process_flag(trap_exit, true),
erlang:process_flag(fullsweep_after, 10),
{ShardCount, _Source} = determine_shard_count(presence_bus_shards),
{ShardCount, _Source} = shard_utils:determine_shard_count(presence_bus_shards),
Shards = start_shards(ShardCount, #{}),
{ok, #{shards => Shards, shard_count => ShardCount}, hibernate}.
@@ -127,22 +127,6 @@ code_change(_OldVsn, State, _Extra) ->
erlang:garbage_collect(),
{ok, State}.
-spec determine_shard_count(atom()) -> {pos_integer(), configured | auto}.
determine_shard_count(ConfigKey) ->
case fluxer_gateway_env:get(ConfigKey) of
Value when is_integer(Value), Value > 0 ->
{Value, configured};
_ ->
{default_shard_count(), auto}
end.
-spec default_shard_count() -> pos_integer().
default_shard_count() ->
shard_utils:max_positive([
erlang:system_info(logical_processors_available),
erlang:system_info(schedulers_online)
]).
-spec start_shards(pos_integer(), #{}) -> #{non_neg_integer() => shard()}.
start_shards(Count, Acc) ->
lists:foldl(fun start_shard_acc/2, Acc, lists:seq(0, Count - 1)).
@@ -238,26 +222,12 @@ select_shard(Key, Count) when Count > 0 ->
-spec find_shard_by_ref(reference(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_shard_by_ref(Ref, Shards) ->
maps:fold(
fun
(Index, #{ref := R}, _) when R =:= Ref -> {ok, Index};
(_, _, Acc) -> Acc
end,
not_found,
Shards
).
shard_utils:find_shard_by_ref(Ref, Shards).
-spec find_shard_by_pid(pid(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_shard_by_pid(Pid, Shards) ->
maps:fold(
fun
(Index, #{pid := P}, _) when P =:= Pid -> {ok, Index};
(_, _, Acc) -> Acc
end,
not_found,
Shards
).
shard_utils:find_shard_by_pid(Pid, Shards).
-spec safe_gen_server_call(pid() | atom(), term(), timeout()) -> term().
safe_gen_server_call(Server, Request, Timeout) ->
@@ -193,7 +193,7 @@ add_user_to_owner_group(UserId, Acc) ->
-spec local_shard_count() -> pos_integer().
local_shard_count() ->
{Count, _Source} = determine_shard_count(presence_cache_shards),
{Count, _Source} = presence_cache_shards:determine_count(presence_cache_shards),
Count.
-spec select_shard(term(), pos_integer()) -> non_neg_integer().
@@ -271,14 +271,6 @@ accumulate_ets_lookup(TableName, UserId, AccMap) ->
error:badarg -> AccMap
end.
-spec determine_shard_count(atom()) -> {pos_integer(), configured | auto}.
determine_shard_count(ConfigKey) ->
case clustered_ets_cache:determine_shard_count([ConfigKey]) of
{Count, ConfigKey} -> {Count, configured};
{Count, auto} -> {Count, auto};
{Count, _Source} -> {Count, configured}
end.
-spec normalize_shard_groups([{non_neg_integer(), [term()]}]) ->
[{non_neg_integer(), [integer()]}].
normalize_shard_groups(Groups) ->
@@ -66,7 +66,7 @@ handle_down_by_ref(Ref, State) ->
-spec handle_down_by_pid(pid(), state()) -> state().
handle_down_by_pid(Pid, State) ->
Shards = maps:get(shards, State),
case find_by_pid(Pid, Shards) of
case shard_utils:find_shard_by_pid(Pid, Shards) of
{ok, Index} -> restart_state(Index, State);
not_found -> State
end.
@@ -196,14 +196,7 @@ memory_stats(State) ->
-spec find_by_ref(reference(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_by_ref(Ref, Shards) ->
maps:fold(
fun
(Index, #{ref := R}, _) when R =:= Ref -> {ok, Index};
(_, _, Acc) -> Acc
end,
not_found,
Shards
).
shard_utils:find_shard_by_ref(Ref, Shards).
-spec start(non_neg_integer()) -> {ok, shard()} | {error, term()}.
start(Index) ->
@@ -306,18 +299,6 @@ merge_snapshot_reply(Snapshot, AccMap) when is_map(Snapshot) ->
merge_snapshot_reply(_, AccMap) ->
AccMap.
-spec find_by_pid(pid(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_by_pid(Pid, Shards) ->
maps:fold(
fun
(Index, #{pid := P}, _) when P =:= Pid -> {ok, Index};
(_, _, Acc) -> Acc
end,
not_found,
Shards
).
-spec error_reply(term()) -> term().
error_reply({put, _, _}) -> {error, unavailable};
error_reply({delete, _}) -> {error, unavailable};
@@ -36,10 +36,7 @@
-spec determine_count() -> {pos_integer(), configured | auto}.
determine_count() ->
case fluxer_gateway_env:get(presence_shards) of
Value when is_integer(Value), Value > 0 -> {Value, configured};
_ -> {default_count(), auto}
end.
shard_utils:determine_shard_count(presence_shards).
-spec start(non_neg_integer()) -> {ok, shard()} | {error, term()}.
start(Index) ->
@@ -157,33 +154,16 @@ pid_from_table(Key) ->
-spec find_by_ref(reference(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_by_ref(Ref, Shards) ->
maps:fold(
fun
(Index, #{ref := R}, _) when R =:= Ref -> {ok, Index};
(_, _, Acc) -> Acc
end,
not_found,
Shards
).
shard_utils:find_shard_by_ref(Ref, Shards).
-spec find_by_pid(pid(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_by_pid(Pid, Shards) ->
maps:fold(
fun
(Index, #{pid := P}, _) when P =:= Pid -> {ok, Index};
(_, _, Acc) -> Acc
end,
not_found,
Shards
).
shard_utils:find_shard_by_pid(Pid, Shards).
-spec default_count() -> pos_integer().
default_count() ->
shard_utils:max_positive([
erlang:system_info(logical_processors_available),
erlang:system_info(schedulers_online)
]).
clustered_ets_cache:default_shard_count().
-spec ensure_shard(user_id(), state()) -> {non_neg_integer(), state()}.
ensure_shard(Key, State) ->
@@ -269,7 +269,7 @@ push_eligible(State) ->
-spec build_push_create_params(user_id(), map()) -> map() | undefined.
build_push_create_params(UserId, Data) ->
AuthorIdBin = maps:get(<<"id">>, maps:get(<<"author">>, Data, #{}), undefined),
case parse_snowflake(<<"author_id">>, AuthorIdBin) of
case guild_dispatch_decorate:parse_snowflake(<<"author_id">>, AuthorIdBin) of
undefined ->
undefined;
AuthorId ->
@@ -439,14 +439,7 @@ session_holds_push(Session) ->
-spec extract_snowflake(binary(), map()) -> integer() | undefined.
extract_snowflake(FieldName, Data) ->
parse_snowflake(FieldName, maps:get(FieldName, Data, undefined)).
-spec parse_snowflake(binary(), term()) -> integer() | undefined.
parse_snowflake(FieldName, Value) ->
case validation:validate_snowflake(FieldName, Value) of
{ok, Id} -> Id;
{error, _, _} -> undefined
end.
guild_dispatch_decorate:parse_snowflake(FieldName, maps:get(FieldName, Data, undefined)).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
+1 -17
View File
@@ -96,7 +96,7 @@ init([]) ->
erlang:process_flag(fullsweep_after, 0),
_ = fluxer_gateway_env:load(),
session_manager_shards:ensure_shard_table(),
{ShardCount, _Source} = determine_shard_count(),
{ShardCount, _Source} = shard_utils:determine_shard_count(session_shards),
Shards = session_manager_shards:start_shards(ShardCount),
State = #{shards => Shards, shard_count => ShardCount},
session_manager_shards:sync_shard_table(State),
@@ -316,19 +316,3 @@ atom_list([Value | Rest], Acc) when is_atom(Value) ->
atom_list(Rest, [Value | Acc]);
atom_list(_, _) ->
error.
-spec determine_shard_count() -> {pos_integer(), configured | auto}.
determine_shard_count() ->
case fluxer_gateway_env:get(session_shards) of
Value when is_integer(Value), Value > 0 ->
{Value, configured};
_ ->
{default_shard_count(), auto}
end.
-spec default_shard_count() -> pos_integer().
default_shard_count() ->
shard_utils:max_positive([
erlang:system_info(logical_processors_available),
erlang:system_info(schedulers_online)
]).
@@ -93,28 +93,12 @@ safe_shard_call(Pid, Request) ->
-spec find_shard_by_ref(reference(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_shard_by_ref(Ref, Shards) ->
maps:fold(
fun
(_Index, _Shard, {ok, _} = Found) -> Found;
(Index, #{ref := ExistingRef}, not_found) when ExistingRef =:= Ref -> {ok, Index};
(_Index, _Shard, not_found) -> not_found
end,
not_found,
Shards
).
shard_utils:find_shard_by_ref(Ref, Shards).
-spec find_shard_by_pid(pid(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_shard_by_pid(Pid, Shards) ->
maps:fold(
fun
(_Index, _Shard, {ok, _} = Found) -> Found;
(Index, #{pid := ExistingPid}, not_found) when ExistingPid =:= Pid -> {ok, Index};
(_Index, _Shard, not_found) -> not_found
end,
not_found,
Shards
).
shard_utils:find_shard_by_pid(Pid, Shards).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@@ -188,6 +172,21 @@ aggregate_handoff_to_topology_marks_invalid_reply_as_failed_test() ->
aggregate_handoff_to_topology(['peer@host'], State),
InvalidShardPid ! stop.
aggregate_handoff_to_topology_counts_unavailable_and_bare_replies_test() ->
ExitedShardPid = spawn(fun() -> ok end),
ok = gateway_retry_timer:wait(5),
BareShardPid = spawn(fun() -> shard_bare_reply_test_loop(2) end),
State = #{
shards => #{
0 => #{pid => ExitedShardPid, ref => make_ref()},
1 => #{pid => BareShardPid, ref => make_ref()}
},
shard_count => 2
},
{#{attempted := 3, handed_off := 2}, _State} =
aggregate_handoff_to_topology(['peer@host'], State),
BareShardPid ! stop.
shard_reconnect_test_loop(DrainCount) ->
receive
{'$gen_call', From, reconnect_drain} ->
@@ -217,10 +216,13 @@ shard_handoff_test_loop(Result) ->
end.
shard_invalid_reply_test_loop() ->
shard_bare_reply_test_loop(invalid_reply).
shard_bare_reply_test_loop(Reply) ->
receive
{'$gen_call', From, _Request} ->
gen_server:reply(From, invalid_reply),
shard_invalid_reply_test_loop();
gen_server:reply(From, Reply),
shard_bare_reply_test_loop(Reply);
stop ->
ok
after 5000 ->
+40
View File
@@ -5,6 +5,9 @@
-export([
max_positive/1,
determine_shard_count/1,
find_shard_by_ref/2,
find_shard_by_pid/2,
safe_gen_call/3,
safe_gen_call_detailed/3,
safe_gen_call_remote/3,
@@ -14,10 +17,47 @@
check_mailbox_pressure/1
]).
-export_type([shard/0]).
-type shard() :: #{pid := pid(), ref := reference()}.
-spec max_positive(list()) -> pos_integer().
max_positive(Candidates) ->
lists:max([C || C <- Candidates, is_integer(C), C > 0] ++ [1]).
-spec determine_shard_count(atom()) -> {pos_integer(), configured | auto}.
determine_shard_count(ConfigKey) ->
case fluxer_gateway_env:get(ConfigKey) of
Value when is_integer(Value), Value > 0 ->
{Value, configured};
_ ->
{clustered_ets_cache:default_shard_count(), auto}
end.
-spec find_shard_by_ref(reference(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_shard_by_ref(Ref, Shards) ->
maps:fold(
fun
(Index, #{ref := R}, _) when R =:= Ref -> {ok, Index};
(_, _, Acc) -> Acc
end,
not_found,
Shards
).
-spec find_shard_by_pid(pid(), #{non_neg_integer() => shard()}) ->
{ok, non_neg_integer()} | not_found.
find_shard_by_pid(Pid, Shards) ->
maps:fold(
fun
(Index, #{pid := P}, _) when P =:= Pid -> {ok, Index};
(_, _, Acc) -> Acc
end,
not_found,
Shards
).
-spec safe_gen_call(pid() | atom(), term(), pos_integer()) -> term().
safe_gen_call(Pid, Request, Timeout) ->
try