Skip to content
Open
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
4 changes: 3 additions & 1 deletion apps/frontend/src/components/solution/SolutionView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface ISolutionViewProps {
export function useSolutionView(props: ISolutionViewProps) {
const app = useAppState()
const settings = props.contestId ? useContestSettings() : useProblemSettings()
const pull = ref(true)
const showDetails = computed(() => {
if (props.admin) return true
if (solution.state.value?.userId === app.userId) {
Expand Down Expand Up @@ -67,7 +68,7 @@ export function useSolutionView(props: ISolutionViewProps) {
const url = props.contestId
? `contest/${props.contestId}/solution/${props.solutionId}/rejudge`
: `problem/${props.problemId}/solution/${props.solutionId}/rejudge`
await http.post(url)
await http.post(url, { json: { pull: pull.value } })
solution.execute()
autoRefresh.resume()
})
Expand All @@ -88,6 +89,7 @@ export function useSolutionView(props: ISolutionViewProps) {
solution,
showDetails,
showData,
pull,
viewFile,
downloadEndpoint,
submit,
Expand Down
13 changes: 13 additions & 0 deletions apps/frontend/src/components/solution/SolutionView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,17 @@
@click="submit.execute()"
:loading="submit.isLoading.value"
/>
<VBtn
v-if="admin"
variant="outlined"
:prepend-icon="pull ? 'mdi-source-pull' : 'mdi-pin'"
:text="pull ? t('solution.rejudge-mode-pull') : t('solution.rejudge-mode-pin')"
@click="pull = !pull"
/>
<VBtn
v-if="admin"
:text="t('action.rejudge')"
:append-icon="pull ? 'mdi-source-pull' : 'mdi-pin'"
@click="rejudge.execute()"
:loading="rejudge.isLoading.value"
/>
Expand Down Expand Up @@ -143,6 +151,7 @@ const {
solution,
showDetails,
showData,
pull,
viewFile,
downloadEndpoint,
submit,
Expand All @@ -157,9 +166,13 @@ en:
info: Info
details: Details
data: Data
rejudge-mode-pin: Fixed rejudge
rejudge-mode-pull: Pull rejudge
zh-Hans:
solution:
info: 信息
details: 细节
data: 数据
rejudge-mode-pin: 固定重测
rejudge-mode-pull: 拉取重测
</i18n>
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,19 @@
/>
</VCol>
</VRow>
<VBtn
color="red"
variant="outlined"
:prepend-icon="pull ? 'mdi-source-pull' : 'mdi-pin'"
@click="pull = !pull"
>
{{ pull ? t('rejudge-mode-pull') : t('rejudge-mode-pin') }}
</VBtn>
<VBtn
color="red"
variant="elevated"
type="submit"
:append-icon="pull ? 'mdi-source-pull' : 'mdi-pin'"
:loading="rejudgeAllTask.isLoading.value"
>
{{ t('action.rejudge-all') }}
Expand Down Expand Up @@ -193,11 +202,14 @@ const rejudgeOptions = reactive({
submittedAtL: undefined as number | undefined,
submittedAtR: undefined as number | undefined
})
const pull = ref(true)
const rejudgeAllTask = useAsyncTask(async (ev: SubmitEventPromise) => {
const result = await ev
if (!result.valid) return noMessage()
const { modifiedCount } = await http
.post(`contest/${props.contestId}/admin/rejudge-all`, { json: rejudgeOptions })
.post(`contest/${props.contestId}/admin/rejudge-all`, {
json: { ...rejudgeOptions, pull: pull.value }
})
.json<{ modifiedCount: number }>()
return withMessage(t('msg.rejudge-all-success', { count: modifiedCount }))
})
Expand Down Expand Up @@ -258,6 +270,8 @@ const ifNotUndefined = <T,>(cond: unknown, value: T) => (cond !== undefined ? va
en:
ranklist-state: Ranklist State is {state}
reset-runner: Switch Reset Runner
rejudge-mode-pin: Fixed rejudge
rejudge-mode-pull: Pull rejudge
min-score: Min Score
max-score: Max Score
submitted-after: Submitted After
Expand All @@ -270,6 +284,8 @@ en:
zh-Hans:
ranklist-state: '排行榜状态: {state}'
reset-runner: 切换重置运行器
rejudge-mode-pin: 固定重测
rejudge-mode-pull: 拉取重测
min-score: 最小分数
max-score: 最大分数
submitted-after: 提交时间晚于
Expand Down
101 changes: 70 additions & 31 deletions apps/server/src/routes/contest/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { kContestContext } from './inject.js'

export const contestAdminRoutes = defineRoutes(async (s) => {
const { contests, solutions } = s.db
const { contests, problems, solutions } = s.db

s.addHook('onRequest', async (req) => {
ensureCapability(
Expand Down Expand Up @@ -106,6 +106,7 @@ export const contestAdminRoutes = defineRoutes(async (s) => {
description: 'Rejudge all solutions',
body: T.Object({
problemId: T.Optional(T.UUID()),
pull: T.Optional(T.Boolean()),
state: T.Optional(T.Integer({ minimum: 1, maximum: 4 })),
status: T.Optional(T.String()),
runnerId: T.Optional(T.String()),
Expand All @@ -121,36 +122,74 @@ export const contestAdminRoutes = defineRoutes(async (s) => {
}
}
},
async (req) => {
const { modifiedCount } = await solutions.updateMany(
{
contestId: req.inject(kContestContext)._contestId,
problemId: req.body.problemId ? new UUID(req.body.problemId) : undefined,
state: req.body.state || { $ne: SolutionState.CREATED },
status: req.body.status,
runnerId:
typeof req.body.runnerId === 'string'
? req.body.runnerId
? new UUID(req.body.runnerId)
: { $exists: false }
: undefined,
score: generateRangeQuery(req.body.scoreL, req.body.scoreR),
submittedAt: generateRangeQuery(req.body.submittedAtL, req.body.submittedAtR)
},
[
{
$set: {
state: SolutionState.PENDING,
score: 0,
status: '',
metrics: {},
message: ''
}
},
{ $unset: ['taskId', 'runnerId'] }
],
{ ignoreUndefined: true }
)
async (req, rep) => {
const baseQuery = {
contestId: req.inject(kContestContext)._contestId,
problemId: req.body.problemId ? new UUID(req.body.problemId) : undefined,
state: req.body.state || { $ne: SolutionState.CREATED },
status: req.body.status,
runnerId:
typeof req.body.runnerId === 'string'
? req.body.runnerId
? new UUID(req.body.runnerId)
: { $exists: false }
: undefined,
score: generateRangeQuery(req.body.scoreL, req.body.scoreR),
submittedAt: generateRangeQuery(req.body.submittedAtL, req.body.submittedAtR)
}

if (!req.body.pull) {
const { modifiedCount } = await solutions.updateMany(
baseQuery,
[
{
$set: {
state: SolutionState.PENDING,
score: 0,
status: '',
metrics: {},
message: ''
}
},
{ $unset: ['taskId', 'runnerId'] }
],
{ ignoreUndefined: true }
)
return { modifiedCount }
}

const matchedProblemIds = await solutions.distinct('problemId', baseQuery, {
ignoreUndefined: true
})
if (!matchedProblemIds.length) return { modifiedCount: 0 }

const matchedProblems = await problems
.find({ _id: { $in: matchedProblemIds } }, { projection: { currentDataHash: 1, data: 1 } })
.toArray()
let modifiedCount = 0
for (const problem of matchedProblems) {
const currentData = problem.data.find(({ hash }) => hash === problem.currentDataHash)
if (!currentData) return rep.preconditionFailed('Current data not found')
Comment on lines +169 to +172

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

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

In pull-mode, this endpoint updates solutions problem-by-problem in a loop and can return preconditionFailed('Current data not found') mid-way. That leaves any earlier updateMany calls already applied, resulting in a partial rejudge that’s hard to reason about/rollback. Consider validating all matched problems up-front (existence + currentDataHash present in data) before issuing any updates, or performing the updates in a single bulk/transactional operation so failures don’t leave partial state.

Suggested change
let modifiedCount = 0
for (const problem of matchedProblems) {
const currentData = problem.data.find(({ hash }) => hash === problem.currentDataHash)
if (!currentData) return rep.preconditionFailed('Current data not found')
// Validate that all matched problemIds have corresponding problems and current data
if (matchedProblems.length !== matchedProblemIds.length) {
return rep.preconditionFailed('Current data not found')
}
for (const problem of matchedProblems) {
const currentData = problem.data.find(({ hash }) => hash === problem.currentDataHash)
if (!currentData) {
return rep.preconditionFailed('Current data not found')
}
}
let modifiedCount = 0
for (const problem of matchedProblems) {
const currentData = problem.data.find(({ hash }) => hash === problem.currentDataHash)

Copilot uses AI. Check for mistakes.
const result = await solutions.updateMany(
{ ...baseQuery, problemId: problem._id },
[
{
$set: {
label: currentData.config.label,
problemDataHash: problem.currentDataHash,
state: SolutionState.PENDING,
score: 0,
status: '',
metrics: {},
message: ''
}
},
{ $unset: ['taskId', 'runnerId'] }
],
{ ignoreUndefined: true }
)
modifiedCount += result.modifiedCount
}
return { modifiedCount }
}
)
Expand Down
90 changes: 64 additions & 26 deletions apps/server/src/routes/contest/solution/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,36 +94,74 @@ const solutionScopedRoutes = defineRoutes(async (s) => {
}
)

s.post('/rejudge', {}, async (req, rep) => {
const ctx = req.inject(kContestContext)
s.post(
'/rejudge',
{
schema: {
body: T.Object({
pull: T.Optional(T.Boolean())
})
}
},
async (req, rep) => {
const ctx = req.inject(kContestContext)

const solutionId = loadUUID(req.params, 'solutionId', s.httpErrors.badRequest())
const admin = hasCapability(ctx._contestCapability, CONTEST_CAPS.CAP_ADMIN)
if (!admin) return rep.forbidden()
const solutionId = loadUUID(req.params, 'solutionId', s.httpErrors.badRequest())
const admin = hasCapability(ctx._contestCapability, CONTEST_CAPS.CAP_ADMIN)
if (!admin) return rep.forbidden()

const { modifiedCount } = await solutions.updateOne(
{
_id: solutionId,
contestId: ctx._contestId,
state: { $ne: SolutionState.CREATED }
},
[
const { pull } = req.body
let label: string | undefined
let problemDataHash: string | undefined
if (pull) {
const solution = await solutions.findOne(
{
_id: solutionId,
contestId: ctx._contestId,
state: { $ne: SolutionState.CREATED }
},
{ projection: { problemId: 1 } }
)
if (!solution) return rep.notFound()

const problem = await s.db.problems.findOne(
{ _id: solution.problemId },
{ projection: { currentDataHash: 1, data: 1 } }
)
if (!problem) return rep.notFound()
const currentData = problem.data.find(({ hash }) => hash === problem.currentDataHash)
if (!currentData) return rep.preconditionFailed('Current data not found')

label = currentData.config.label
problemDataHash = problem.currentDataHash
}

const { modifiedCount } = await solutions.updateOne(
{
$set: {
state: SolutionState.PENDING,
score: 0,
status: '',
metrics: {},
message: ''
}
_id: solutionId,
contestId: ctx._contestId,
state: { $ne: SolutionState.CREATED }
},
{ $unset: ['taskId', 'runnerId'] }
],
{ ignoreUndefined: true }
)
if (modifiedCount === 0) return rep.notFound()
return {}
})
[
{
$set: {
label,
problemDataHash,
state: SolutionState.PENDING,
score: 0,
status: '',
metrics: {},
message: ''
}
},
{ $unset: ['taskId', 'runnerId'] }
],
{ ignoreUndefined: true }
)
if (modifiedCount === 0) return rep.notFound()
return {}
}
)

s.get(
'/',
Expand Down
21 changes: 20 additions & 1 deletion apps/server/src/routes/problem/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { getFileUrl, defineRoutes, paramSchemaMerger } from '../common/index.js'
import { kProblemContext } from './inject.js'

const dataScopedRoutes = defineRoutes(async (s) => {
const { problems } = s.db
const { instances, problems, solutions } = s.db

s.addHook('onRoute', paramSchemaMerger(T.Object({ hash: T.Hash() })))
s.register(getFileUrl, {
Expand All @@ -36,6 +36,25 @@ const dataScopedRoutes = defineRoutes(async (s) => {

ensureCapability(ctx._problemCapability, PROBLEM_CAPS.CAP_CONTENT, s.httpErrors.forbidden())
const hash = (req.params as { hash: string }).hash
const [referencedSolution, referencedInstance] = await Promise.all([
solutions.findOne(
{
problemId: ctx._problemId,
problemDataHash: hash
},
{ projection: { _id: 1 } }
),
instances.findOne(
{
problemId: ctx._problemId,
problemDataHash: hash
},
{ projection: { _id: 1 } }
)
])
if (referencedSolution || referencedInstance) {
return rep.conflict('Problem data is still referenced')
}
const { modifiedCount } = await problems.updateOne(
{ _id: ctx._problemId, currentDataHash: { $ne: hash } },
{ $pull: { data: { hash } } }
Expand Down
Loading
Loading