Skip to content

Commit

Permalink
(wip)
Browse files Browse the repository at this point in the history
  • Loading branch information
shine00chang committed Dec 19, 2024
1 parent 94d9e46 commit dab7c7c
Show file tree
Hide file tree
Showing 12 changed files with 134 additions and 10,529 deletions.
10,449 changes: 0 additions & 10,449 deletions packages/api-server/docs/api.yaml

This file was deleted.

44 changes: 35 additions & 9 deletions packages/api-server/src/routes/v1/contest.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ import {
TeamInvitationSchema,
ProblemSchema
} from '@argoncs/types'
import { fetchContestProblem } from '@argoncs/common'
import {
UnauthorizedError,
badRequestSchema,
conflictSchema,
forbiddenSchema,
methodNotAllowedSchema,
notFoundSchema,
unauthorizedSchema
unauthorizedSchema,
BadRequestError
} from 'http-errors-enhanced'
import {
contestBegan,
Expand All @@ -42,7 +42,8 @@ import {
removeProblemFromContest,
updateContest,
publishContest,
fetchPublishedContests
fetchPublishedContests,
fetchContestProblem
} from '../../services/contest.services.js'
import {
createTeam,
Expand All @@ -55,12 +56,11 @@ import {
makeTeamCaptain,
removeTeamMember
} from '../../services/team.services.js'
import { isTeamCaptain, isTeamMember } from '../../auth/team.auth.js'
import { createSubmission, querySubmissions } from '../../services/submission.services.js'
import { isTeamCaptain } from '../../auth/team.auth.js'
import { createSubmission, querySubmissions, rejudgeProblem } from '../../services/submission.services.js'
import { hasVerifiedEmail } from '../../auth/email.auth.js'
import { userAuthHook } from '../../hooks/authentication.hooks.js'
import { contestInfoHook } from '../../hooks/contest.hooks.js'
import { requestUserProfile } from '../../utils/auth.utils.js'
import { fetchUser } from '../../services/user.services.js'
import { createPolygonUploadSession } from '../../services/testcase.services.js'

Expand Down Expand Up @@ -165,9 +165,9 @@ async function contestProblemRoutes (problemRoutes: FastifyTypeBox): Promise<voi
]
},
async (request, reply) => {
if (request.user == null) {
throw new UnauthorizedError('User not logged in')
}
if (request.user == null)
throw new UnauthorizedError("not logged in");

const submission = request.body
const { contestId, problemId } = request.params
const created = await createSubmission({
Expand All @@ -181,6 +181,32 @@ async function contestProblemRoutes (problemRoutes: FastifyTypeBox): Promise<voi
}
)

problemRoutes.post(
'/:problemId/rejudge',
{
schema: {
response: {
202: Type.Object({ regrades: Type.Number() }),
400: badRequestSchema,
401: unauthorizedSchema,
403: forbiddenSchema,
404: notFoundSchema
},
params: Type.Object({ problemId: Type.String() })
},
onRequest: [userAuthHook, problemRoutes.auth([
[hasDomainPrivilege(['contest.manage'])],
[hasContestPrivilege(['manage'])]
])]
},
async (request, reply) => {
// @ts-ignore
const { problemId } = request.params;
const rejudges = await rejudgeProblem({ problemId })
return await reply.status(202).send({ rejudges })
}
)

problemRoutes.get(
'/polygon-upload-session',
{
Expand Down
3 changes: 2 additions & 1 deletion packages/api-server/src/routes/v1/user.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import { NotFoundError, badRequestSchema, conflictSchema, forbiddenSchema, notFo
import { completeTeamInvitation } from '../../services/team.services.js'
import { hasDomainPrivilege } from '../../auth/scope.auth.js'
import { isTeamMember } from '../../auth/team.auth.js'
import { fetchContestProblem, fetchSubmission } from '@argoncs/common'
import { fetchSubmission } from '@argoncs/common'
import { isSuperAdmin } from '../../auth/role.auth.js'
import { userAuthHook } from '../../hooks/authentication.hooks.js'
import { submissionInfoHook } from '../../hooks/submission.hooks.js'
import gravatarUrl from 'gravatar-url'
import { querySubmissions } from '../../services/submission.services.js'
import {fetchContestProblem} from '../../services/contest.services.js'

async function userProfileRoutes (profileRoutes: FastifyTypeBox): Promise<void> {
profileRoutes.get(
Expand Down
12 changes: 10 additions & 2 deletions packages/api-server/src/services/contest.services.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { MongoServerError, contestCollection, contestProblemCollection, contestProblemListCollection, mongoClient, ranklistRedis, recalculateTeamTotalScore, teamScoreCollection } from '@argoncs/common'
import { type ConetstProblemList, type Contest, type NewContest, type TeamScore } from '@argoncs/types'
import { Problem, type ConetstProblemList, type Contest, type NewContest, type TeamScore } from '@argoncs/types'
import { ConflictError, MethodNotAllowedError, NotFoundError } from 'http-errors-enhanced'
import { nanoid } from 'nanoid'
import { CONTEST_CACHE_KEY, CONTEST_PATH_CACHE_KEY, PROBLEMLIST_CACHE_KEY, deleteCache, fetchCacheUntilLockAcquired, releaseLock, setCache } from '@argoncs/common'
Expand Down Expand Up @@ -97,6 +97,14 @@ export async function publishContest ({ contestId, published }: { contestId: str
await deleteCache({ key: `${CONTEST_CACHE_KEY}:${contest.id}` })
}

export async function fetchContestProblem ({ problemId }: { problemId: string }): Promise<Problem> {
const problem = await contestProblemCollection.findOne({ id: problemId })
if (problem == null) {
throw new NotFoundError('Problem not found')
}
return problem
}

export async function fetchContestProblemList ({ contestId }: { contestId: string }): Promise<ConetstProblemList> {
const cache = await fetchCacheUntilLockAcquired<ConetstProblemList>({ key: `${PROBLEMLIST_CACHE_KEY}:${contestId}` })
if (cache != null) {
Expand Down Expand Up @@ -161,7 +169,7 @@ export async function fetchContestRanklist ({ contestId }: { contestId: string }
if (cache == null ||
(31536000 * 1000 - (await ranklistRedis.pttl(contestId)) > 1000 &&
(await ranklistRedis.getdel(`${contestId}-obsolete`)) != null)) {
const ranklist = await teamScoreCollection.find({ contestId }).sort({ totalScore: -1, lastTime: 1 }).toArray()
const ranklist = await teamScoreCollection.find({ contestId }).sort({ totalScore: -1, penalty: 1 }).toArray()
await ranklistRedis.set(contestId, JSON.stringify(ranklist))
await ranklistRedis.expire(contestId, 31536000) // One year
return ranklist
Expand Down
39 changes: 36 additions & 3 deletions packages/api-server/src/services/submission.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,25 @@ import {
type Submission,
type Problem
} from '@argoncs/types' /*=*/
import { rabbitMQ, judgerExchange, judgerTasksKey, submissionCollection, fetchContestProblem } from '@argoncs/common'
import { rabbitMQ, judgerExchange, judgerTasksKey, submissionCollection } from '@argoncs/common'
import { languageConfigs } from '../../configs/language.configs.js'

import { nanoid } from 'nanoid'
import {fetchContestProblem} from './contest.services.js'

export async function createSubmission (
{ submission, userId, problemId, contestId, teamId = undefined }:
{ submission: NewSubmission, userId: string, problemId: string, contestId: string, teamId?: string }): Promise<{ submissionId: string }>
{
const problem = await fetchContestProblem({ problemId })
// Ensure problem exists
await fetchContestProblem({ problemId })

const submissionId = nanoid()
let pendingSubmission: Submission = {
...submission,
id: submissionId,
status: SubmissionStatus.Compiling,
problemId: problem.id,
problemId,
teamId,
userId,
contestId,
Expand Down Expand Up @@ -89,3 +91,34 @@ export async function querySubmissions ({ query }: { query: { problemId?: string

return submissions;
}

// returns the number of submissions to rejudge
export async function rejudgeProblem ({ problemId }: { problemId: string }):
Promise<number>
{
let rejudge = 0;

for await (const submission of submissionCollection.find({ problemId })) {
rejudge ++;

const { id, source, language } = submission;

await submissionCollection.updateOne(
{ id },
{
$set: { status: SubmissionStatus.Compiling },
$unset: { testcases: 1, penatly: 1, score: 1 }
});

const task: CompilingTask = {
submissionId: id,
type: JudgerTaskType.Compiling,
source,
language,
constraints: languageConfigs[language].constraints
}
rabbitMQ.publish(judgerExchange, judgerTasksKey, Buffer.from(JSON.stringify(task)))
}

return rejudge
}
2 changes: 1 addition & 1 deletion packages/api-server/src/services/team.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export async function createTeam ({ newTeam, contestId, userId }: { newTeam: New
await userCollection.updateOne({ id: userId },
{ $set: { [`teams.${contestId}`]: id } }, { session })

await teamScoreCollection.insertOne({ id, contestId, scores: {}, time: {}, lastTime: 0, totalScore: 0 })
await teamScoreCollection.insertOne({ id, contestId, scores: {}, penalty: {}, time: {}, totalScore: 0, totalPenalty: 0})
})
} finally {
await session.endSession()
Expand Down
1 change: 0 additions & 1 deletion packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ export * from './connections/sentry.connections.js'
export * from './utils/time.utils.js'

export * from './services/cache.services.js'
export * from './services/problem.services.js'
export * from './services/submission.services.js'
export * from './services/team.services.js'
11 changes: 0 additions & 11 deletions packages/common/src/services/problem.services.ts

This file was deleted.

30 changes: 9 additions & 21 deletions packages/common/src/services/team.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,22 @@ export async function recalculateTeamTotalScore ({ contestId, teamId = undefined
$match: query
},
{
$project: { v: { $objectToArray: '$scores' } }
$project: {
s: { $objectToArray: '$scores' },
p: { $objectToArray: '$penalties' },
}
},
{
$set: { totalScore: { $sum: '$v.v' } }
$set: {
totalScore: { $sum: '$s.v' },
totalPenalty: { $sum: '$p.v' }
}
},
{
$unset: "v"
$unset: ['s', 'p']
},
{
$merge: { into: "teamScores" }
}
]).toArray()

await teamScoreCollection.aggregate([
{
$match: query
},
{
$project: { v: { $objectToArray: '$time' } }
},
{
$set: { lastTime: { $max: '$v.v' } }
},
{
$unset: "v"
},
{
$merge: { into: "lastTime" }
}
]).toArray()
}
68 changes: 38 additions & 30 deletions packages/result-handler/src/services/result.services.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { contestProblemCollection, fetchContestProblem, fetchSubmission, judgerExchange, judgerTasksKey, rabbitMQ, ranklistRedis, recalculateTeamTotalScore, submissionCollection, teamScoreCollection } from '@argoncs/common'
import { contestCollection, contestProblemCollection, fetchSubmission, judgerExchange, judgerTasksKey, rabbitMQ, ranklistRedis, recalculateTeamTotalScore, submissionCollection, teamScoreCollection } from '@argoncs/common'
import { type CompilingResult, CompilingStatus, type GradingResult, GradingStatus, type GradingTask, JudgerTaskType, type Problem, SubmissionStatus, type CompilingCheckerResult } from '@argoncs/types' /*=*/
import { NotFoundError } from 'http-errors-enhanced'
import {NotFoundError} from 'http-errors-enhanced';
import path from 'path'

export async function handleCompileResult (compileResult: CompilingResult, submissionId: string): Promise<void> {
Expand All @@ -26,7 +26,9 @@ export async function handleCompileResult (compileResult: CompilingResult, submi

const { problemId } = submission

let problem: Problem = await fetchContestProblem({ problemId });
const problem = await contestProblemCollection.findOne({ problemId });
if (problem === null)
throw new NotFoundError("problem does not exist");

if (problem.testcases == null) {
await completeGrading(submissionId, 'Problem does not have testcases');
Expand Down Expand Up @@ -79,7 +81,10 @@ export async function handleCompileResult (compileResult: CompilingResult, submi
export async function completeGrading (submissionId: string, log?: string): Promise<void> {
const submission = await fetchSubmission({ submissionId })
const { problemId, contestId, teamId, status, createdAt } = submission;
const { partials } = await fetchContestProblem({ problemId })
const problem = await contestProblemCollection.findOne({ problemId });
const contest = await contestCollection.findOne({ contestId });
if (contest == null) throw new NotFoundError("contest does not exist")
if (problem == null) throw new NotFoundError("problem does not exist")

// Unexpected status
if (status !== SubmissionStatus.Compiling &&
Expand All @@ -99,32 +104,37 @@ export async function completeGrading (submissionId: string, log?: string): Prom
// Calculate score
// eslint-disable-next-line @typescript-eslint/restrict-plus-operands
const passes = testcases.reduce((t, testcase) => t + (testcase.result!).status == GradingStatus.Accepted ? 1 : 0, 1);
const score = partials ?
const score = problem.partials ?
passes / testcases.length * 100 :
(passes == testcases.length ? 100 : 0);
const was = await submissionCollection.find({ problemId, teamId, score: { $ne: 100 }, createdAt: { $lt: createdAt } }).count();
const penalty = score === 100 ?
was * 10 + (createdAt - contest.startTime) / 3600 :
0;

await submissionCollection.updateOne({ id: submissionId }, {
$set: {
score,
penalty,
status: SubmissionStatus.Graded
},
$unset: { gradedCases: '' }
})

// Skip score update if testing submission
if (submission.contestId == null || submission.teamId == null)
if (contestId == null || teamId == null)
return

const { modifiedCount } = await teamScoreCollection.updateOne({ contestId: submission.contestId, id: submission.teamId }, {
$max: { [`scores.${submission.problemId}`]: score }
const { modifiedCount } = await teamScoreCollection.updateOne({ contestId, id: teamId }, {
$max: { [`scores.${problemId}`]: score }
})

// Only update if score increased
if (modifiedCount > 0) {
await teamScoreCollection.updateOne({ contestId: submission.contestId, id: submission.teamId }, {
$max: { [`time.${submission.problemId}`]: submission.createdAt }
await teamScoreCollection.updateOne({ contestId, id: teamId }, {
$max: { [`time.${problemId}`]: createdAt },
$set: { [`penalty.${problemId}`]: penalty }
})
const { contestId, teamId } = submission
await recalculateTeamTotalScore({ contestId, teamId })
await ranklistRedis.set(`${submission.contestId}-obsolete`, 1)
}
Expand All @@ -133,27 +143,24 @@ export async function completeGrading (submissionId: string, log?: string): Prom
export async function handleGradingResult (gradingResult: GradingResult, submissionId: string, testcaseIndex: number): Promise<void> {
const submission = await fetchSubmission({ submissionId })

if (submission.status === SubmissionStatus.Grading) {
if (submission.testcases[testcaseIndex] == null) {
throw new NotFoundError('No testcase found at the given index')
}
submission.testcases[testcaseIndex].result = gradingResult
await submissionCollection.updateOne({ id: submissionId }, {
$set: {
[`testcases.${testcaseIndex}.result`]: gradingResult,
},
$inc: {
gradedCases: 1,
}
if (submission.status !== SubmissionStatus.Grading)
return

if (submission.testcases[testcaseIndex] == null)
throw new NotFoundError('No testcase found at the given index')

submission.testcases[testcaseIndex].result = gradingResult
await submissionCollection.updateOne(
{ id: submissionId },
{
$set: { [`testcases.${testcaseIndex}.result`]: gradingResult },
$inc: { gradedCases: 1 }
})

const updatedSubmission = await fetchSubmission({ submissionId })
if (updatedSubmission.status === SubmissionStatus.Grading) {
if (updatedSubmission.gradedCases === updatedSubmission.testcases.length) {
await completeGrading(submissionId)
}
}
}
const updatedSubmission = await fetchSubmission({ submissionId })
if (updatedSubmission.status === SubmissionStatus.Grading &&
updatedSubmission.gradedCases === updatedSubmission.testcases.length)
await completeGrading(submissionId)
}

export async function handleCompileCheckerResult (result: CompilingCheckerResult, problemId: string): Promise<void> {
Expand All @@ -168,3 +175,4 @@ export async function handleCompileCheckerResult (result: CompilingCheckerResult
console.log('checker compilation result recieved: ', checker);
await contestProblemCollection.updateOne({ id: problemId }, { $set: { checker }});
}

Loading

0 comments on commit dab7c7c

Please sign in to comment.