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,127 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
const fs = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { Sequelize, QueryTypes } = require('sequelize');
|
||||
const { migrateSchema } = require('../../back/shared/schemaMigrations');
|
||||
const load = require('../helpers/load-security-module.cjs');
|
||||
|
||||
async function createLegacy(storage = ':memory:', omitEnvs = false) {
|
||||
const database = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage,
|
||||
logging: false,
|
||||
});
|
||||
for (const table of [
|
||||
'CrontabViews',
|
||||
'Subscriptions',
|
||||
'Crontabs',
|
||||
...(omitEnvs ? [] : ['Envs']),
|
||||
]) {
|
||||
await database.query(
|
||||
`CREATE TABLE "${table}" (id INTEGER PRIMARY KEY, name TEXT)`,
|
||||
);
|
||||
}
|
||||
await database.query(
|
||||
'INSERT INTO "Crontabs" (id, name) VALUES (1, \'keep-me\')',
|
||||
);
|
||||
return database;
|
||||
}
|
||||
|
||||
test('legacy database upgrades without losing rows and repeated migration is idempotent', async (t) => {
|
||||
const database = await createLegacy();
|
||||
t.after(() => database.close());
|
||||
await migrateSchema(database);
|
||||
await migrateSchema(database);
|
||||
const rows = await database.query('SELECT * FROM "Crontabs"', {
|
||||
type: QueryTypes.SELECT,
|
||||
});
|
||||
assert.equal(rows[0].name, 'keep-me');
|
||||
assert.equal(rows[0].queued_token, null);
|
||||
assert.ok(Object.hasOwn(rows[0], 'allow_multiple_instances'));
|
||||
const applied = await database.query('SELECT id FROM "SchemaMigrations"', {
|
||||
type: QueryTypes.SELECT,
|
||||
});
|
||||
assert.equal(applied.length, 15);
|
||||
});
|
||||
|
||||
test('migration failure rolls back added columns and can be retried after repair', async (t) => {
|
||||
const database = await createLegacy(':memory:', true);
|
||||
t.after(() => database.close());
|
||||
await assert.rejects(migrateSchema(database));
|
||||
const columns = await database.getQueryInterface().describeTable('Crontabs');
|
||||
assert.equal(Object.hasOwn(columns, 'work_dir'), false);
|
||||
await database.query(
|
||||
'CREATE TABLE "Envs" (id INTEGER PRIMARY KEY, name TEXT)',
|
||||
);
|
||||
await migrateSchema(database);
|
||||
assert.ok(
|
||||
Object.hasOwn(
|
||||
await database.getQueryInterface().describeTable('Crontabs'),
|
||||
'work_dir',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('offline pre-upgrade backup restores the legacy schema and data', async (t) => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'ql-migration-'));
|
||||
t.after(() => fs.rm(dir, { recursive: true, force: true }));
|
||||
const file = path.join(dir, 'database.sqlite');
|
||||
const backup = path.join(dir, 'before.sqlite');
|
||||
let database = await createLegacy(file);
|
||||
await database.close();
|
||||
await fs.copyFile(file, backup);
|
||||
database = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: file,
|
||||
logging: false,
|
||||
});
|
||||
await migrateSchema(database);
|
||||
await database.close();
|
||||
await fs.copyFile(backup, file);
|
||||
database = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: file,
|
||||
logging: false,
|
||||
});
|
||||
try {
|
||||
assert.equal(
|
||||
Object.hasOwn(
|
||||
await database.getQueryInterface().describeTable('Crontabs'),
|
||||
'work_dir',
|
||||
),
|
||||
false,
|
||||
);
|
||||
const rows = await database.query('SELECT name FROM "Crontabs"', {
|
||||
type: QueryTypes.SELECT,
|
||||
});
|
||||
assert.equal(rows[0].name, 'keep-me');
|
||||
} finally {
|
||||
await database.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('database loader rejects initialization failure rather than allowing workers to start', async () => {
|
||||
const mocks = { './logger': { error() {}, info() {} } };
|
||||
for (const [file, model] of [
|
||||
['env', 'EnvModel'],
|
||||
['cron', 'CrontabModel'],
|
||||
['dependence', 'DependenceModel'],
|
||||
['open', 'AppModel'],
|
||||
['system', 'SystemModel'],
|
||||
['subscription', 'SubscriptionModel'],
|
||||
['cronView', 'CrontabViewModel'],
|
||||
['cronStats', 'CrontabStatModel'],
|
||||
['runningInstance', 'RunningInstanceModel'],
|
||||
])
|
||||
mocks[`../data/${file}`] = { [model]: { sync: async () => {} } };
|
||||
mocks['../data'] = { sequelize: {} };
|
||||
mocks['../shared/schemaMigrations'] = {
|
||||
migrateSchema: async () => {
|
||||
throw new Error('SQLITE_FULL');
|
||||
},
|
||||
};
|
||||
const initialize = load(path.resolve('back/loaders/db.ts'), mocks).default;
|
||||
await assert.rejects(initialize(), /SQLITE_FULL/);
|
||||
});
|
||||
Reference in New Issue
Block a user