Merge branch 'master' into contest-problemlist

This commit is contained in:
undefined committed 2022-10-18 16:56:18 +08:00
commit 58c0cade0e
28 files changed
+280 -163

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@hydrooj/hydrojudge", "name": "@hydrooj/hydrojudge",
"bin": "bin/hydrojudge.js", "bin": "bin/hydrojudge.js",
"version": "3.1.0", "version": "3.1.1",
"main": "package.json", "main": "package.json",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",
+6 -3
View File
@@ -2,9 +2,9 @@
/* eslint-disable no-await-in-loop */ /* eslint-disable no-await-in-loop */
import path from 'path'; import path from 'path';
import fs from 'fs-extra'; import fs from 'fs-extra';
import type { JudgeResultBody } from 'hydrooj';
import { import {
SettingModel, StorageModel, SystemModel, TaskModel, JudgeResultBody, RecordModel, SettingModel,
StorageModel, SystemModel, TaskModel,
} from 'hydrooj'; } from 'hydrooj';
import { end, next } from 'hydrooj/src/handler/judge'; import { end, next } from 'hydrooj/src/handler/judge';
import { processTestdata } from '../cases'; import { processTestdata } from '../cases';
@@ -75,7 +75,10 @@ const session = {
export async function postInit() { export async function postInit() {
if (SystemModel.get('hydrojudge.disable')) return; if (SystemModel.get('hydrojudge.disable')) return;
const handle = (t) => (new JudgeTask(session, t)).handle().catch(logger.error); const handle = async (t) => {
const rdoc = await RecordModel.get(t.domainId, t.rid);
(new JudgeTask(session, Object.assign(rdoc, t))).handle().catch(logger.error);
};
TaskModel.consume({ type: 'judge' }, handle); TaskModel.consume({ type: 'judge' }, handle);
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle); TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle);
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "hydrooj", "name": "hydrooj",
"version": "4.0.18", "version": "4.1.1",
"bin": "bin/hydrooj.js", "bin": "bin/hydrooj.js",
"main": "src/plugin-api", "main": "src/plugin-api",
"module": "src/plugin-api", "module": "src/plugin-api",
+10 -12
View File
@@ -17,9 +17,9 @@ import message from '../model/message';
import * as oplog from '../model/oplog'; import * as oplog from '../model/oplog';
import problem from '../model/problem'; import problem from '../model/problem';
import record from '../model/record'; import record from '../model/record';
import ScheduleModel from '../model/schedule';
import storage from '../model/storage'; import storage from '../model/storage';
import * as system from '../model/system'; import * as system from '../model/system';
import TaskModel from '../model/task';
import user from '../model/user'; import user from '../model/user';
import { import {
Handler, param, post, Types, Handler, param, post, Types,
@@ -52,7 +52,7 @@ registerResolver(
'Get a contest by ID', 'Get a contest by ID',
); );
TaskModel.Worker.addHandler('contest', async (doc) => { ScheduleModel.Worker.addHandler('contest', async (doc) => {
const tdoc = await contest.get(doc.domainId, doc.tid); const tdoc = await contest.get(doc.domainId, doc.tid);
if (!tdoc) return; if (!tdoc) return;
const tasks = []; const tasks = [];
@@ -199,7 +199,7 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST); if (!this.user.own(this.tdoc)) this.checkPerm(PERM.PERM_EDIT_CONTEST);
await contest.del(domainId, tid); await contest.del(domainId, tid);
await record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' }); await record.updateMulti(domainId, { domainId, contest: tid }, undefined, undefined, { contest: '' });
await TaskModel.deleteMany({ await ScheduleModel.deleteMany({
type: 'schedule', subType: 'contest', domainId, tid, type: 'schedule', subType: 'contest', domainId, tid,
}); });
this.response.redirect = this.url('contest_main'); this.response.redirect = this.url('contest_main');
@@ -346,20 +346,18 @@ export class ContestEditHandler extends Handler {
const task = { const task = {
type: 'schedule', subType: 'contest', domainId, tid, type: 'schedule', subType: 'contest', domainId, tid,
}; };
await TaskModel.deleteMany(task); await ScheduleModel.deleteMany(task);
const operation = [];
if (Date.now() <= endAt.getTime() && autoHide) { if (Date.now() <= endAt.getTime() && autoHide) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await Promise.all(pids.map((pid) => problem.edit(domainId, pid, { hidden: true }))); await Promise.all(pids.map((pid) => problem.edit(domainId, pid, { hidden: true })));
await TaskModel.add({ operation.push('unhide');
...task,
operation: ['unhide'],
executeAfter: endAt,
});
} }
if (lock && lockAt <= endAt) { if (lock && lockAt <= endAt) operation.push('unlock');
await TaskModel.add({ if (operation.length) {
await ScheduleModel.add({
...task, ...task,
operation: ['unlock'], operation,
executeAfter: endAt, executeAfter: endAt,
}); });
} }
+4 -5
View File
@@ -103,9 +103,7 @@ export async function postJudge(rdoc: RecordDoc) {
contest: { $ne: new ObjectID('0'.repeat(24)) }, contest: { $ne: new ObjectID('0'.repeat(24)) },
}).project({ _id: 1, contest: 1 }).toArray(); }).project({ _id: 1, contest: 1 }).toArray();
const priority = await record.submissionPriority(rdoc.uid, -5000 - rdocs.length * 5 - 50); const priority = await record.submissionPriority(rdoc.uid, -5000 - rdocs.length * 5 - 50);
await Promise.all(rdocs.map( await record.judge(rdoc.domainId, rdocs.map((r) => r._id), priority, {}, { hackRejudge: input });
(r) => record.judge(rdoc.domainId, r._id, priority, r.contest ? { detail: false } : {}, { hackRejudge: input }),
));
} catch (e) { } catch (e) {
next({ next({
rid: rdoc._id, domainId: rdoc.domainId, key: 'next', message: { message: 'Unable to apply hack: {0}', params: [e.message] }, rid: rdoc._id, domainId: rdoc.domainId, key: 'next', message: { message: 'Unable to apply hack: {0}', params: [e.message] },
@@ -202,10 +200,11 @@ class JudgeConnectionHandler extends ConnectionHandler {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
if (!t) await sleep(500); if (!t) await sleep(500);
} }
this.send({ task: t }); let rdoc = await record.get(t.domainId, t.rid);
this.send({ task: { ...rdoc, ...t } });
this.processing = t; this.processing = t;
const $set = { status: builtin.STATUS.STATUS_FETCHED }; const $set = { status: builtin.STATUS.STATUS_FETCHED };
const rdoc = await record.update(t.domainId, t.rid, $set, {}); rdoc = await record.update(t.domainId, t.rid, $set, {});
bus.broadcast('record/change', rdoc, $set, {}); bus.broadcast('record/change', rdoc, $set, {});
} }
+4 -3
View File
@@ -286,10 +286,11 @@ class SystemUserPrivHandler extends SystemHandler {
} }
@requireSudo @requireSudo
@param('uid', Types.Int, true) @param('uid', Types.Int)
@param('priv', Types.UnsignedInt) @param('priv', Types.UnsignedInt)
async post(domainId: string, uid: number, priv: number) { @param('system', Types.Boolean, true)
if (typeof uid === 'number') { async post(domainId: string, uid: number, priv: number, editSystem: boolean) {
if (!editSystem) {
const udoc = await user.getById(domainId, uid); const udoc = await user.getById(domainId, uid);
if (!udoc) throw new UserNotFoundError(uid); if (!udoc) throw new UserNotFoundError(uid);
if (udoc.priv === -1 || priv === -1 || priv === allPriv) throw new ForbiddenError('you can not edit user as SU in web.'); if (udoc.priv === -1 || priv === -1 || priv === allPriv) throw new ForbiddenError('you can not edit user as SU in web.');
+5 -5
View File
@@ -443,17 +443,17 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
@param('pid', Types.UnsignedInt) @param('pid', Types.UnsignedInt)
async postRejudge(domainId: string, pid: number) { async postRejudge(domainId: string, pid: number) {
this.checkPerm(PERM.PERM_REJUDGE_PROBLEM); this.checkPerm(PERM.PERM_REJUDGE_PROBLEM);
// TODO maybe async?
const rdocs = await record.getMulti(domainId, { const rdocs = await record.getMulti(domainId, {
pid, pid,
contest: { $ne: new ObjectID('0'.repeat(24)) }, contest: { $ne: new ObjectID('0'.repeat(24)) },
'files.hack': { $exists: false }, 'files.hack': { $exists: false },
}).project({ _id: 1, contest: 1 }).toArray(); }).project({ _id: 1, contest: 1 }).toArray();
const priority = await record.submissionPriority(this.user._id, -10000 - rdocs.length * 5 - 50); const priority = await record.submissionPriority(this.user._id, -10000 - rdocs.length * 5 - 50);
await Promise.all(rdocs.map( await record.reset(domainId, rdocs.map((rdoc) => rdoc._id), true);
(doc) => record.reset(domainId, doc._id, true) await Promise.all([
.then(() => record.judge(domainId, doc._id, priority, doc.contest ? { detail: false } : {})), record.judge(domainId, rdocs.filter((i) => i.contest).map((i) => i._id), priority, { detail: false }),
)); record.judge(domainId, rdocs.filter((i) => !i.contest).map((i) => i._id), priority, {}),
]);
this.back(); this.back();
} }
+1 -4
View File
@@ -100,10 +100,7 @@ class RecordListHandler extends ContestDetailBaseHandler {
filterStatus: status, filterStatus: status,
}; };
if (this.user.hasPriv(PRIV.PRIV_VIEW_JUDGE_STATISTICS) && !full) { if (this.user.hasPriv(PRIV.PRIV_VIEW_JUDGE_STATISTICS) && !full) {
this.response.body.statistics = { this.response.body.statistics = await record.stat(all ? undefined : domainId);
...await record.stat(all ? undefined : domainId),
delay: await TaskModel.getDelay({ type: 'judge' }),
};
} }
} }
} }
+2 -2
View File
@@ -16,9 +16,9 @@ import domain from '../model/domain';
import oauth from '../model/oauth'; import oauth from '../model/oauth';
import * as oplog from '../model/oplog'; import * as oplog from '../model/oplog';
import problem, { ProblemDoc } from '../model/problem'; import problem, { ProblemDoc } from '../model/problem';
import ScheduleModel from '../model/schedule';
import SolutionModel from '../model/solution'; import SolutionModel from '../model/solution';
import * as system from '../model/system'; import * as system from '../model/system';
import task from '../model/task';
import token from '../model/token'; import token from '../model/token';
import user from '../model/user'; import user from '../model/user';
import { import {
@@ -376,7 +376,7 @@ class UserDetailHandler extends Handler {
class UserDeleteHandler extends Handler { class UserDeleteHandler extends Handler {
async post({ password }) { async post({ password }) {
this.user.checkPassword(password); this.user.checkPassword(password);
const tid = await task.add({ const tid = await ScheduleModel.add({
executeAfter: moment().add(7, 'days').toDate(), executeAfter: moment().add(7, 'days').toDate(),
type: 'script', type: 'script',
id: 'deleteUser', id: 'deleteUser',
+10 -1
View File
@@ -551,11 +551,18 @@ export interface Task {
_id: ObjectID; _id: ObjectID;
type: string; type: string;
subType?: string; subType?: string;
executeAfter: Date;
priority: number; priority: number;
[key: string]: any; [key: string]: any;
} }
export interface Schedule {
_id: ObjectID;
type: string;
subType?: string;
executeAfter: Date;
[key: string]: any;
}
export interface FileNode { export interface FileNode {
/** File Path In S3 */ /** File Path In S3 */
_id: string; _id: string;
@@ -618,6 +625,7 @@ declare module './service/db' {
'oplog': OplogDoc; 'oplog': OplogDoc;
'event': EventDoc; 'event': EventDoc;
'opcount': OpCountDoc; 'opcount': OpCountDoc;
'schedule': Schedule;
} }
} }
@@ -637,6 +645,7 @@ export interface Model {
solution: typeof import('./model/solution').default, solution: typeof import('./model/solution').default,
system: typeof import('./model/system'), system: typeof import('./model/system'),
task: typeof import('./model/task').default, task: typeof import('./model/task').default,
schedule: typeof import('./model/schedule').default;
oplog: typeof import('./model/oplog'), oplog: typeof import('./model/oplog'),
token: typeof import('./model/token').default, token: typeof import('./model/token').default,
training: typeof import('./model/training'), training: typeof import('./model/training'),
+3 -3
View File
@@ -10,7 +10,7 @@ import { buildProjection } from '../utils';
import * as contest from './contest'; import * as contest from './contest';
import * as document from './document'; import * as document from './document';
import problem from './problem'; import problem from './problem';
import TaskModel from './task'; import ScheduleModel from './schedule';
import * as training from './training'; import * as training from './training';
export interface DiscussionDoc extends Document { } export interface DiscussionDoc extends Document { }
@@ -294,8 +294,8 @@ async function updateSort() {
export function apply(ctx: Context) { export function apply(ctx: Context) {
ctx.using(['worker'], async ({ worker }) => { ctx.using(['worker'], async ({ worker }) => {
worker.addHandler('discussion.sort', updateSort); worker.addHandler('discussion.sort', updateSort);
if (!await TaskModel.count({ type: 'schedule', subType: 'discussion.sort' })) { if (!await ScheduleModel.count({ type: 'schedule', subType: 'discussion.sort' })) {
await TaskModel.add({ await ScheduleModel.add({
type: 'schedule', type: 'schedule',
subType: 'discussion.sort', subType: 'discussion.sort',
executeAfter: moment().minute(0).second(0).millisecond(0).toDate(), executeAfter: moment().minute(0).second(0).millisecond(0).toDate(),
+13 -14
View File
@@ -1,5 +1,5 @@
/* eslint-disable object-curly-newline */ /* eslint-disable object-curly-newline */
import { omit, sum } from 'lodash'; import { sum } from 'lodash';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import { import {
FilterQuery, MatchKeysAndValues, FilterQuery, MatchKeysAndValues,
@@ -9,7 +9,7 @@ import {
import { Context } from '../context'; import { Context } from '../context';
import { ProblemNotFoundError } from '../error'; import { ProblemNotFoundError } from '../error';
import { import {
FileInfo, JudgeMeta, JudgeRequest, ProblemConfigFile, RecordDoc, FileInfo, JudgeMeta, ProblemConfigFile, RecordDoc,
} from '../interface'; } from '../interface';
import db from '../service/db'; import db from '../service/db';
import { MaybeArray, NumberKeys } from '../typeutils'; import { MaybeArray, NumberKeys } from '../typeutils';
@@ -63,8 +63,10 @@ export default class RecordModel {
}; };
} }
static async judge(domainId: string, rid: ObjectID, priority = 0, config: ProblemConfigFile = {}, meta: Partial<JudgeMeta> = {}) { static async judge(domainId: string, rids: MaybeArray<ObjectID>, priority = 0, config: ProblemConfigFile = {}, meta: Partial<JudgeMeta> = {}) {
const rdoc = await RecordModel.get(domainId, rid); rids = rids instanceof Array ? rids : [rids];
if (!rids.length) return null;
const rdoc = await RecordModel.get(domainId, rids[0]);
if (!rdoc) return null; if (!rdoc) return null;
let data: FileInfo[] = []; let data: FileInfo[] = [];
let source = `${domainId}/${rdoc.pid}`; let source = `${domainId}/${rdoc.pid}`;
@@ -81,23 +83,20 @@ export default class RecordModel {
data = pdoc.data; data = pdoc.data;
if (typeof pdoc.config === 'string') throw new Error(pdoc.config); if (typeof pdoc.config === 'string') throw new Error(pdoc.config);
if (pdoc.config.type === 'remote_judge') { if (pdoc.config.type === 'remote_judge') {
if (rdoc.contest?.toHexString() !== '0'.repeat(24)) { await task.deleteMany({ rid: { $in: rids } });
return await task.add({ return await task.addMany(rids.map((rid) => ({
...omit(rdoc, ['_id']), ...(pdoc.config as any),
priority, priority,
type: 'remotejudge', type: 'remotejudge',
subType: pdoc.config.subType,
target: pdoc.config.target,
rid, rid,
domainId, domainId,
config, config,
data, data,
}); } as any)));
} }
} }
} await task.deleteMany({ rid: { $in: rids } });
return await task.add({ return await task.addMany(rids.map((rid) => ({
...omit(rdoc, ['_id']),
priority, priority,
type: 'judge', type: 'judge',
rid, rid,
@@ -106,7 +105,7 @@ export default class RecordModel {
data, data,
source, source,
meta, meta,
} as unknown as JudgeRequest); } as any)));
} }
static async add( static async add(
+162
View File
@@ -0,0 +1,162 @@
import moment from 'moment-timezone';
import { FilterQuery, ObjectID } from 'mongodb';
import { sleep } from '@hydrooj/utils/lib/utils';
import { Context, Service } from '../context';
import { Schedule } from '../interface';
import { Logger } from '../logger';
import * as bus from '../service/bus';
import db from '../service/db';
const logger = new Logger('model/schedule');
const coll = db.collection('schedule');
async function getFirst(query: FilterQuery<Schedule>) {
if (process.env.CI) return null;
const q = { ...query };
q.executeAfter = q.executeAfter || { $lt: new Date() };
const res = await coll.findOneAndDelete(q);
if (res.value) {
logger.debug('%o', res.value);
if (res.value.interval) {
const executeAfter = moment(res.value.executeAfter).add(...res.value.interval).toDate();
await coll.insertOne({ ...res.value, executeAfter });
}
return res.value;
}
return null;
}
class Consumer {
consuming: boolean;
running?: any;
constructor(public filter: any, public func: Function, public destoryOnError = true) {
this.consuming = true;
this.consume();
bus.on('app/exit', this.destory);
}
async consume() {
while (this.consuming) {
try {
// eslint-disable-next-line no-await-in-loop
const res = await getFirst(this.filter);
if (res) {
this.running = res;
// eslint-disable-next-line no-await-in-loop
await this.func(res);
this.running = null;
// eslint-disable-next-line no-await-in-loop
} else await sleep(1000);
} catch (err) {
logger.error(err);
if (this.destoryOnError) this.destory();
}
}
}
async destory() {
this.consuming = false;
}
}
class WorkerService extends Service {
private handlers: Record<string, Function> = {};
public consumer = new Consumer(
{ type: 'schedule', subType: { $in: Object.keys(this.handlers) } },
async (doc) => {
try {
logger.debug('Worker task: %o', doc);
const start = Date.now();
await Promise.race([
this.handlers[doc.subType](doc),
sleep(300000),
]);
const spent = Date.now() - start;
if (spent > 500) logger.warn('Slow worker task (%d ms): %s', spent, doc);
} catch (e) {
logger.error('Worker task fail: ', e);
logger.error('%o', doc);
}
},
false,
);
public addHandler(type: string, handler: Function) {
this.handlers[type] = handler;
this.consumer.filter = { type: 'schedule', subType: { $in: Object.keys(this.handlers) } };
this.caller?.on('dispose', () => {
delete this.handlers[type];
});
}
}
const Worker = new WorkerService(app, 'worker', false);
class ScheduleModel {
static coll = coll;
static async add(task: Partial<Schedule> & { type: string }) {
const res = await coll.insertOne({
...task,
executeAfter: task.executeAfter || new Date(),
_id: new ObjectID(),
});
return res.insertedId;
}
static get(_id: ObjectID) {
return coll.findOne({ _id });
}
static count(query: FilterQuery<Schedule>) {
return coll.find(query).count();
}
static del(_id: ObjectID) {
return coll.deleteOne({ _id });
}
static deleteMany(query: FilterQuery<Schedule>) {
return coll.deleteMany(query);
}
static getFirst = getFirst;
static Worker = Worker;
}
declare module '../context' {
interface Context {
worker: WorkerService;
}
}
export async function apply(ctx: Context) {
Context.service('worker', WorkerService);
ctx.worker = Worker;
Worker.addHandler('task.daily', async () => {
await global.Hydro.model.record.coll.deleteMany({ contest: new ObjectID('000000000000000000000000') });
await global.Hydro.script.rp?.run({}, new Logger('task/rp').debug);
await global.Hydro.script.problemStat?.run({}, new Logger('task/problem').debug);
if (global.Hydro.model.system.get('server.checkUpdate') && !(new Date().getDay() % 3)) {
await global.Hydro.script.checkUpdate?.run({}, new Logger('task/checkUpdate').debug);
}
await ctx.parallel('task/daily');
});
ctx.on('domain/delete', (domainId) => coll.deleteMany({ domainId }));
if (process.env.NODE_APP_INSTANCE !== '0') return;
if (!await ScheduleModel.count({ type: 'schedule', subType: 'task.daily' })) {
await ScheduleModel.add({
type: 'schedule',
subType: 'task.daily',
executeAfter: moment().add(1, 'day').hour(3).minute(0).second(0).millisecond(0).toDate(),
interval: [1, 'day'],
});
}
await db.ensureIndexes(coll, { name: 'schedule', key: { type: 1, subType: 1, executeAfter: -1 } });
}
export default ScheduleModel;
global.Hydro.model.schedule = ScheduleModel;
+4 -4
View File
@@ -7,8 +7,8 @@ import type { Readable } from 'stream';
import * as bus from '../service/bus'; import * as bus from '../service/bus';
import db from '../service/db'; import db from '../service/db';
import storage from '../service/storage'; import storage from '../service/storage';
import ScheduleModel from './schedule';
import * as system from './system'; import * as system from './system';
import TaskModel from './task';
export class StorageModel { export class StorageModel {
static coll = db.collection('storage'); static coll = db.collection('storage');
@@ -135,15 +135,15 @@ async function cleanFiles() {
res = await StorageModel.coll.findOneAndDelete({ autoDelete: { $lte: new Date() } }); res = await StorageModel.coll.findOneAndDelete({ autoDelete: { $lte: new Date() } });
} }
} }
TaskModel.Worker.addHandler('storage.prune', cleanFiles); ScheduleModel.Worker.addHandler('storage.prune', cleanFiles);
bus.on('ready', async () => { bus.on('ready', async () => {
if (process.env.NODE_APP_INSTANCE !== '0') return; if (process.env.NODE_APP_INSTANCE !== '0') return;
await db.ensureIndexes( await db.ensureIndexes(
StorageModel.coll, StorageModel.coll,
{ key: { path: 1, autoDelete: 1 }, sparse: true, name: 'autoDelete' }, { key: { path: 1, autoDelete: 1 }, sparse: true, name: 'autoDelete' },
); );
if (!await TaskModel.count({ type: 'schedule', subType: 'storage.prune' })) { if (!await ScheduleModel.count({ type: 'schedule', subType: 'storage.prune' })) {
await TaskModel.add({ await ScheduleModel.add({
type: 'schedule', type: 'schedule',
subType: 'storage.prune', subType: 'storage.prune',
executeAfter: moment().startOf('hour').toDate(), executeAfter: moment().startOf('hour').toDate(),
+9 -78
View File
@@ -1,9 +1,8 @@
import { hostname } from 'os'; import { hostname } from 'os';
import moment from 'moment-timezone';
import { FilterQuery, ObjectID } from 'mongodb'; import { FilterQuery, ObjectID } from 'mongodb';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { sleep } from '@hydrooj/utils/lib/utils'; import { sleep } from '@hydrooj/utils/lib/utils';
import { Context, Service } from '../context'; import { Context } from '../context';
import { EventDoc, Task } from '../interface'; import { EventDoc, Task } from '../interface';
import { Logger } from '../logger'; import { Logger } from '../logger';
import * as bus from '../service/bus'; import * as bus from '../service/bus';
@@ -16,14 +15,9 @@ const collEvent = db.collection('event');
async function getFirst(query: FilterQuery<Task>) { async function getFirst(query: FilterQuery<Task>) {
if (process.env.CI) return null; if (process.env.CI) return null;
const q = { ...query }; const q = { ...query };
q.executeAfter = q.executeAfter || { $lt: new Date() };
const res = await coll.findOneAndDelete(q, { sort: { priority: -1 } }); const res = await coll.findOneAndDelete(q, { sort: { priority: -1 } });
if (res.value) { if (res.value) {
logger.debug('%o', res.value); logger.debug('%o', res.value);
if (res.value.interval) {
const executeAfter = moment(res.value.executeAfter).add(...res.value.interval).toDate();
await coll.insertOne({ ...res.value, executeAfter });
}
return res.value; return res.value;
} }
return null; return null;
@@ -63,51 +57,24 @@ class Consumer {
} }
} }
class WorkerService extends Service {
private handlers: Record<string, Function> = {};
public consumer = new Consumer(
{ type: 'schedule', subType: { $in: Object.keys(this.handlers) } },
async (doc) => {
try {
logger.debug('Worker task: %o', doc);
const start = Date.now();
await Promise.race([
this.handlers[doc.subType](doc),
sleep(300000),
]);
const spent = Date.now() - start;
if (spent > 500) logger.warn('Slow worker task (%d ms): %o', spent, doc);
} catch (e) {
logger.error('Worker task fail: ', e);
logger.error('%o', doc);
}
},
false,
);
public addHandler(type: string, handler: Function) {
this.handlers[type] = handler;
this.consumer.filter = { type: 'schedule', subType: { $in: Object.keys(this.handlers) } };
this.caller?.on('dispose', () => {
delete this.handlers[type];
});
}
}
const Worker = new WorkerService(app, 'worker', false);
class TaskModel { class TaskModel {
static coll = coll;
static async add(task: Partial<Task> & { type: string }) { static async add(task: Partial<Task> & { type: string }) {
const t: Task = { const t: Task = {
...task, ...task,
priority: task.priority ?? 0, priority: task.priority ?? 0,
executeAfter: task.executeAfter || new Date(),
_id: new ObjectID(), _id: new ObjectID(),
}; };
const res = await coll.insertOne(t); const res = await coll.insertOne(t);
return res.insertedId; return res.insertedId;
} }
static async addMany(tasks: Task[]) {
const res = await coll.insertMany(tasks);
return res.insertedIds;
}
static get(_id: ObjectID) { static get(_id: ObjectID) {
return coll.findOne({ _id }); return coll.findOne({ _id });
} }
@@ -126,43 +93,14 @@ class TaskModel {
static getFirst = getFirst; static getFirst = getFirst;
static async getDelay(query?: FilterQuery<Task>): Promise<[number, Date]> {
const now = new Date();
const [res] = await coll.find(query).sort({ executeAfter: 1 }).limit(1).toArray();
if (res) return [Math.max(0, now.getTime() - res.executeAfter.getTime()), res.executeAfter];
return [0, now];
}
static async consume(query: any, cb: Function, destoryOnError = true) { static async consume(query: any, cb: Function, destoryOnError = true) {
return new Consumer(query, cb, destoryOnError); return new Consumer(query, cb, destoryOnError);
} }
static Consumer = Consumer;
static WorkerService = WorkerService;
static Worker = Worker;
} }
const id = process.env.exec_mode === 'cluster_mode' ? hostname() : nanoid(); const id = process.env.exec_mode === 'cluster_mode' ? hostname() : nanoid();
declare module '../context' {
interface Context {
worker: WorkerService;
}
}
export async function apply(ctx: Context) { export async function apply(ctx: Context) {
Context.service('worker', WorkerService);
ctx.worker = Worker;
Worker.addHandler('task.daily', async () => {
await global.Hydro.model.record.coll.deleteMany({ contest: new ObjectID('000000000000000000000000') });
await global.Hydro.script.rp?.run({}, new Logger('task/rp').debug);
await global.Hydro.script.problemStat?.run({}, new Logger('task/problem').debug);
if (global.Hydro.model.system.get('server.checkUpdate') && !(new Date().getDay() % 3)) {
await global.Hydro.script.checkUpdate?.run({}, new Logger('task/checkUpdate').debug);
}
await ctx.parallel('task/daily');
});
ctx.on('domain/delete', (domainId) => coll.deleteMany({ domainId })); ctx.on('domain/delete', (domainId) => coll.deleteMany({ domainId }));
ctx.on('bus/broadcast', (event, payload) => { ctx.on('bus/broadcast', (event, payload) => {
collEvent.insertOne({ collEvent.insertOne({
@@ -174,14 +112,6 @@ export async function apply(ctx: Context) {
}); });
if (process.env.NODE_APP_INSTANCE !== '0') return; if (process.env.NODE_APP_INSTANCE !== '0') return;
if (!await TaskModel.count({ type: 'schedule', subType: 'task.daily' })) {
await TaskModel.add({
type: 'schedule',
subType: 'task.daily',
executeAfter: moment().add(1, 'day').hour(3).minute(0).second(0).millisecond(0).toDate(),
interval: [1, 'day'],
});
}
await collEvent.createIndex({ expire: 1 }, { expireAfterSeconds: 0 }); await collEvent.createIndex({ expire: 1 }, { expireAfterSeconds: 0 });
const stream = collEvent.watch(); const stream = collEvent.watch();
const handleEvent = async (doc: EventDoc) => { const handleEvent = async (doc: EventDoc) => {
@@ -208,6 +138,7 @@ export async function apply(ctx: Context) {
await (res.value ? handleEvent(res.value) : sleep(500)); await (res.value ? handleEvent(res.value) : sleep(500));
} }
}); });
await db.ensureIndexes(coll, { name: 'task', key: { type: 1, subType: 1, priority: -1 } });
} }
export default TaskModel; export default TaskModel;
+8 -1
View File
@@ -15,6 +15,7 @@ import domain from './model/domain';
import MessageModel from './model/message'; import MessageModel from './model/message';
import problem from './model/problem'; import problem from './model/problem';
import RecordModel from './model/record'; import RecordModel from './model/record';
import ScheduleModel from './model/schedule';
import * as system from './model/system'; import * as system from './model/system';
import TaskModel from './model/task'; import TaskModel from './model/task';
import user from './model/user'; import user from './model/user';
@@ -395,7 +396,7 @@ const scripts: UpgradeScript[] = [
const _FRESH_INSTALL_IGNORE = 1; const _FRESH_INSTALL_IGNORE = 1;
const tasks = await db.collection('task').find({ type: 'schedule', subType: 'contest.problemHide' }).toArray(); const tasks = await db.collection('task').find({ type: 'schedule', subType: 'contest.problemHide' }).toArray();
for (const task of tasks) { for (const task of tasks) {
await TaskModel.add({ ...task, subType: 'contest', operation: ['unhide'] }); await ScheduleModel.add({ ...task, subType: 'contest', operation: ['unhide'] });
} }
await TaskModel.deleteMany({ type: 'schedule', subType: 'contest.problemHide' }); await TaskModel.deleteMany({ type: 'schedule', subType: 'contest.problemHide' });
return true; return true;
@@ -563,6 +564,12 @@ const scripts: UpgradeScript[] = [
await db.collection('cache' as any).deleteMany({}); await db.collection('cache' as any).deleteMany({});
return true; return true;
}, },
async function _69_70() {
const tasks = await TaskModel.coll.find({ type: 'schedule', subType: 'contest' }).toArray();
if (tasks.length) await ScheduleModel.coll.insertMany(tasks as any);
await TaskModel.coll.deleteMany({});
return true;
},
]; ];
export default scripts; export default scripts;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/migrate", "name": "@hydrooj/migrate",
"version": "0.2.0", "version": "0.2.1",
"main": "index.ts", "main": "index.ts",
"repository": "git@github.com:hydro-dev/Hydro.git", "repository": "git@github.com:hydro-dev/Hydro.git",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
+2 -2
View File
@@ -42,7 +42,7 @@ const langMap = {
16: 'js', 16: 'js',
17: 'go', 17: 'go',
}; };
const nameMap = { const nameMap: Record<string, string> = {
'sample.in': 'sample0.in', 'sample.in': 'sample0.in',
'sample.out': 'sample0.out', 'sample.out': 'sample0.out',
'test.in': 'test0.in', 'test.in': 'test0.in',
@@ -291,7 +291,7 @@ export async function run({
report({ message: `Syncing testdata for ${file.name}` }); report({ message: `Syncing testdata for ${file.name}` });
for (const data of datas) { for (const data of datas) {
if (data.isDirectory()) continue; if (data.isDirectory()) continue;
const filename = nameMap[data.name] || data; const filename = nameMap[data.name] || data.name;
await ProblemModel.addTestdata(domainId, pdoc.docId, filename, `${dataDir}/${file.name}/${data.name}`); await ProblemModel.addTestdata(domainId, pdoc.docId, filename, `${dataDir}/${file.name}/${data.name}`);
} }
await ProblemModel.addTestdata(domainId, pdoc.docId, 'config.yaml', Buffer.from(pdoc.config as string)); await ProblemModel.addTestdata(domainId, pdoc.docId, 'config.yaml', Buffer.from(pdoc.config as string));
+3 -1
View File
@@ -1,3 +1,4 @@
import { hostname } from 'os';
import { AggregatorRegistry, metric } from 'prom-client'; import { AggregatorRegistry, metric } from 'prom-client';
import { Context, Handler, SystemModel } from 'hydrooj'; import { Context, Handler, SystemModel } from 'hydrooj';
import { createRegistry } from './metrics'; import { createRegistry } from './metrics';
@@ -39,10 +40,11 @@ class MetricsHandler extends Handler {
} }
export function apply(ctx: Context) { export function apply(ctx: Context) {
if (process.env.HYDRO_CLI) return;
const registry = createRegistry(ctx); const registry = createRegistry(ctx);
ctx.on('metrics', (id, metrics) => { instances[id] = metrics; }); ctx.on('metrics', (id, metrics) => { instances[id] = metrics; });
ctx.setInterval(async () => { ctx.setInterval(async () => {
ctx.broadcast('metrics', process.env.NODE_APP_INSTANCE!, await registry.getMetricsAsJSON()); ctx.broadcast('metrics', `${hostname()}/${process.env.NODE_APP_INSTANCE}`, await registry.getMetricsAsJSON());
}, 5000 * (+SystemModel.get('prom-client.collect_rate') || 1)); }, 5000 * (+SystemModel.get('prom-client.collect_rate') || 1));
ctx.Route('metrics', '/metrics', MetricsHandler); ctx.Route('metrics', '/metrics', MetricsHandler);
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/prom-client", "name": "@hydrooj/prom-client",
"version": "0.1.0", "version": "0.1.1",
"main": "index.ts", "main": "index.ts",
"dependencies": { "dependencies": {
"prom-client": "^14.1.0" "prom-client": "^14.1.0"
@@ -26,7 +26,7 @@ export default connect((state: any) => ({
}); });
}, },
}))(class MonacoEditor extends React.PureComponent<ScratchpadOptions> { }))(class MonacoEditor extends React.PureComponent<ScratchpadOptions> {
disposable = []; disposable: monaco.IDisposable[] = [];
__prevent_trigger_change_event = false; __prevent_trigger_change_event = false;
model: monaco.editor.ITextModel; model: monaco.editor.ITextModel;
editor: monaco.editor.IStandaloneCodeEditor; editor: monaco.editor.IStandaloneCodeEditor;
@@ -78,7 +78,7 @@ export default connect((state: any) => ({
[ [
{ {
range: model.getFullModelRange(), range: model.getFullModelRange(),
text: value, text: value!,
}, },
], ],
() => null, () => null,
+9 -10
View File
@@ -4,11 +4,8 @@ import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl'; import tpl from 'vj/utils/tpl';
import createHint from './hint'; import createHint from './hint';
let inserted = false; let hintInserted = false;
$(tpl`<div style="display: none" class="dialog__body--user-select">
export default async function selectUser(hint?: string) {
if (!inserted) {
const el = $(tpl`<div style="display: none" class="dialog__body--user-select">
<div class="row"><div class="columns"> <div class="row"><div class="columns">
<h1 name="select_user_hint">${i18n('Select User')}</h1> <h1 name="select_user_hint">${i18n('Select User')}</h1>
</div></div> </div></div>
@@ -19,11 +16,7 @@ export default async function selectUser(hint?: string) {
</label> </label>
</div> </div>
</div> </div>
</div>`); </div>`).appendTo(document.body);
el.appendTo(document.body);
if (hint) createHint(hint, $('[name="select_user_hint"]'));
inserted = true;
}
const userSelector = UserSelectAutoComplete.getOrConstruct($('.dialog__body--user-select [name="user"]')) as any; const userSelector = UserSelectAutoComplete.getOrConstruct($('.dialog__body--user-select [name="user"]')) as any;
const userSelectDialog = new ActionDialog({ const userSelectDialog = new ActionDialog({
$body: $('.dialog__body--user-select > div'), $body: $('.dialog__body--user-select > div'),
@@ -35,6 +28,12 @@ export default async function selectUser(hint?: string) {
return true; return true;
}, },
}); });
export default async function selectUser(hint?: string) {
if (hint && !hintInserted) {
createHint(hint, $('[name="select_user_hint"]'));
hintInserted = true;
}
userSelector.clear(); userSelector.clear();
const action = await userSelectDialog.open(); const action = await userSelectDialog.open();
if (action !== 'ok') return null; if (action !== 'ok') return null;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/ui-default", "name": "@hydrooj/ui-default",
"version": "4.41.14", "version": "4.41.17",
"author": "undefined <i@undefined.moe>", "author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"main": "index.ts", "main": "index.ts",
@@ -20,10 +20,11 @@ const page = new NamedPage('manage_user_priv', () => {
async function changeUserPriv(uid, priv) { async function changeUserPriv(uid, priv) {
try { try {
const res = await request.post('', uid !== 'default' ? { const res = await request.post('', {
uid, uid: uid !== 'default' ? uid : 0,
priv, priv,
} : { priv }); system: uid === 'default',
});
if (res.url && res.url !== window.location.href) window.location.href = res.url; if (res.url && res.url !== window.location.href) window.location.href = res.url;
else { else {
Notification.success(i18n('Priv has been updated to {0}.', priv)); Notification.success(i18n('Priv has been updated to {0}.', priv));
@@ -54,8 +55,8 @@ const page = new NamedPage('manage_user_priv', () => {
handleOpenUserPrivDialog(user.udoc); handleOpenUserPrivDialog(user.udoc);
} }
$('[name="select_user"]').on('click', () => handleClickSelectUser()); $(document).on('click', '[name="select_user"]', () => handleClickSelectUser());
$('[name="set_priv"]').on('click', (ev) => handleOpenUserPrivDialog(ev)); $(document).on('click', '[name="set_priv"]', (ev) => handleOpenUserPrivDialog(ev));
}); });
export default page; export default page;
@@ -163,18 +163,26 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
$('.loader-container').hide(); $('.loader-container').hide();
} }
let progress = false;
async function enterScratchpadMode() { async function enterScratchpadMode() {
if (progress) return;
progress = true;
await extender.extend(); await extender.extend();
await loadReact(); await loadReact();
renderReact(); renderReact();
await scratchpadFadeIn(); await scratchpadFadeIn();
progress = false;
} }
async function leaveScratchpadMode() { async function leaveScratchpadMode() {
if (progress) return;
progress = true;
await scratchpadFadeOut(); await scratchpadFadeOut();
$('.problem-content-container').append($('.problem-content')); $('.problem-content-container').append($('.problem-content'));
await extender.collapse(); await extender.collapse();
unmountReact(); unmountReact();
progress = false;
} }
async function loadObjective() { async function loadObjective() {
@@ -124,7 +124,6 @@
</div> </div>
<div class="section__body"> <div class="section__body">
<dl class="large horizontal"> <dl class="large horizontal">
<dt>{{ _('Delay')}}</dt><dd>{{ statistics.delay[0]|int }}</dd>
<dt>{{ _('5 Min')}}</dt><dd>{{ statistics['d5min'] }}</dd> <dt>{{ _('5 Min')}}</dt><dd>{{ statistics['d5min'] }}</dd>
<dt>{{ _('1 Hour')}}</dt><dd>{{ statistics['d1h'] }}</dd> <dt>{{ _('1 Hour')}}</dt><dd>{{ statistics['d1h'] }}</dd>
<dt>{{ _('Day') }}</dt><dd>{{ statistics['day'] }}</dd> <dt>{{ _('Day') }}</dt><dd>{{ statistics['day'] }}</dd>
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@hydrooj/vjudge", "name": "@hydrooj/vjudge",
"version": "1.6.1", "version": "1.6.2",
"description": "Submit problems to remote oj", "description": "Submit problems to remote oj",
"main": "package.json", "main": "package.json",
"repository": "https://github.com/hydro-dev/Hydro.git", "repository": "https://github.com/hydro-dev/Hydro.git",
+3 -1
View File
@@ -2,7 +2,7 @@
import os from 'os'; import os from 'os';
import { import {
Context, db, DomainModel, JudgeHandler, Logger, Context, db, DomainModel, JudgeHandler, Logger,
ProblemModel, sleep, STATUS, TaskModel, Time, ProblemModel, RecordModel, sleep, STATUS, TaskModel, Time,
} from 'hydrooj'; } from 'hydrooj';
import { BasicProvider, IBasicProvider, RemoteAccount } from './interface'; import { BasicProvider, IBasicProvider, RemoteAccount } from './interface';
import { getDifficulty } from './providers/codeforces'; import { getDifficulty } from './providers/codeforces';
@@ -35,6 +35,8 @@ class Service {
} }
async judge(task) { async judge(task) {
const rdoc = await RecordModel.get(task.domainId, task.rid);
task = Object.assign(rdoc, task);
const next = (payload) => JudgeHandler.next({ ...payload, rid: task.rid }); const next = (payload) => JudgeHandler.next({ ...payload, rid: task.rid });
const end = (payload) => JudgeHandler.end({ ...payload, rid: task.rid }); const end = (payload) => JudgeHandler.end({ ...payload, rid: task.rid });
await next({ status: STATUS.STATUS_FETCHED }); await next({ status: STATUS.STATUS_FETCHED });