Skip to content

Commit 949e2ba

Browse files
authored
perf: optimize friend queries (@fehmer) (monkeytypegame#7080)
Combine two queries (first get all friend UIDs, then call leaderboard) into one query to reduce db roundtrips. Use the same approach for the friends list in user dal. Note: when updating mongodb to 6+ we could use unionWith in case we don't need the metadata (lb use-case)
1 parent 81f09b9 commit 949e2ba

File tree

8 files changed

+446
-282
lines changed

8 files changed

+446
-282
lines changed

backend/__tests__/__integration__/dal/connections.spec.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ObjectId } from "mongodb";
1111

1212
import * as ConnectionsDal from "../../../src/dal/connections";
1313
import { createConnection } from "../../__testData__/connections";
14+
import { createUser } from "../../__testData__/users";
1415

1516
describe("ConnectionsDal", () => {
1617
beforeAll(async () => {
@@ -401,4 +402,92 @@ describe("ConnectionsDal", () => {
401402
]);
402403
});
403404
});
405+
406+
describe("aggregateWithAcceptedConnections", () => {
407+
it("should return friend uids", async () => {
408+
//GIVE
409+
const uid = (await createUser()).uid;
410+
const friendOne = await createConnection({
411+
initiatorUid: uid,
412+
receiverUid: (await createUser()).uid,
413+
status: "accepted",
414+
});
415+
const friendTwo = await createConnection({
416+
initiatorUid: (await createUser()).uid,
417+
receiverUid: uid,
418+
status: "accepted",
419+
});
420+
const friendThree = await createConnection({
421+
initiatorUid: (await createUser()).uid,
422+
receiverUid: uid,
423+
status: "accepted",
424+
});
425+
const _pending = await createConnection({
426+
initiatorUid: uid,
427+
receiverUid: (await createUser()).uid,
428+
status: "pending",
429+
});
430+
const _blocked = await createConnection({
431+
initiatorUid: uid,
432+
receiverUid: (await createUser()).uid,
433+
status: "blocked",
434+
});
435+
const _decoy = await createConnection({
436+
receiverUid: (await createUser()).uid,
437+
status: "accepted",
438+
});
439+
440+
//WHEN
441+
const friendUids = await ConnectionsDal.aggregateWithAcceptedConnections<{
442+
uid: string;
443+
}>({ collectionName: "users", uid }, [{ $project: { uid: true } }]);
444+
445+
//THEN
446+
expect(friendUids.flatMap((it) => it.uid).toSorted()).toEqual([
447+
uid,
448+
friendOne.receiverUid,
449+
friendTwo.initiatorUid,
450+
friendThree.initiatorUid,
451+
]);
452+
});
453+
it("should return friend uids and metaData", async () => {
454+
//GIVE
455+
const me = await createUser();
456+
const friend = await createUser();
457+
458+
const connection = await createConnection({
459+
initiatorUid: me.uid,
460+
receiverUid: friend.uid,
461+
status: "accepted",
462+
});
463+
464+
//WHEN
465+
const friendUids = await ConnectionsDal.aggregateWithAcceptedConnections(
466+
{ collectionName: "users", uid: me.uid, includeMetaData: true },
467+
[
468+
{
469+
$project: {
470+
uid: true,
471+
lastModified: "$connectionMeta.lastModified",
472+
connectionId: "$connectionMeta._id",
473+
},
474+
},
475+
]
476+
);
477+
478+
//THEN
479+
expect(friendUids).toEqual([
480+
{
481+
_id: friend._id,
482+
connectionId: connection._id,
483+
lastModified: connection.lastModified,
484+
uid: friend.uid,
485+
},
486+
{
487+
_id: me._id,
488+
uid: me.uid,
489+
},
490+
]);
491+
});
492+
});
404493
});

backend/__tests__/__integration__/dal/leaderboards.isolated.spec.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it, expect, afterEach, vi } from "vitest";
1+
import { describe, it, expect, vi, afterEach } from "vitest";
22
import _ from "lodash";
33
import { ObjectId } from "mongodb";
44
import * as UserDal from "../../../src/dal/user";
@@ -11,6 +11,7 @@ import * as DB from "../../../src/init/db";
1111
import { LbPersonalBests } from "../../../src/utils/pb";
1212

1313
import { pb } from "../../__testData__/users";
14+
import { createConnection } from "../../__testData__/connections";
1415

