feat: improve guild collection rpcs
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user