core: judge: add concurrency control to task consumer (#719)

Co-authored-by: undefined <i@undefined.moe>
This commit is contained in:
Yang Gaoandundefined authored and GitHub committed 2024-01-28 01:23:05 +08:00
1 parent 8b10f51430
commit 6b759cbe63
9 files changed
+128 -81

No files matched your search

+4 -1
View File
@@ -90,7 +90,10 @@ export async function postInit(ctx) {
await (new JudgeTask(session, JSON.parse(JSON.stringify(Object.assign(rdoc, t))))).handle().catch(logger.error); await (new JudgeTask(session, JSON.parse(JSON.stringify(Object.assign(rdoc, t))))).handle().catch(logger.error);
}; };
const parallelism = Math.max(getConfig('parallelism'), 2); const parallelism = Math.max(getConfig('parallelism'), 2);
for (let i = 1; i < parallelism; i++) TaskModel.consume({ type: 'judge' }, handle); const taskConsumer = TaskModel.consume({ type: 'judge' }, handle, true, parallelism);
ctx.on('system/setting', () => {
taskConsumer.setConcurrency(Math.max(getConfig('parallelism'), 2));
});
TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle); TaskModel.consume({ type: 'judge', priority: { $gt: -50 } }, handle);
TaskModel.consume({ type: 'generate' }, handle); TaskModel.consume({ type: 'generate' }, handle);
} }
+54 -56
View File
@@ -21,14 +21,13 @@ import record from '../model/record';
import * as setting from '../model/setting'; import * as setting from '../model/setting';
import storage from '../model/storage'; import storage from '../model/storage';
import * as system from '../model/system'; import * as system from '../model/system';
import task from '../model/task'; import task, { Consumer } from '../model/task';
import user from '../model/user'; import user from '../model/user';
import * as bus from '../service/bus'; import * as bus from '../service/bus';
import { updateJudge } from '../service/monitor'; import { updateJudge } from '../service/monitor';
import { import {
ConnectionHandler, Handler, post, subscribe, Types, ConnectionHandler, Handler, post, subscribe, Types,
} from '../service/server'; } from '../service/server';
import { sleep } from '../utils';
const logger = new Logger('judge'); const logger = new Logger('judge');
@@ -45,6 +44,8 @@ function parseCaseResult(body: TestCase): Required<TestCase> {
function processPayload(body: Partial<JudgeResultBody>) { function processPayload(body: Partial<JudgeResultBody>) {
const $set: Partial<RecordDoc> = {}; const $set: Partial<RecordDoc> = {};
const $push: any = {}; const $push: any = {};
const $unset: any = {};
const $inc: any = {};
if (body.cases?.length) { if (body.cases?.length) {
const c = body.cases.map(parseCaseResult); const c = body.cases.map(parseCaseResult);
$push.testCases = { $each: c }; $push.testCases = { $each: c };
@@ -64,13 +65,18 @@ function processPayload(body: Partial<JudgeResultBody>) {
if (Number.isFinite(body.memory)) $set.memory = body.memory; if (Number.isFinite(body.memory)) $set.memory = body.memory;
if (body.progress !== undefined) $set.progress = body.progress; if (body.progress !== undefined) $set.progress = body.progress;
if (body.subtasks) $set.subtasks = body.subtasks; if (body.subtasks) $set.subtasks = body.subtasks;
return { $set, $push }; if (body.addProgress) $inc.progress = body.addProgress;
return {
$set, $push, $unset, $inc,
};
} }
export async function next(body: Partial<JudgeResultBody>) { export async function next(body: Partial<JudgeResultBody>) {
body.rid = new ObjectId(body.rid); body.rid = new ObjectId(body.rid);
const { $set, $push } = processPayload(body); const {
const rdoc = await record.update(body.domainId, body.rid, $set, $push, {}, body.addProgress ? { progress: body.addProgress } : {}); $set, $push, $unset, $inc,
} = processPayload(body);
const rdoc = await record.update(body.domainId, body.rid, $set, $push, $unset, $inc);
bus.broadcast('record/change', rdoc, $set, $push, body); bus.broadcast('record/change', rdoc, $set, $push, body);
return rdoc; return rdoc;
} }
@@ -210,25 +216,25 @@ export class JudgeFileUpdateHandler extends Handler {
} }
} }
class JudgeConnectionHandler extends ConnectionHandler { export class JudgeConnectionHandler extends ConnectionHandler {
category = '#judge'; category = '#judge';
processing: Task[] = [];
started = false;
closed = false;
query: any = { type: { $in: ['judge', 'generate'] } }; query: any = { type: { $in: ['judge', 'generate'] } };
rdocs: Record<string, RecordDoc> = {};
concurrency = 1; concurrency = 1;
queue: PQueue = new PQueue({ concurrency: 1 }); consumer: Consumer = null;
callback: (res?: any) => void = null; startTimeout: NodeJS.Timeout = null;
tasks: Record<string, {
resolve: (_: any) => void,
queue: PQueue,
domainId: string,
t: Task,
}> = {};
async prepare() { async prepare() {
logger.info('Judge daemon connected from ', this.request.ip); logger.info('Judge daemon connected from ', this.request.ip);
this.sendLanguageConfig(); this.sendLanguageConfig();
// TODO deprecated, just for compatibility // TODO deprecated, just for compatibility
setTimeout(() => { this.startTimeout = setTimeout(() => {
if (this.started || this.closed) return; this.consumer ||= task.consume(this.query, this.newTask.bind(this), true, this.concurrency);
this.newTask().catch((e) => logger.error(e));
this.started = true;
}, 15000); }, 15000);
} }
@@ -237,31 +243,23 @@ class JudgeConnectionHandler extends ConnectionHandler {
this.send({ language: setting.langs }); this.send({ language: setting.langs });
} }
async newTask() { async newTask(t: Task) {
while (!this.closed) {
/* eslint-disable no-await-in-loop */
if (this.processing.length >= this.concurrency) {
await Promise.race([
sleep(500),
new Promise((resolve) => {
this.callback = resolve;
}),
]);
continue;
}
const t = await task.getFirst(this.query);
if (!t) {
await sleep(500);
continue;
}
const rdoc = await record.get(t.domainId, t.rid); const rdoc = await record.get(t.domainId, t.rid);
if (!rdoc) continue; if (!rdoc) return;
const rid = rdoc._id.toHexString();
let resolve: (_: any) => void;
const p = new Promise((r) => { resolve = r; });
this.tasks[rid] = {
queue: new PQueue({ concurrency: 1 }),
domainId: rdoc.domainId,
resolve,
t,
};
this.send({ task: { ...rdoc, ...t } }); this.send({ task: { ...rdoc, ...t } });
this.rdocs[rdoc._id.toHexString()] = rdoc; this.tasks[rid].queue.add(() => next({ status: STATUS.STATUS_FETCHED, domainId: rdoc.domainId, rid: rdoc._id }));
this.processing.push(t); await p;
await next({ status: STATUS.STATUS_FETCHED, domainId: rdoc.domainId, rid: rdoc._id }); delete this.tasks[rid];
}
} }
async message(msg) { async message(msg) {
@@ -271,47 +269,47 @@ class JudgeConnectionHandler extends ConnectionHandler {
logger[method]('%o', omit(msg, keys)); logger[method]('%o', omit(msg, keys));
} }
if (['next', 'end'].includes(msg.key)) { if (['next', 'end'].includes(msg.key)) {
const rdoc = this.rdocs[msg.rid]; const t = this.tasks[msg.key];
if (!rdoc) return; if (!t) return;
msg.domainId = rdoc.domainId; msg.domainId = t.domainId;
if (msg.key === 'next') this.queue.add(() => next(msg)); if (msg.key === 'next') t.queue.add(() => next(msg));
if (msg.key === 'end') { if (msg.key === 'end') {
this.processing = this.processing.filter((t) => t.rid.toHexString() !== msg.rid); if (!msg.nop) t.queue.add(() => end({ judger: this.user._id, ...msg }));
delete this.rdocs[msg.rid]; t.resolve(null);
this.callback?.();
if (!msg.nop) {
this.queue.add(() => end({ judger: this.user._id, ...msg }).catch((e) => logger.error(e)));
}
} }
} else if (msg.key === 'status') { } else if (msg.key === 'status') {
await updateJudge(msg.info); await updateJudge(msg.info);
} else if (msg.key === 'prio' && typeof msg.prio === 'number') { } else if (msg.key === 'prio' && typeof msg.prio === 'number') {
// TODO deprecated, use config instead // TODO deprecated, use config instead
this.query.priority = { $gt: msg.prio }; this.query.priority = { $gt: msg.prio };
this.consumer?.setQuery(this.query);
} else if (msg.key === 'config') { } else if (msg.key === 'config') {
if (Number.isSafeInteger(msg.prio)) { if (Number.isSafeInteger(msg.prio)) {
this.query.priority = { $gt: msg.prio }; this.query.priority = { $gt: msg.prio };
this.consumer?.setQuery(this.query);
} }
if (Number.isSafeInteger(msg.concurrency) && msg.concurrency > 0) { if (Number.isSafeInteger(msg.concurrency) && msg.concurrency > 0) {
this.concurrency = msg.concurrency; this.concurrency = msg.concurrency;
this.callback?.(); this.consumer?.setConcurrency(msg.concurrency);
} }
if (msg.lang instanceof Array && msg.lang.every((i) => typeof i === 'string')) { if (msg.lang instanceof Array && msg.lang.every((i) => typeof i === 'string')) {
this.query.lang = { $in: msg.lang }; this.query.lang = { $in: msg.lang };
this.consumer?.setQuery(this.query);
} }
} else if (msg.key === 'start') { } else if (msg.key === 'start') {
if (this.started) return; clearTimeout(this.startTimeout);
this.newTask().catch((e) => logger.error(e)); this.consumer ||= task.consume(this.query, this.newTask.bind(this), true, this.concurrency);
this.started = true;
} }
} }
async cleanup() { async cleanup() {
this.closed = true; clearTimeout(this.startTimeout);
this.consumer?.destroy();
logger.info('Judge daemon disconnected from ', this.request.ip); logger.info('Judge daemon disconnected from ', this.request.ip);
await Promise.all(this.processing.map(async (t) => { await Promise.all(Object.values(this.tasks).map(async ({ t }) => {
await record.reset(t.domainId, t.rid, false); const rdoc = await record.reset(t.domainId, t.rid, false);
return await task.add(t); bus.broadcast('record/change', rdoc);
return task.add(t);
})); }));
} }
} }
+16 -3
View File
@@ -253,6 +253,8 @@ class RecordMainConnectionHandler extends ConnectionHandler {
pretest = false; pretest = false;
tdoc: Tdoc; tdoc: Tdoc;
applyProjection = false; applyProjection = false;
queue: Map<string, () => Promise<any>> = new Map();
throttleQueueClear: () => void;
@param('tid', Types.ObjectId, true) @param('tid', Types.ObjectId, true)
@param('pid', Types.ProblemId, true) @param('pid', Types.ProblemId, true)
@@ -301,6 +303,7 @@ class RecordMainConnectionHandler extends ConnectionHandler {
this.checkPriv(PRIV.PRIV_MANAGE_ALL_DOMAIN); this.checkPriv(PRIV.PRIV_MANAGE_ALL_DOMAIN);
this.allDomain = true; this.allDomain = true;
} }
this.throttleQueueClear = throttle(this.queueClear, 100, { trailing: true });
} }
async message(msg: { rids: string[] }) { async message(msg: { rids: string[] }) {
@@ -338,15 +341,25 @@ class RecordMainConnectionHandler extends ConnectionHandler {
if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM)) pdoc = null; if (!this.user.hasPerm(PERM.PERM_VIEW_PROBLEM)) pdoc = null;
} }
if (this.applyProjection && typeof rdoc.input !== 'string') rdoc = contest.applyProjection(tdoc, rdoc, this.user); if (this.applyProjection && typeof rdoc.input !== 'string') rdoc = contest.applyProjection(tdoc, rdoc, this.user);
if (this.pretest) this.send({ rdoc: omit(rdoc, ['code', 'input']) }); if (this.pretest) this.queueSend(rdoc._id.toHexString(), async () => ({ rdoc: omit(rdoc, ['code', 'input']) }));
else { else {
this.send({ this.queueSend(rdoc._id.toHexString(), async () => ({
html: await this.renderHTML('record_main_tr.html', { html: await this.renderHTML('record_main_tr.html', {
rdoc, udoc, pdoc, tdoc, allDomain: this.allDomain, rdoc, udoc, pdoc, tdoc, allDomain: this.allDomain,
}), }),
}); }));
} }
} }
queueSend(rid: string, fn: () => Promise<any>) {
this.queue.set(rid, fn);
this.throttleQueueClear();
}
async queueClear() {
await Promise.all([...this.queue.values()].map(async (fn) => this.send(await fn())));
this.queue.clear();
}
} }
class RecordDetailConnectionHandler extends ConnectionHandler { class RecordDetailConnectionHandler extends ConnectionHandler {
+4 -4
View File
@@ -31,10 +31,10 @@ class Consumer {
consuming: boolean; consuming: boolean;
running?: any; running?: any;
constructor(public filter: any, public func: Function, public destoryOnError = true) { constructor(public filter: any, public func: Function, public destroyOnError = true) {
this.consuming = true; this.consuming = true;
this.consume(); this.consume();
bus.on('app/exit', this.destory); bus.on('app/exit', this.destroy);
} }
async consume() { async consume() {
@@ -51,12 +51,12 @@ class Consumer {
} else await sleep(1000); } else await sleep(1000);
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
if (this.destoryOnError) this.destory(); if (this.destroyOnError) this.destroy();
} }
} }
} }
async destory() { async destroy() {
this.consuming = false; this.consuming = false;
} }
} }
+46 -13
View File
@@ -25,37 +25,70 @@ async function getFirst(query: Filter<Task>) {
return null; return null;
} }
class Consumer { export class Consumer {
consuming: boolean; consuming: boolean;
processing: Set<Task> = new Set();
running?: any; running?: any;
notify: (res?: any) => void;
constructor(public filter: any, public func: Function, public destoryOnError = true) { constructor(public filter: any, public func: (t: Task) => Promise<void>, public destroyOnError = true, private concurrency = 1) {
this.consuming = true; this.consuming = true;
this.consume(); this.consume();
bus.on('app/exit', this.destory); bus.on('app/exit', this.destroy);
} }
async consume() { async consume() {
while (this.consuming) { while (this.consuming) {
try { try {
if (this.processing.size >= this.concurrency) {
// eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => {
this.notify = resolve;
});
continue;
}
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const res = await getFirst(this.filter); const res = await getFirst(this.filter);
if (res) { if (!res) {
this.running = res; let timeout: NodeJS.Timeout = null;
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await this.func(res); await new Promise((resolve) => {
this.running = null; timeout = setTimeout(resolve, 1000 / (this.concurrency - this.processing.size));
// eslint-disable-next-line no-await-in-loop this.notify = resolve;
} else await sleep(1000); });
clearTimeout(timeout);
continue;
}
this.processing.add(res);
this.func(res)
.catch((err) => {
logger.error(err);
if (this.destroyOnError) this.destroy();
})
.finally(() => {
this.processing.delete(res);
this.notify?.();
});
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
if (this.destoryOnError) this.destory(); if (this.destroyOnError) this.destroy();
} }
} }
} }
async destory() { async destroy() {
this.consuming = false; this.consuming = false;
this.notify?.();
}
setConcurrency(concurrency: number) {
this.concurrency = concurrency;
this.notify?.();
}
setQuery(query: string) {
this.filter = query;
this.notify?.();
} }
} }
@@ -95,8 +128,8 @@ class TaskModel {
static getFirst = getFirst; static getFirst = getFirst;
static consume(query: any, cb: Function, destoryOnError = true) { static consume(query: any, cb: (t: Task) => Promise<void>, destroyOnError = true, concurrency = 1) {
return new Consumer(query, cb, destoryOnError); return new Consumer(query, cb, destroyOnError, concurrency);
} }
} }
+1 -1
View File
@@ -417,7 +417,6 @@ export function Connection(
h.compression = new Shorty(); h.compression = new Shorty();
conn.send('shorty'); conn.send('shorty');
} }
conn.pause();
if (h._prepare) await h._prepare(args); if (h._prepare) await h._prepare(args);
if (h.prepare) await h.prepare(args); if (h.prepare) await h.prepare(args);
// eslint-disable-next-line @typescript-eslint/no-shadow // eslint-disable-next-line @typescript-eslint/no-shadow
@@ -606,6 +605,7 @@ ${ctx.response.status} ${endTime - startTime}ms ${ctx.response.length}`);
socket.close(1003, 'Websocket Error'); socket.close(1003, 'Websocket Error');
} catch (e) { } } catch (e) { }
}); });
socket.pause();
const ctx: any = app.createContext(request, {} as any); const ctx: any = app.createContext(request, {} as any);
await domainLayer(ctx, () => baseLayer(ctx, () => layers[1](ctx, () => userLayer(ctx, () => { })))); await domainLayer(ctx, () => baseLayer(ctx, () => layers[1](ctx, () => userLayer(ctx, () => { }))));
for (const manager of router.wsStack) { for (const manager of router.wsStack) {
@@ -256,7 +256,7 @@ export default class Editor extends DOMAttachedObject {
if (hasFocus) this.focus(); if (hasFocus) this.focus();
} }
destory() { destroy() {
this.detach(); this.detach();
if (this.vditor?.destroy) this.vditor.destroy(); if (this.vditor?.destroy) this.vditor.destroy();
else if (this.editor?.dispose) this.editor.dispose(); else if (this.editor?.dispose) this.editor.dispose();
@@ -34,7 +34,7 @@ async function startEdit(filename, value, fileCategory = 'file') {
}); });
const action = await promise; const action = await promise;
value = (editor.value() as string).replace(/\r\n/g, '\n'); value = (editor.value() as string).replace(/\r\n/g, '\n');
editor.destory(); editor.destroy();
if (action !== 'ok') return null; if (action !== 'ok') return null;
return value; return value;
} }
@@ -143,7 +143,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor e
} }
componentWillUnmount() { componentWillUnmount() {
if (this.vjEditor) this.vjEditor.destory(); if (this.vjEditor) this.vjEditor.destroy();
if (this.model) this.model.dispose(); if (this.model) this.model.dispose();
if (this.editor) this.editor.dispose(); if (this.editor) this.editor.dispose();
this.disposable.map((i) => i.dispose()); this.disposable.map((i) => i.dispose());