mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-15 19:57:07 +08:00
fix: 修复任务生命周期与调度就绪,优化执行和构建开销 (#3069)
* fix: harden task lifecycle and scheduler readiness * fix: confine log writes to the configured log directory * fix: verify complete build artifacts and untracked inputs * fix: reconcile scheduler state and make stop win startup races * fix: isolate cron generations and serialize scheduler recovery
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const { Sequelize, DataTypes } = require('sequelize');
|
||||
const load = require('../helpers/load-security-module.cjs');
|
||||
async function fixture(t, onKill = async () => {}) {
|
||||
const db = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: ':memory:',
|
||||
logging: false,
|
||||
});
|
||||
t.after(() => db.close());
|
||||
const crons = db.define('Cron', {
|
||||
command: DataTypes.STRING,
|
||||
status: DataTypes.INTEGER,
|
||||
pid: DataTypes.INTEGER,
|
||||
log_path: DataTypes.STRING,
|
||||
queued_token: DataTypes.STRING,
|
||||
last_execution_time: DataTypes.INTEGER,
|
||||
last_running_time: DataTypes.INTEGER,
|
||||
});
|
||||
const instances = db.define('Instance', {
|
||||
cron_id: DataTypes.INTEGER,
|
||||
pid: DataTypes.INTEGER,
|
||||
status: DataTypes.INTEGER,
|
||||
exit_code: DataTypes.INTEGER,
|
||||
finished_at: DataTypes.INTEGER,
|
||||
});
|
||||
await db.sync();
|
||||
await crons.bulkCreate([
|
||||
{ id: 1, command: 'task one.js', status: 0, pid: 101, log_path: 'one.log' },
|
||||
{ id: 2, command: 'task two.js', status: 0, pid: 201, log_path: 'two.log' },
|
||||
]);
|
||||
await instances.bulkCreate([
|
||||
{ id: 1, cron_id: 1, pid: 101, status: 0 },
|
||||
{ id: 2, cron_id: 1, pid: 102, status: 0 },
|
||||
{ id: 3, cron_id: 1, pid: 99, status: 1, exit_code: 0 },
|
||||
{ id: 4, cron_id: 1, pid: 98, status: 3, exit_code: 7 },
|
||||
{ id: 5, cron_id: 2, pid: 201, status: 0 },
|
||||
]);
|
||||
let service;
|
||||
const killed = [];
|
||||
const CronService = load(path.resolve('back/services/cron.ts'), {
|
||||
'../config': {},
|
||||
'../data/cron': {
|
||||
CrontabModel: crons,
|
||||
CrontabStatus: { queued: 3, running: 0, idle: 1 },
|
||||
},
|
||||
'../data/runningInstance': {
|
||||
RunningInstanceModel: instances,
|
||||
InstanceStatus: { running: 0, finished: 1, stopped: 2, error: 3 },
|
||||
},
|
||||
'../config/util': {
|
||||
killTask: async (pid, wait) => {
|
||||
assert.equal(wait, true);
|
||||
killed.push(pid);
|
||||
await onKill({ pid, service, instances, crons });
|
||||
},
|
||||
killAllTasks: async () => {
|
||||
throw Error('command-wide scans must not run');
|
||||
},
|
||||
},
|
||||
'../config/const': {},
|
||||
'../schedule/client': {},
|
||||
'../shared/pLimit': {},
|
||||
'../shared/utils': {},
|
||||
'../shared/i18n': { t: (s) => s },
|
||||
'../shared/logReader': {},
|
||||
'../shared/logStreamManager': {},
|
||||
'../shared/childProcess': require('../../back/shared/childProcess'),
|
||||
}).default;
|
||||
service = new CronService({ info() {}, error() {} });
|
||||
service.getDb = ({ id }) => crons.findByPk(id);
|
||||
return { service, instances, crons, killed };
|
||||
}
|
||||
async function report(service, pid, code) {
|
||||
await service.status({
|
||||
ids: [1],
|
||||
status: 1,
|
||||
pid,
|
||||
log_path: 'one.log',
|
||||
last_running_time: 1,
|
||||
last_execution_time: 100,
|
||||
exit_code: code,
|
||||
});
|
||||
}
|
||||
test('stop wins when shell exit writes error or success during termination, preserving exit codes', async (t) => {
|
||||
const f = await fixture(t, async ({ service }) => {
|
||||
await report(service, 101, 143);
|
||||
await report(service, 102, 0);
|
||||
});
|
||||
await f.service.stop([1]);
|
||||
const rows = await f.instances.findAll({ order: [['id', 'ASC']], raw: true });
|
||||
assert.deepEqual(
|
||||
rows.map((r) => [r.id, r.status, r.exit_code]),
|
||||
[
|
||||
[1, 2, 143],
|
||||
[2, 2, 0],
|
||||
[3, 1, 0],
|
||||
[4, 3, 7],
|
||||
[5, 0, null],
|
||||
],
|
||||
);
|
||||
});
|
||||
test('late shell completion cannot overwrite stopped instances', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.service.stop([1]);
|
||||
await report(f.service, 101, 143);
|
||||
assert.equal((await f.instances.findByPk(1)).status, 2);
|
||||
assert.equal((await f.instances.findByPk(2)).status, 2);
|
||||
});
|
||||
test('rows created after the stop snapshot are not relabelled', async (t) => {
|
||||
const f = await fixture(t, async ({ instances }) => {
|
||||
if (!(await instances.findByPk(6)))
|
||||
await instances.create({ id: 6, cron_id: 1, pid: 103, status: 0 });
|
||||
});
|
||||
await f.service.stop([1]);
|
||||
assert.equal((await f.instances.findByPk(6)).status, 0);
|
||||
assert.equal((await f.instances.findByPk(1)).status, 2);
|
||||
});
|
||||
test('repeated stop does not rewrite historical rows or their finished timestamps', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.service.stop([1]);
|
||||
const before = await f.instances.findAll({
|
||||
raw: true,
|
||||
order: [['id', 'ASC']],
|
||||
});
|
||||
await f.service.stop([1]);
|
||||
assert.deepEqual(
|
||||
await f.instances.findAll({ raw: true, order: [['id', 'ASC']] }),
|
||||
before,
|
||||
);
|
||||
});
|
||||
test('batch stop captures running instances from every requested cron', async (t) => {
|
||||
const f = await fixture(t);
|
||||
await f.service.stop([1, 2]);
|
||||
assert.equal((await f.instances.findByPk(5)).status, 2);
|
||||
assert.equal((await f.instances.findByPk(3)).status, 1);
|
||||
});
|
||||
|
||||
test('stop signals every snapshotted PID exactly once and preserves a later running instance', async (t) => {
|
||||
const f = await fixture(t, async ({ instances, crons }) => {
|
||||
if (!(await instances.findByPk(6))) {
|
||||
await instances.create({ id: 6, cron_id: 1, pid: 103, status: 0 });
|
||||
await crons.update(
|
||||
{ pid: 103, log_path: 'later.log', status: 0 },
|
||||
{ where: { id: 1 } },
|
||||
);
|
||||
}
|
||||
});
|
||||
await f.service.stop([1]);
|
||||
assert.deepEqual(f.killed, [101, 102]);
|
||||
assert.equal((await f.instances.findByPk(6)).status, 0);
|
||||
assert.equal((await f.crons.findByPk(1)).pid, 103);
|
||||
assert.equal((await f.crons.findByPk(1)).status, 0);
|
||||
});
|
||||
|
||||
test('failed termination is not finalized as stopped', async (t) => {
|
||||
const f = await fixture(t, async ({ pid }) => {
|
||||
if (pid === 102) throw Error('still alive');
|
||||
});
|
||||
await f.service.stop([1]);
|
||||
assert.deepEqual(f.killed, [101, 102]);
|
||||
assert.equal((await f.instances.findByPk(1)).status, 2);
|
||||
assert.equal((await f.instances.findByPk(2)).status, 0);
|
||||
assert.equal((await f.crons.findByPk(1)).status, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user