feat: improve guild collection rpcs

This commit is contained in:
Hampus Kraft
2026-02-18 20:50:11 +00:00
parent 571a8af29d
commit 67267d509d
3 changed files with 435 additions and 13 deletions

View File

@@ -22,6 +22,16 @@
-define(BATCH_SIZE, 10).
-define(BATCH_DELAY_MS, 100).
-define(GUILD_COLLECTION_FETCH_TIMEOUT_MS, 120000).
-define(GUILD_MEMBER_COLLECTION_LIMIT, 250).
-define(GUILD_COLLECTIONS, [
<<"guild">>,
<<"roles">>,
<<"channels">>,
<<"emojis">>,
<<"stickers">>,
<<"members">>
]).
-export([start_link/1]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
@@ -468,16 +478,195 @@ lookup_existing_guild(GuildId, GuildName, State) ->
-spec fetch_guild_data(guild_id()) -> fetch_result().
fetch_guild_data(GuildId) ->
Parent = self(),
Ref = make_ref(),
_ = [
spawn_monitor(fun() ->
Parent ! {Ref, Collection, fetch_guild_collection(GuildId, Collection)}
end)
|| Collection <- ?GUILD_COLLECTIONS
],
DeadlineMs = erlang:monotonic_time(millisecond) + ?GUILD_COLLECTION_FETCH_TIMEOUT_MS,
collect_guild_collection_results(Ref, ?GUILD_COLLECTIONS, #{}, DeadlineMs).
-spec collect_guild_collection_results(reference(), [binary()], guild_data(), integer()) ->
fetch_result().
collect_guild_collection_results(_Ref, [], Acc, _DeadlineMs) ->
{ok, Acc};
collect_guild_collection_results(Ref, PendingCollections, Acc, DeadlineMs) ->
NowMs = erlang:monotonic_time(millisecond),
RemainingMs = DeadlineMs - NowMs,
case RemainingMs > 0 of
false ->
{error, {guild_collection_fetch_timeout, PendingCollections}};
true ->
receive
{Ref, Collection, {ok, Data}} ->
Key = guild_collection_result_key(Collection),
NewPending = lists:delete(Collection, PendingCollections),
NewAcc = maps:put(Key, Data, Acc),
collect_guild_collection_results(Ref, NewPending, NewAcc, DeadlineMs);
{Ref, Collection, {error, Reason}} ->
{error, {guild_collection_fetch_failed, Collection, Reason}};
{'DOWN', _, process, _, _} ->
collect_guild_collection_results(Ref, PendingCollections, Acc, DeadlineMs)
after RemainingMs ->
{error, {guild_collection_fetch_timeout, PendingCollections}}
end
end.
-spec guild_collection_result_key(binary()) -> binary().
guild_collection_result_key(<<"guild">>) -> <<"guild">>;
guild_collection_result_key(<<"roles">>) -> <<"roles">>;
guild_collection_result_key(<<"channels">>) -> <<"channels">>;
guild_collection_result_key(<<"emojis">>) -> <<"emojis">>;
guild_collection_result_key(<<"stickers">>) -> <<"stickers">>;
guild_collection_result_key(<<"members">>) -> <<"members">>;
guild_collection_result_key(Collection) -> Collection.
-spec fetch_guild_collection(guild_id(), binary()) -> {ok, term()} | {error, term()}.
fetch_guild_collection(GuildId, <<"members">>) ->
fetch_guild_members_collection_stream(GuildId, undefined, []);
fetch_guild_collection(GuildId, Collection) ->
RpcRequest = #{
<<"type">> => <<"guild">>,
<<"type">> => <<"guild_collection">>,
<<"guild_id">> => type_conv:to_binary(GuildId),
<<"version">> => 1
<<"collection">> => Collection
},
rpc_client:call(RpcRequest).
case rpc_client:call(RpcRequest) of
{ok, Data} ->
case maps:get(Collection, Data, undefined) of
undefined -> {error, {invalid_collection_response, Collection}};
Value -> {ok, Value}
end;
{error, Reason} ->
{error, Reason}
end.
-spec fetch_guild_members_collection_stream(guild_id(), binary() | undefined, [[map()]]) ->
{ok, [map()]} | {error, term()}.
fetch_guild_members_collection_stream(GuildId, AfterUserId, ChunksAcc) ->
RpcRequest0 = #{
<<"type">> => <<"guild_collection">>,
<<"guild_id">> => type_conv:to_binary(GuildId),
<<"collection">> => <<"members">>,
<<"limit">> => ?GUILD_MEMBER_COLLECTION_LIMIT
},
RpcRequest = maybe_put_after_user_id(AfterUserId, RpcRequest0),
case rpc_client:call(RpcRequest) of
{ok, Data} ->
parse_members_collection_page(GuildId, Data, ChunksAcc);
{error, Reason} ->
{error, Reason}
end.
-spec parse_members_collection_page(guild_id(), map(), [[map()]]) -> {ok, [map()]} | {error, term()}.
parse_members_collection_page(GuildId, Data, ChunksAcc) ->
Members = maps:get(<<"members">>, Data, undefined),
HasMore = maps:get(<<"has_more">>, Data, false),
NextAfterUserId = maps:get(<<"next_after_user_id">>, Data, null),
case Members of
MemberList when is_list(MemberList) ->
parse_members_collection_page_result(
GuildId,
MemberList,
HasMore,
NextAfterUserId,
ChunksAcc
);
_ ->
{error, invalid_members_collection_payload}
end.
-spec parse_members_collection_page_result(
guild_id(),
[map()],
term(),
term(),
[[map()]]
) ->
{ok, [map()]} | {error, term()}.
parse_members_collection_page_result(
GuildId,
MemberList,
true,
NextAfterUserId,
ChunksAcc
) when is_binary(NextAfterUserId), MemberList =/= [] ->
fetch_guild_members_collection_stream(
GuildId,
NextAfterUserId,
[MemberList | ChunksAcc]
);
parse_members_collection_page_result(
_GuildId,
[],
true,
_NextAfterUserId,
_ChunksAcc
) ->
{error, invalid_members_collection_empty_page};
parse_members_collection_page_result(
_GuildId,
_MemberList,
true,
_NextAfterUserId,
_ChunksAcc
) ->
{error, invalid_members_collection_cursor};
parse_members_collection_page_result(
_GuildId,
MemberList,
false,
_NextAfterUserId,
ChunksAcc
) ->
{ok, lists:append(lists:reverse([MemberList | ChunksAcc]))};
parse_members_collection_page_result(
_GuildId,
_MemberList,
_HasMore,
_NextAfterUserId,
_ChunksAcc
) ->
{error, invalid_members_collection_has_more}.
-spec maybe_put_after_user_id(binary() | undefined, map()) -> map().
maybe_put_after_user_id(undefined, RpcRequest) ->
RpcRequest;
maybe_put_after_user_id(AfterUserId, RpcRequest) when is_binary(AfterUserId) ->
maps:put(<<"after_user_id">>, AfterUserId, RpcRequest).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
parse_members_collection_page_result_final_page_test() ->
Members = [
#{<<"user">> => #{<<"id">> => <<"1">>}}
],
?assertEqual(
{ok, Members},
parse_members_collection_page_result(42, Members, false, null, [])
).
parse_members_collection_page_result_invalid_cursor_test() ->
Members = [
#{<<"user">> => #{<<"id">> => <<"1">>}}
],
?assertEqual(
{error, invalid_members_collection_cursor},
parse_members_collection_page_result(42, Members, true, null, [])
).
maybe_put_after_user_id_test() ->
BaseRequest = #{
<<"type">> => <<"guild_collection">>,
<<"collection">> => <<"members">>
},
?assertEqual(BaseRequest, maybe_put_after_user_id(undefined, BaseRequest)),
WithCursor = maybe_put_after_user_id(<<"100">>, BaseRequest),
?assertEqual(<<"100">>, maps:get(<<"after_user_id">>, WithCursor)).
select_guilds_to_reload_empty_ids_test() ->
Guilds = #{1 => {self(), make_ref()}, 2 => {self(), make_ref()}},
Result = select_guilds_to_reload([], Guilds),