mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user