Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions backend/src/api/controllers/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ import { MonkeyRequest } from "../types";
import { getFunbox, checkCompatibility } from "@monkeytype/funbox";
import { tryCatch } from "@monkeytype/util/trycatch";
import { getCachedConfiguration } from "../../init/configuration";
import { allTimeLeaderboardCache } from "../../utils/all-time-leaderboard-cache";

try {
if (!anticheatImplemented()) throw new Error("undefined");
Expand Down Expand Up @@ -534,6 +535,13 @@ export async function addResult(
},
dailyLeaderboardsConfig,
);
try {
allTimeLeaderboardCache.clear();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not the correct place, this is the dailyLeaderboard.

console.log("All-time leaderboard cache cleared");
} catch (error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how can this happen?

console.warn("Cache clear failed (non-critical):", error);
}

if (
dailyLeaderboardRank >= 1 &&
dailyLeaderboardRank <= 10 &&
Expand Down
16 changes: 15 additions & 1 deletion backend/src/api/controllers/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import MonkeyError, {
isFirebaseError,
} from "../../utils/error";
import { MonkeyResponse } from "../../utils/monkey-response";
import { getCached, setCached, invalidateUserCache } from "../../utils/cache";
import * as DiscordUtils from "../../utils/discord";
import {
buildAgentLog,
Expand Down Expand Up @@ -914,6 +915,10 @@ export async function getProfile(
): Promise<GetProfileResponse> {
const { uidOrName } = req.params;

const cacheKey = `user:profile:${uidOrName}`;
const cached = await getCached<GetProfileResponse>(cacheKey);
if (cached !== null) return cached;

const user = req.query.isUid
? await UserDAL.getUser(uidOrName, "get user profile")
: await UserDAL.getUserByName(uidOrName, "get user profile");
Expand Down Expand Up @@ -987,7 +992,11 @@ export async function getProfile(
};

if (banned) {
return new MonkeyResponse("Profile retrived: banned user", baseProfile);
await setCached(
cacheKey,
new MonkeyResponse("Profile retrieved: banned user", baseProfile),
);
return new MonkeyResponse("Profile retrieved: banned user", baseProfile);
}

const allTimeLbs = await getAllTimeLbs(user.uid);
Expand All @@ -1005,6 +1014,10 @@ export async function getProfile(
} else {
delete profileData.testActivity;
}
await setCached(
cacheKey,
new MonkeyResponse("Profile retrieved", profileData),
);
return new MonkeyResponse("Profile retrieved", profileData);
}

Expand Down Expand Up @@ -1050,6 +1063,7 @@ export async function updateProfile(
};

await UserDAL.updateProfile(uid, profileDetailsUpdates, user.inventory);
await invalidateUserCache(uid);

return new MonkeyResponse("Profile updated", profileDetailsUpdates);
}
Expand Down
Empty file removed backend/src/credentials/.gitkeep
Empty file.
35 changes: 27 additions & 8 deletions backend/src/dal/leaderboards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { DBUser, getUsersCollection } from "./user";
import MonkeyError from "../utils/error";
import { aggregateWithAcceptedConnections } from "./connections";

import { allTimeLeaderboardCache } from "../utils/all-time-leaderboard-cache";

export type DBLeaderboardEntry = LeaderboardEntry & {
_id: ObjectId;
};
Expand Down Expand Up @@ -46,6 +48,14 @@ export async function get(
throw new MonkeyError(500, "Invalid page or pageSize");
}

if (page === 0 && pageSize === 50 && uid === undefined) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets move the cache to the controller/leaderboard and remove unnecessary try/catch and logs

const cached = allTimeLeaderboardCache.get({ mode, language });
if (cached) {
console.log("✅ Cache HIT - leaderboards");
return cached.data as DBLeaderboardEntry[];
}
}

const skip = page * pageSize;
const limit = pageSize;

Expand Down Expand Up @@ -83,12 +93,23 @@ export async function get(
leaderboard = leaderboard.map((it) => omit(it, ["isPremium"]));
}

if (page === 0 && pageSize === 50 && uid === undefined) {
try {
allTimeLeaderboardCache.set(
{ mode, language },
leaderboard,
await getCount(mode, mode2, language),
);
console.log(" Cache SET - leaderboards");
} catch (error) {
console.warn("Cache set failed:", error);
}
}

