fix(api): batch member user lookups on guild load (#2783)

This commit is contained in:
Hampus
2026-09-14 23:06:36 +02:00
committed by GitHub
parent c212d315f4
commit 50cec92738
4 changed files with 26 additions and 4 deletions
+3 -2
View File
@@ -365,8 +365,9 @@ FLUXER_DISCOVERY_ENABLED=true
# messages, 320 for snowflakes and 64 elsewhere, and they govern every service
# Compose does not forward this to. A router holds a slot for the whole round
# trip to its shard, so this is a ceiling on requests in flight at once and not a
# rate: too low a value does not slow requests down, it rejects them, and the api
# turns that rejection into a 503.
# rate: too low a value does not slow requests down, it rejects them. The api
# turns that rejection into a 503 and logs "shard rejected the request because
# it is at its concurrency limit".
#FLUXER_SVC_MAX_CONCURRENT_REQUESTS=192
# The api and the Rust services name their fixed Postgres statement shapes so the
+4
View File
@@ -795,6 +795,10 @@ export class RpcService {
requestCache: RequestCache;
}): Promise<Array<GuildMemberResponse>> {
const {guildId, members, requestCache} = params;
await this.userCacheService.getUserPartialResponses(
members.map((member) => member.userId),
requestCache,
);
const mappedMembers = await allSettledWithConcurrency(members, RPC_RESPONSE_MAP_CONCURRENCY, (member) =>
mapGuildMemberToResponse(member, this.userCacheService, requestCache),
);
@@ -1,11 +1,12 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {setupTestGuildWithMembers} from '@app/api/guild/tests/GuildTestUtils';
import {createUsersServiceClient} from '@app/api/infrastructure/UsersServiceClient';
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
import {HTTP_STATUS, TEST_IDS} from '@app/api/test/TestConstants';
import {createBuilder} from '@app/api/test/TestRequestBuilder';
import {RpcRequest} from '@fluxer/schema/src/domains/rpc/RpcSchemas';
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
import {afterEach, beforeEach, describe, expect, test, vi} from 'vitest';
interface RpcGuildCollectionMembersResponse {
type: 'guild_collection';
@@ -69,6 +70,22 @@ describe('RpcService guild member collection pagination', () => {
expect(seenIds.slice().sort()).toStrictEqual(expectedIds.slice().sort());
});
test('looks up every member user on a page in one users service request', async () => {
const {owner, members, guild} = await setupTestGuildWithMembers(harness, 3);
const memberIds = [owner.userId, ...members.map((member) => member.userId)].sort();
const lookups = vi.spyOn(createUsersServiceClient(), 'getUserPartialResponses');
try {
const data = await fetchMemberPage(harness, guild.id, 1000);
expect(data.members.map((member) => member.user.id).sort()).toStrictEqual(memberIds);
const memberLookups = lookups.mock.calls
.map(([userIds]) => userIds.map(String).sort())
.filter((userIds) => userIds.some((userId) => memberIds.includes(userId)));
expect(memberLookups).toStrictEqual([memberIds]);
} finally {
lookups.mockRestore();
}
});
test('rejects an unknown guild on the first member page', async () => {
await createBuilder(harness, '')
.post('/test/rpc-session-init')
@@ -605,7 +605,7 @@ Default `8090`. The health and metrics port. Not published.
#### `FLUXER_SVC_MAX_CONCURRENT_REQUESTS`
Defaults to 192 for messages, 320 for snowflakes, 64 otherwise. In-flight request ceiling. The built-in defaults key off `FLUXER_SVC_NAME`. Compose forwards `FLUXER_SVC_MAX_CONCURRENT_REQUESTS` to `users`, `users-shard`, `messages`, and `messages-shard` at a default of 20, which pairs with their 20-connection Postgres pools, and leaves the other containers on the built-in defaults.
Defaults to 192 for messages, 320 for snowflakes, 64 otherwise. In-flight request ceiling. The built-in defaults key off `FLUXER_SVC_NAME`. Compose forwards `FLUXER_SVC_MAX_CONCURRENT_REQUESTS` to `users`, `users-shard`, `messages`, and `messages-shard`, and leaves the other containers on the built-in defaults. Left unset, those four keep their built-in defaults too. Once set, the one value replaces the default on all four, so a value below 192 also lowers the messages ceiling. A router holds a slot for the whole round trip to its shard. A request over the ceiling is rejected at once. The API request behind it fails with a 503, and the API logs `shard rejected the request because it is at its concurrency limit`.
#### `POD_NAME`