1516
describe("LeaderboardsDal", () => {
1617
afterEach(async () => {
@@ -307,9 +308,20 @@ describe("LeaderboardsDal", () => {
307308
it("should get for friends only", async () => {
308309
//GIVEN
309310
const rank1 = await createUser(lbBests(pb(90), pb(100, 90, 2)));
311+
const uid = rank1.uid;
310312
const _rank2 = await createUser(lbBests(undefined, pb(100, 90, 1)));
311313
const _rank3 = await createUser(lbBests(undefined, pb(100, 80, 2)));
312314
const rank4 = await createUser(lbBests(undefined, pb(90, 100, 1)));
315+
316+
//two friends, one is not on the leaderboard
317+
await createConnection({
318+
initiatorUid: uid,
319+
receiverUid: rank4.uid,
320+
status: "accepted",
321+
});
322+
323+
await createConnection({ initiatorUid: uid, status: "accepted" });
324+
313325
await LeaderboardsDal.update("time", "60", "english");
314326

315327
//WHEN
@@ -321,7 +333,7 @@ describe("LeaderboardsDal", () => {
321333
0,
322334
50,
323335
false,
324-
[rank1.uid, rank4.uid]
336+
uid
325337
)) as LeaderboardsDal.DBLeaderboardEntry[];
326338

327339
//THEN
@@ -335,11 +347,23 @@ describe("LeaderboardsDal", () => {
335347
it("should get for friends only with page", async () => {
336348
//GIVEN
337349
const rank1 = await createUser(lbBests(pb(90), pb(105, 90, 2)));
350+
const uid = rank1.uid;
338351
const rank2 = await createUser(lbBests(undefined, pb(100, 90, 1)));
339352
const _rank3 = await createUser(lbBests(undefined, pb(95, 80, 2)));
340353
const rank4 = await createUser(lbBests(undefined, pb(90, 100, 1)));
341354
await LeaderboardsDal.update("time", "60", "english");
342355

356+
await createConnection({
357+
initiatorUid: uid,
358+
receiverUid: rank2.uid,
359+
status: "accepted",
360+
});
361+
await createConnection({
362+
initiatorUid: rank4.uid,
363+
receiverUid: uid,
364+
status: "accepted",
365+
});
366+
343367
//WHEN
344368
const results = (await LeaderboardsDal.get(
345369
"time",
@@ -348,7 +372,7 @@ describe("LeaderboardsDal", () => {
348372
1,
349373
2,
350374
false,
351-
[rank1.uid, rank2.uid, rank4.uid]
375+
uid
352376
)) as LeaderboardsDal.DBLeaderboardEntry[];
353377

354378
//THEN
@@ -360,6 +384,7 @@ describe("LeaderboardsDal", () => {
360384
});
361385
it("should return empty list if no friends", async () => {
362386
//GIVEN
387+
const uid = new ObjectId().toHexString();
363388

364389
//WHEN
365390
const results = (await LeaderboardsDal.get(
@@ -369,7 +394,7 @@ describe("LeaderboardsDal", () => {
369394
1,
370395
2,
371396
false,
372-
[]
397+
uid
373398
)) as LeaderboardsDal.DBLeaderboardEntry[];
374399
//THEN
375400
expect(results).toEqual([]);
@@ -378,10 +403,10 @@ describe("LeaderboardsDal", () => {
378403
describe("getCount / getRank", () => {
379404
it("should get count", async () => {
380405
//GIVEN
381-
await createUser(lbBests(undefined, pb(105)));
382-
await createUser(lbBests(undefined, pb(100)));
383-
const me = await createUser(lbBests(undefined, pb(95)));
384-
await createUser(lbBests(undefined, pb(90)));
406+
await createUser(lbBests(undefined, pb(105)), { name: "One" });
407+
await createUser(lbBests(undefined, pb(100)), { name: "Two" });
408+
const me = await createUser(lbBests(undefined, pb(95)), { name: "Me" });
409+
await createUser(lbBests(undefined, pb(90)), { name: "Three" });
385410
await LeaderboardsDal.update("time", "60", "english");
386411

387412
//WHEN / THEN
@@ -405,19 +430,26 @@ describe("LeaderboardsDal", () => {
405430
await createUser(lbBests(undefined, pb(95)));
406431
const friendTwo = await createUser(lbBests(undefined, pb(90)));
407432
const me = await createUser(lbBests(undefined, pb(99)));
408-
409-
console.log("me", me.uid);
410-
411433
await LeaderboardsDal.update("time", "60", "english");
412434

413-
const friends = [friendOne.uid, friendTwo.uid, me.uid];
435+
await createConnection({
436+
initiatorUid: me.uid,
437+
receiverUid: friendOne.uid,
438+
status: "accepted",
439+
});
440+
441+
await createConnection({
442+
initiatorUid: friendTwo.uid,
443+
receiverUid: me.uid,
444+
status: "accepted",
445+
});
414446

415447
//WHEN / THEN
416448

417-
expect(await LeaderboardsDal.getCount("time", "60", "english", friends)) //
449+
expect(await LeaderboardsDal.getCount("time", "60", "english", me.uid)) //
418450
.toEqual(3);
419451
expect(
420-
await LeaderboardsDal.getRank("time", "60", "english", me.uid, friends)
452+
await LeaderboardsDal.getRank("time", "60", "english", me.uid, true)
421453
) //
422454
.toEqual(
423455
expect.objectContaining({

backend/__tests__/__testData__/connections.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ export async function createConnection(
1616
},
1717
maxPerUser
1818
);
19-
await ConnectionsDal.getCollection().updateOne(
20-
{ _id: result._id },
21-
{ $set: data }
22-
);
19+
await ConnectionsDal.__testing
20+
.getCollection()
21+
.updateOne({ _id: result._id }, { $set: data });
2322
return { ...result, ...data };
2423
}

backend/__tests__/api/controllers/leaderboard.spec.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,10 @@ describe("Loaderboard Controller", () => {
3030
describe("get leaderboard", () => {
3131
const getLeaderboardMock = vi.spyOn(LeaderboardDal, "get");
3232
const getLeaderboardCountMock = vi.spyOn(LeaderboardDal, "getCount");
33-
const getFriendsUidsMock = vi.spyOn(ConnectionsDal, "getFriendsUids");
3433

3534
beforeEach(() => {
3635
getLeaderboardMock.mockClear();
3736
getLeaderboardCountMock.mockClear();
38-
getFriendsUidsMock.mockClear();
3937
getLeaderboardCountMock.mockResolvedValue(42);
4038
});
4139

@@ -154,7 +152,6 @@ describe("Loaderboard Controller", () => {
154152
//GIVEN
155153
await enableConnectionsFeature(true);
156154
getLeaderboardMock.mockResolvedValue([]);
157-
getFriendsUidsMock.mockResolvedValue(["uidOne", "uidTwo"]);
158155
getLeaderboardCountMock.mockResolvedValue(2);
159156

160157
//WHEN
@@ -180,13 +177,13 @@ describe("Loaderboard Controller", () => {
180177
0,
181178
50,
182179
false,
183-
["uidOne", "uidTwo"]
180+
uid
184181
);
185182
expect(getLeaderboardCountMock).toHaveBeenCalledWith(
186183
"time",
187184
"60",
188185
"english",
189-
["uidOne", "uidTwo"]
186+
uid
190187
);
191188
});
192189

@@ -286,11 +283,9 @@ describe("Loaderboard Controller", () => {
286283

287284
describe("get rank", () => {
288285
const getLeaderboardRankMock = vi.spyOn(LeaderboardDal, "getRank");
289-
const getFriendsUidsMock = vi.spyOn(ConnectionsDal, "getFriendsUids");
290286

291287
afterEach(() => {
292288
getLeaderboardRankMock.mockClear();
293-
getFriendsUidsMock.mockClear();
294289
});
295290

296291
it("fails withouth authentication", async () => {
@@ -335,14 +330,12 @@ describe("Loaderboard Controller", () => {
335330
"60",
336331
"english",
337332
uid,
338-
undefined
333+
false
339334
);
340335
});
341336
it("should get for english time 60 friends only", async () => {
342337
//GIVEN
343338
await enableConnectionsFeature(true);
344-
const friends = ["friendOne", "friendTwo"];
345-
getFriendsUidsMock.mockResolvedValue(friends);
346339
getLeaderboardRankMock.mockResolvedValue({} as any);
347340

348341
//WHEN
@@ -363,9 +356,8 @@ describe("Loaderboard Controller", () => {
363356
"60",
364357
"english",
365358
uid,
366-
friends
359+
true
367360
);
368-
expect(getFriendsUidsMock).toHaveBeenCalledWith(uid);
369361
});
370362
it("should get with ape key", async () => {
371363
await acceptApeKeys(true);

0 commit comments

Comments
 (0)