return leaderboard;
} catch (e) {
// oxlint-disable-next-line no-unsafe-member-access
if (e.error === 175) {
if ((e as unknown as { error: number }).error === 175) {
//QueryPlanKilled, collection was removed during the query
return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

Copy link
Author

@LuckySilver0021 LuckySilver0021 Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reverting this will scream linting errors and it wont commit the code changes, I tried fixing it but I couldnt make it.
help me with this please, but apart from that I have successfully made the requested changes.

as of now I will just update the pr except this part only becuase I couldnt commit and push the changes,

further let me know how to deal with that.
Thanks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// oxlint-disable-next-line no-unsafe-member-access is telling the linter to ignore this. npm run lint-be doesn't fail for me.

}
throw e;
}
Expand Down Expand Up @@ -162,10 +183,8 @@ export async function getRank(
return results[0] ?? null;
}
} catch (e) {
// oxlint-disable-next-line no-unsafe-member-access
if (e.error === 175) {
if ((e as unknown as { error: number }).error === 175) {
//QueryPlanKilled, collection was removed during the query
return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

}
throw e;
}
Expand Down Expand Up @@ -393,8 +412,8 @@ async function createIndex(
Logger.warning(`Index ${key} not matching, dropping and recreating...`);

const existingIndex = (await getUsersCollection().listIndexes().toArray())
// oxlint-disable-next-line no-unsafe-member-access
.map((it) => it.name as string)

.map((it: unknown) => (it as { name: string }).name)
.find((it) => it.startsWith(key));

if (existingIndex !== undefined && existingIndex !== null) {
Expand Down
40 changes: 40 additions & 0 deletions backend/src/utils/all-time-leaderboard-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
type AllTimeCacheKey = {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add mode2 to the cache key

mode: string;
language: string;
};

type CacheEntry = {
data: unknown[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep the type

count: number;
timestamp: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need a timestamp and only clear using .clear()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the timestamp is removed then how will we we keep track of the 15 minutes?
or maybe What Im thinking is youre trying to say that the data will be cached until the data is changed explicitly like someone came into the leaderboard or someone overtook someone's position.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check the discussion in the original pr: #7415 (comment)

We know when the leaderboard is refreshed and can invalidate the cache at this time.

invalidate/empty the cache on LeaderboardDal.update

};

class AllTimeLeaderboardCache {
private cache = new Map<string, CacheEntry>();
private readonly TTL = 900_000; // == 15 minutes of TTL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove


private getKey({ mode, language }: AllTimeCacheKey): string {
return `alltime-lb:${mode}:${language}`;
}

get(key: AllTimeCacheKey): CacheEntry | null {
const cacheKey = this.getKey(key);
const entry = this.cache.get(cacheKey);

if (!entry || Date.now() - entry.timestamp > this.TTL) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove

if (entry) this.cache.delete(cacheKey);
return null;
}
return entry;
}

set(key: AllTimeCacheKey, data: unknown[], count: number): void {
this.cache.set(this.getKey(key), { data, count, timestamp: Date.now() });
}

clear(): void {
this.cache.clear();
}
}

export const allTimeLeaderboardCache = new AllTimeLeaderboardCache();
42 changes: 42 additions & 0 deletions backend/src/utils/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { getConnection } from "../init/redis";

const CACHE_PREFIX = "cache:";
const TTL = 300; // == 5 minutes

export async function getCached<T>(key: string): Promise<T | null> {
const redis = getConnection();
if (!redis) return null;

try {
const data = await redis.get(`${CACHE_PREFIX}${key}`);
if (data === null || data === undefined || data === "") return null;
return JSON.parse(data) as T;
} catch {
return null;
}
}

export async function setCached<T>(key: string, data: T): Promise<void> {
const redis = getConnection();
if (!redis) return;

try {
await redis.setex(`${CACHE_PREFIX}${key}`, TTL, JSON.stringify(data));
} catch (err) {
console.error("Cache set failed:", err);
}
}

export async function invalidateUserCache(userId: string): Promise<void> {
const redis = getConnection();
if (!redis) return;

try {
const keys = await redis.keys(`${CACHE_PREFIX}user:profile:${userId}*`);
if (keys.length > 0) {
await redis.del(keys);
}
} catch (err) {
console.error("Cache invalidation failed:", err);
}
}
2 changes: 1 addition & 1 deletion package.json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"oxlint": "1.40.0",
"oxlint-tsgolint": "0.11.1",
"prettier": "3.7.1",
"turbo": "2.5.6",
"turbo": "2.7.5",
"vitest": "4.0.15"
},
"lint-staged": {
Expand Down
Loading