mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
214 lines
5.9 KiB
JavaScript
214 lines
5.9 KiB
JavaScript
require('ts-node/register/transpile-only');
|
|
|
|
const assert = require('node:assert/strict');
|
|
const { afterEach, test } = require('node:test');
|
|
const { Sequelize } = require('sequelize');
|
|
const {
|
|
defineSchemaMigrationModel,
|
|
} = require('../../back/data/schemaMigration');
|
|
const { runSchemaMigration } = require('../../back/migrations/0002-run-schema');
|
|
const {
|
|
runCancellationRequestMigration,
|
|
} = require('../../back/migrations/0004-run-cancellation-request');
|
|
const {
|
|
runAttemptDeadlineMigration,
|
|
} = require('../../back/migrations/0006-run-attempt-deadline');
|
|
const { runMigrations } = require('../../back/migrations/runner');
|
|
const {
|
|
LegacySequelizeRunRepository,
|
|
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
|
|
const {
|
|
LegacySequelizePrimaryTimeoutSource,
|
|
} = require('../../back/runtime/adapters/legacy-sequelize/primaryTimeoutSource');
|
|
const {
|
|
PrimaryTimeoutRequester,
|
|
} = require('../../back/runtime/application/primaryTimeoutRequester');
|
|
const {
|
|
RunCommandService,
|
|
} = require('../../back/runtime/application/runCommandService');
|
|
|
|
const NOW = 1_750_300_000_000;
|
|
const databases = [];
|
|
|
|
function page(candidates, overrides = {}) {
|
|
return { candidates, truncated: false, ...overrides };
|
|
}
|
|
|
|
function candidate(index, overrides = {}) {
|
|
return {
|
|
runId: `run-${index}`,
|
|
attemptId: `attempt-${index}`,
|
|
deadlineAtMs: NOW - index,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(databases.splice(0).map((database) => database.close()));
|
|
});
|
|
|
|
test('commits a durable timeout request through the real Repository boundary', async () => {
|
|
const database = new Sequelize({
|
|
dialect: 'sqlite',
|
|
storage: ':memory:',
|
|
logging: false,
|
|
});
|
|
databases.push(database);
|
|
await runMigrations({
|
|
database,
|
|
migrationModel: defineSchemaMigrationModel(database),
|
|
migrations: [
|
|
runSchemaMigration,
|
|
runCancellationRequestMigration,
|
|
runAttemptDeadlineMigration,
|
|
],
|
|
logger: { info() {} },
|
|
});
|
|
const repository = new LegacySequelizeRunRepository(database);
|
|
const runId = '019f7300-0000-7000-8000-000000000101';
|
|
const attemptId = '019f7300-0000-7000-8000-000000000102';
|
|
await repository.transaction(async (transaction) => {
|
|
await transaction.insertRun({
|
|
id: runId,
|
|
projectId: 'default',
|
|
taskId: 'timeout-task',
|
|
taskRevision: 'revision-1',
|
|
triggerType: 'manual',
|
|
executionOrigin: 'manual',
|
|
executionOwner: 'runtime',
|
|
status: 'running',
|
|
version: 3,
|
|
eventSequence: 3,
|
|
priority: 0,
|
|
createdAtMs: NOW - 10_000,
|
|
startedAtMs: NOW - 9_000,
|
|
});
|
|
await transaction.insertAttempt({
|
|
id: attemptId,
|
|
runId,
|
|
attempt: 1,
|
|
status: 'running',
|
|
executorType: 'local_process',
|
|
callbackSequence: 0,
|
|
createdAtMs: NOW - 10_000,
|
|
startedAtMs: NOW - 9_000,
|
|
deadlineAtMs: NOW - 1,
|
|
});
|
|
});
|
|
const requester = new PrimaryTimeoutRequester(
|
|
new LegacySequelizePrimaryTimeoutSource(database),
|
|
new RunCommandService(repository),
|
|
);
|
|
|
|
const result = await requester.requestBatch({ nowMs: NOW });
|
|
assert.equal(result.accepted, 1);
|
|
assert.equal(result.failed, 0);
|
|
const persisted = await repository.findRunById(runId);
|
|
assert.equal(persisted.cancelRequestedAtMs, NOW);
|
|
assert.equal(persisted.cancelReason, 'timeout');
|
|
assert.deepEqual(
|
|
(await repository.listEvents(runId)).map((event) => ({
|
|
type: event.type,
|
|
actorType: event.actorType,
|
|
actorId: event.actorId,
|
|
})),
|
|
[
|
|
{
|
|
type: 'run.cancel_requested',
|
|
actorType: 'system',
|
|
actorId: 'runtime:timeout',
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
test('persists timeout intent for each due candidate without side effects', async () => {
|
|
const commands = [];
|
|
const requester = new PrimaryTimeoutRequester(
|
|
{
|
|
async listOverdue(options) {
|
|
assert.deepEqual(options, { nowMs: NOW, limit: 8 });
|
|
return page([candidate(1), candidate(2), candidate(3)]);
|
|
},
|
|
},
|
|
{
|
|
async requestCancellation(command) {
|
|
commands.push(command);
|
|
if (command.runId === 'run-1') return { status: 'accepted' };
|
|
if (command.runId === 'run-2') return { status: 'already_requested' };
|
|
return { status: 'already_terminal' };
|
|
},
|
|
},
|
|
);
|
|
|
|
const result = await requester.requestBatch({ nowMs: NOW, limit: 8 });
|
|
assert.deepEqual(result, {
|
|
scanned: 3,
|
|
accepted: 1,
|
|
alreadyRequested: 1,
|
|
alreadyTerminal: 1,
|
|
failed: 0,
|
|
truncated: false,
|
|
});
|
|
assert.deepEqual(
|
|
commands.map(({ runId, attemptId, atMs, reason, actor }) => ({
|
|
runId,
|
|
attemptId,
|
|
atMs,
|
|
reason,
|
|
actor,
|
|
})),
|
|
[1, 2, 3].map((index) => ({
|
|
runId: `run-${index}`,
|
|
attemptId: `attempt-${index}`,
|
|
atMs: NOW,
|
|
reason: 'timeout',
|
|
actor: { type: 'system', id: 'runtime:timeout' },
|
|
})),
|
|
);
|
|
});
|
|
|
|
test('isolates candidate failures and refuses future deadline output', async () => {
|
|
let calls = 0;
|
|
const requester = new PrimaryTimeoutRequester(
|
|
{
|
|
async listOverdue() {
|
|
return page([
|
|
candidate(1),
|
|
candidate(2, { deadlineAtMs: NOW + 1 }),
|
|
candidate(3),
|
|
]);
|
|
},
|
|
},
|
|
{
|
|
async requestCancellation() {
|
|
calls += 1;
|
|
if (calls === 1) throw new Error('database unavailable');
|
|
return { status: 'accepted' };
|
|
},
|
|
},
|
|
);
|
|
|
|
const result = await requester.requestBatch({ nowMs: NOW });
|
|
assert.equal(result.scanned, 3);
|
|
assert.equal(result.accepted, 1);
|
|
assert.equal(result.failed, 2);
|
|
assert.equal(calls, 2);
|
|
});
|
|
|
|
test('validates its clock before querying the source', async () => {
|
|
let queried = false;
|
|
const requester = new PrimaryTimeoutRequester(
|
|
{
|
|
async listOverdue() {
|
|
queried = true;
|
|
return page([]);
|
|
},
|
|
},
|
|
{ async requestCancellation() {} },
|
|
{ now: () => -1 },
|
|
);
|
|
await assert.rejects(requester.requestBatch(), /nowMs/);
|
|
assert.equal(queried, false);
|
|
});
|