scoreboard-xcpcio: experimental scoreboard caching

This commit is contained in:
undefined committed 2025-11-09 00:07:59 +08:00
1 parent 643416373f
commit ccfa4674ed
2 files changed
+93 -37

No files matched your search

+91 -35
View File
@@ -1,6 +1,8 @@
import path from 'path'; import path from 'path';
import { LRUCache } from 'lru-cache';
import { import {
avatar, ContestModel, Context, fs, getAlphabeticId, ObjectId, PERM, Schema, STATUS, Types, UserModel, avatar, ContestModel, Context, fs, getAlphabeticId, ObjectId, PERM,
RecordDoc, Schema, STATUS, Tdoc, Types, UserModel,
} from 'hydrooj'; } from 'hydrooj';
const file = fs.readFileSync(path.join(__dirname, 'public/assets/board.html'), 'utf8'); const file = fs.readFileSync(path.join(__dirname, 'public/assets/board.html'), 'utf8');
@@ -23,22 +25,19 @@ const status = {
[STATUS.STATUS_CANCELED]: 'CANCELED', [STATUS.STATUS_CANCELED]: 'CANCELED',
}; };
export async function apply(ctx: Context) { function submissionBase(tdoc: Tdoc, rdoc: RecordDoc) {
ctx.inject(['scoreboard'], ({ scoreboard }) => { // NOTE: rdoc can be either record, or a tsdoc detail entry
scoreboard.addView('xcpcio', 'XCPCIO', { const submit = new ObjectId(rdoc._id || (rdoc as any).rid).getTimestamp().getTime();
tdoc: 'tdoc', return {
groups: 'groups', problem_id: tdoc.pids.indexOf(rdoc.pid),
json: Types.Boolean, team_id: `${rdoc.uid}`,
realtime: Types.Boolean, timestamp: Math.floor(submit - tdoc.beginAt.getTime()),
gold: Schema.transform(Schema.union([Schema.string(), Schema.number().step(1).min(0)]), (v) => +v).default(0), language: rdoc.lang || '',
silver: Schema.transform(Schema.union([Schema.string(), Schema.number().step(1).min(0)]), (v) => +v).default(0), submission_id: rdoc._id,
bronze: Schema.transform(Schema.union([Schema.string(), Schema.number().step(1).min(0)]), (v) => +v).default(0), };
}, { }
async display({
tdoc, groups, json, realtime, gold, silver, bronze, async function loadContestState(tdoc: Tdoc, realtime: boolean) {
}) {
if (realtime && !this.user.own(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
if (json || this.request.json) {
const tsdocs = await ContestModel.getMultiStatus(tdoc.domainId, { docId: tdoc.docId }).toArray(); const tsdocs = await ContestModel.getMultiStatus(tdoc.domainId, { docId: tdoc.docId }).toArray();
const udict = await UserModel.getList(tdoc.domainId, tsdocs.map((i) => i.uid)); const udict = await UserModel.getList(tdoc.domainId, tsdocs.map((i) => i.uid));
const teams = tsdocs.map((i) => { const teams = tsdocs.map((i) => {
@@ -56,7 +55,80 @@ export async function apply(ctx: Context) {
], ],
}; };
}); });
const relatedGroups = teams.flatMap((i) => i.group); return {
submissions: tsdocs.flatMap((i) => (i.journal || []).map((j) => {
const submit = new ObjectId(j.rid as string).getTimestamp().getTime();
const curStatus = status[j.status] || 'SYSTEM_ERROR';
return {
...submissionBase(tdoc, j),
status: (ContestModel.isLocked(tdoc) && submit > tdoc.lockAt.getTime() && !realtime)
? 'PENDING'
: curStatus,
};
})),
teams,
};
}
export const name = 'scoreboard-xcpcio';
export const Config = Schema.object({
cacheTTL: Schema.number().default(0).description('Cache TTL in milliseconds'),
cacheSize: Schema.number().default(100).description('Cache size'),
});
export async function apply(ctx: Context, config: ReturnType<typeof Config>) {
const lru = new LRUCache<string, Awaited<ReturnType<typeof loadContestState>>>({
max: config.cacheSize,
ttl: config.cacheTTL,
// NOTE: currently we force all entries to expire
// to make sure that any patch error won't last too long
// can use updateAgeOnGet=false,updateAgeOnHas=false later if this patching is fully tested.
noUpdateTTL: true,
});
if (config.cacheTTL) {
ctx.on('record/judge', async (rdoc) => {
if (!rdoc.contest) return;
const realtime = lru.get(`${rdoc.contest.toHexString()}/realtime`);
const pub = lru.get(`${rdoc.contest.toHexString()}/public`);
if (!realtime && !pub) return;
const tdoc = await ContestModel.get(rdoc.domainId, rdoc.contest);
const submit = new ObjectId(rdoc._id).getTimestamp().getTime();
const isLocked = ContestModel.isLocked(tdoc) && submit > tdoc.lockAt.getTime();
const statusStr = status[rdoc.status] || 'SYSTEM_ERROR';
if (realtime) {
const found = realtime.submissions.find((i) => i.submission_id === rdoc._id.toHexString());
if (found) found.status = statusStr;
else realtime.submissions.push({ ...submissionBase(tdoc, rdoc), status: statusStr });
}
if (pub) {
const found = pub.submissions.find((i) => i.submission_id === rdoc._id.toHexString());
if (found && !isLocked) found.status = statusStr;
else pub.submissions.push({ ...submissionBase(tdoc, rdoc), status: isLocked ? 'PENDING' : statusStr });
}
});
}
ctx.inject(['scoreboard'], ({ scoreboard }) => {
scoreboard.addView('xcpcio', 'XCPCIO', {
tdoc: 'tdoc',
groups: 'groups',
json: Types.Boolean,
realtime: Types.Boolean,
gold: Schema.transform(Schema.union([Schema.string(), Schema.number().step(1).min(0)]), (v) => +v).default(0),
silver: Schema.transform(Schema.union([Schema.string(), Schema.number().step(1).min(0)]), (v) => +v).default(0),
bronze: Schema.transform(Schema.union([Schema.string(), Schema.number().step(1).min(0)]), (v) => +v).default(0),
}, {
async display({
tdoc, groups, json, realtime, gold, silver, bronze,
}) {
if (realtime && !this.user.own(tdoc)) this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
if (json || this.request.json) {
const isLocked = ContestModel.isLocked(tdoc);
const cacheKey = `${tdoc.docId.toHexString()}/${(isLocked && realtime) ? 'realtime' : 'public'}`;
const state = lru.get(cacheKey) || await loadContestState(tdoc, realtime);
if (config.cacheTTL) lru.set(cacheKey, state);
const relatedGroups = state.teams.flatMap((i) => i.group);
this.response.body = { this.response.body = {
contest: { contest: {
contest_name: tdoc.title, contest_name: tdoc.title,
@@ -100,23 +172,7 @@ export async function apply(ctx: Context) {
submission_timestamp_unit: 'millisecond', submission_timestamp_unit: 'millisecond',
}, },
}, },
submissions: tsdocs.flatMap((i) => (i.journal || []).map((j) => { ...state,
const submit = new ObjectId(j.rid as string).getTimestamp().getTime();
const curStatus = status[j.status] || 'SYSTEM_ERROR';
return {
problem_id: tdoc.pids.indexOf(j.pid),
team_id: `${i.uid}`,
timestamp: Math.floor(submit - tdoc.beginAt.getTime()),
status: realtime
? curStatus
: ContestModel.isLocked(tdoc) && submit > tdoc.lockAt.getTime()
? 'PENDING'
: curStatus,
language: j.lang || '',
submission_id: j.rid,
};
})),
teams,
}; };
} else { } else {
this.response.template = 'xcpcio_board.html'; this.response.template = 'xcpcio_board.html';
+2 -2
View File
@@ -8,9 +8,9 @@
"postinstall": "node -r @hydrooj/register install.ts" "postinstall": "node -r @hydrooj/register install.ts"
}, },
"dependencies": { "dependencies": {
"@xcpcio/board-app": "^0.67.0" "@xcpcio/board-app": "^0.74.1"
}, },
"devDependencies": { "devDependencies": {
"@xcpcio/types": "^0.67.0" "@xcpcio/types": "^0.74.1"
} }
} }