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,396 @@
|
||||
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 {
|
||||
LegacyShadowRunCorrelator,
|
||||
} = require('../../back/runtime/application/legacyShadowRunCorrelator');
|
||||
const {
|
||||
LegacyShadowRunWriter,
|
||||
} = require('../../back/runtime/application/legacyShadowRunWriter');
|
||||
|
||||
const databases = [];
|
||||
let idSequence = 600;
|
||||
let timeSequence = 1_750_000_100_000;
|
||||
|
||||
function nextId() {
|
||||
idSequence += 1;
|
||||
return `019f70f0-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
|
||||
}
|
||||
|
||||
function nextTime() {
|
||||
timeSequence += 10;
|
||||
return timeSequence;
|
||||
}
|
||||
|
||||
async function createRepository() {
|
||||
const database = new Sequelize({
|
||||
dialect: 'sqlite',
|
||||
storage: ':memory:',
|
||||
logging: false,
|
||||
});
|
||||
const migrationModel = defineSchemaMigrationModel(database);
|
||||
await runMigrations({
|
||||
database,
|
||||
migrationModel,
|
||||
migrations: [
|
||||
runSchemaMigration,
|
||||
runCancellationRequestMigration,
|
||||
runAttemptDeadlineMigration,
|
||||
],
|
||||
logger: { info() {} },
|
||||
});
|
||||
databases.push(database);
|
||||
return new LegacySequelizeRunRepository(database);
|
||||
}
|
||||
|
||||
async function createActive(writer, overrides = {}) {
|
||||
const atMs = nextTime();
|
||||
const reference = await writer.accept({
|
||||
origin: 'manual',
|
||||
projectId: 'default',
|
||||
taskId: 'legacy-cron:15',
|
||||
taskRevision: 'sha256:revision',
|
||||
legacyCronId: 15,
|
||||
triggerType: 'manual',
|
||||
acceptedAtMs: atMs,
|
||||
...overrides,
|
||||
});
|
||||
await writer.spawned(reference, {
|
||||
atMs: atMs + 1,
|
||||
...(overrides.pid === undefined ? {} : { pid: overrides.pid }),
|
||||
...(overrides.logArtifactId === undefined
|
||||
? {}
|
||||
: { logArtifactId: overrides.logArtifactId }),
|
||||
});
|
||||
await writer.running(reference, atMs + 2);
|
||||
return { reference, atMs };
|
||||
}
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(databases.splice(0).map((database) => database.close()));
|
||||
});
|
||||
|
||||
test('lists bounded active candidates by legacy cron and origin', async () => {
|
||||
const repository = await createRepository();
|
||||
const writer = new LegacyShadowRunWriter(repository, nextId);
|
||||
await createActive(writer, { pid: 11, logArtifactId: 'log-a' });
|
||||
await createActive(writer, { pid: 22, logArtifactId: 'log-b' });
|
||||
await createActive(writer, {
|
||||
origin: 'scheduled_node',
|
||||
pid: 33,
|
||||
logArtifactId: 'log-c',
|
||||
});
|
||||
|
||||
const manual = await repository.listActiveByLegacyCron({
|
||||
legacyCronId: 15,
|
||||
origins: ['manual'],
|
||||
});
|
||||
assert.equal(manual.candidates.length, 2);
|
||||
assert.equal(manual.truncated, false);
|
||||
assert.deepEqual(
|
||||
new Set(manual.candidates.map((candidate) => candidate.pid)),
|
||||
new Set([11, 22]),
|
||||
);
|
||||
|
||||
const bounded = await repository.listActiveByLegacyCron({
|
||||
legacyCronId: 15,
|
||||
origins: ['manual', 'scheduled_node'],
|
||||
limit: 1,
|
||||
});
|
||||
assert.equal(bounded.candidates.length, 1);
|
||||
assert.equal(bounded.truncated, true);
|
||||
await assert.rejects(
|
||||
repository.listActiveByLegacyCron({
|
||||
legacyCronId: 0,
|
||||
origins: ['manual'],
|
||||
}),
|
||||
RangeError,
|
||||
);
|
||||
});
|
||||
|
||||
test('correlates callbacks by log before pid and refuses ambiguity', async () => {
|
||||
const repository = await createRepository();
|
||||
const writer = new LegacyShadowRunWriter(repository, nextId);
|
||||
const first = await createActive(writer, {
|
||||
pid: 41,
|
||||
logArtifactId: 'log-first',
|
||||
});
|
||||
const second = await createActive(writer, {
|
||||
pid: 42,
|
||||
logArtifactId: 'log-second',
|
||||
});
|
||||
const failures = [];
|
||||
const correlator = new LegacyShadowRunCorrelator(repository, writer, {
|
||||
failure: (failure) => failures.push(failure),
|
||||
});
|
||||
|
||||
const ambiguous = await correlator.callback(
|
||||
{
|
||||
legacyCronId: 15,
|
||||
atMs: nextTime(),
|
||||
phase: 'running',
|
||||
},
|
||||
['manual'],
|
||||
);
|
||||
assert.equal(ambiguous.matched, 0);
|
||||
assert.equal(failures[0].reason, 'ambiguous');
|
||||
|
||||
const conflicting = await correlator.callback(
|
||||
{
|
||||
legacyCronId: 15,
|
||||
pid: 41,
|
||||
logArtifactId: 'log-second',
|
||||
atMs: nextTime(),
|
||||
phase: 'finished',
|
||||
exitCode: 0,
|
||||
},
|
||||
['manual'],
|
||||
);
|
||||
assert.equal(conflicting.matched, 0);
|
||||
assert.equal(failures[1].reason, 'ambiguous');
|
||||
|
||||
const finished = await correlator.callback(
|
||||
{
|
||||
legacyCronId: 15,
|
||||
pid: 999,
|
||||
logArtifactId: 'log-second',
|
||||
atMs: nextTime(),
|
||||
phase: 'finished',
|
||||
exitCode: 0,
|
||||
},
|
||||
['manual'],
|
||||
);
|
||||
assert.equal(finished.matched, 1);
|
||||
assert.equal(
|
||||
(await repository.findRunById(second.reference.runId)).status,
|
||||
'succeeded',
|
||||
);
|
||||
assert.equal(
|
||||
(await repository.findRunById(first.reference.runId)).status,
|
||||
'running',
|
||||
);
|
||||
});
|
||||
|
||||
test('cancels one exact candidate or every active candidate', async () => {
|
||||
const repository = await createRepository();
|
||||
const writer = new LegacyShadowRunWriter(repository, nextId);
|
||||
const first = await createActive(writer, { pid: 51 });
|
||||
const second = await createActive(writer, { pid: 52 });
|
||||
const failures = [];
|
||||
const correlator = new LegacyShadowRunCorrelator(repository, writer, {
|
||||
failure: (failure) => failures.push(failure),
|
||||
});
|
||||
|
||||
const one = await correlator.cancel(
|
||||
{
|
||||
legacyCronId: 15,
|
||||
pid: 51,
|
||||
atMs: nextTime(),
|
||||
scope: 'one',
|
||||
reason: 'user',
|
||||
},
|
||||
['manual'],
|
||||
);
|
||||
assert.equal(one.matched, 1);
|
||||
assert.equal(
|
||||
(await repository.findRunById(first.reference.runId)).status,
|
||||
'cancelled',
|
||||
);
|
||||
assert.equal(
|
||||
(await repository.findRunById(second.reference.runId)).status,
|
||||
'running',
|
||||
);
|
||||
|
||||
const all = await correlator.cancel(
|
||||
{
|
||||
legacyCronId: 15,
|
||||
atMs: nextTime(),
|
||||
scope: 'all',
|
||||
reason: 'policy',
|
||||
},
|
||||
['manual'],
|
||||
);
|
||||
assert.equal(all.matched, 1);
|
||||
assert.equal(
|
||||
(await repository.findRunById(second.reference.runId)).status,
|
||||
'cancelled',
|
||||
);
|
||||
assert.deepEqual(failures, []);
|
||||
});
|
||||
|
||||
test('recovers an out-of-order finish and ignores duplicate terminal callbacks', async () => {
|
||||
const repository = await createRepository();
|
||||
const writer = new LegacyShadowRunWriter(repository, nextId);
|
||||
const atMs = nextTime();
|
||||
const reference = await writer.accept({
|
||||
origin: 'manual',
|
||||
projectId: 'default',
|
||||
taskId: 'legacy-cron:16',
|
||||
taskRevision: 'sha256:revision',
|
||||
legacyCronId: 16,
|
||||
triggerType: 'manual',
|
||||
acceptedAtMs: atMs,
|
||||
});
|
||||
const failures = [];
|
||||
const correlator = new LegacyShadowRunCorrelator(repository, writer, {
|
||||
failure: (failure) => failures.push(failure),
|
||||
});
|
||||
|
||||
const first = await correlator.callback(
|
||||
{
|
||||
legacyCronId: 16,
|
||||
pid: 160,
|
||||
logArtifactId: 'log-16',
|
||||
atMs: atMs + 10,
|
||||
phase: 'finished',
|
||||
exitCode: 0,
|
||||
},
|
||||
['manual'],
|
||||
);
|
||||
assert.equal(first.matched, 1);
|
||||
assert.equal(
|
||||
(await repository.findRunById(reference.runId)).status,
|
||||
'succeeded',
|
||||
);
|
||||
const events = await repository.listEvents(reference.runId);
|
||||
assert.deepEqual(
|
||||
events.map((event) => event.type),
|
||||
[
|
||||
'run.created',
|
||||
'run.queued',
|
||||
'run.dispatching',
|
||||
'attempt.starting',
|
||||
'attempt.running',
|
||||
'run.running',
|
||||
'attempt.succeeded',
|
||||
'run.succeeded',
|
||||
],
|
||||
);
|
||||
|
||||
const duplicate = await correlator.callback(
|
||||
{
|
||||
legacyCronId: 16,
|
||||
pid: 160,
|
||||
logArtifactId: 'log-16',
|
||||
atMs: atMs + 20,
|
||||
phase: 'finished',
|
||||
exitCode: 0,
|
||||
},
|
||||
['manual'],
|
||||
);
|
||||
assert.equal(duplicate.matched, 0);
|
||||
assert.equal(failures[0].reason, 'unmatched');
|
||||
assert.equal((await repository.listEvents(reference.runId)).length, 8);
|
||||
});
|
||||
|
||||
test('keeps cancellation terminal when a late successful callback arrives', async () => {
|
||||
const repository = await createRepository();
|
||||
const writer = new LegacyShadowRunWriter(repository, nextId);
|
||||
const active = await createActive(writer, { pid: 170 });
|
||||
const failures = [];
|
||||
const correlator = new LegacyShadowRunCorrelator(repository, writer, {
|
||||
failure: (failure) => failures.push(failure),
|
||||
});
|
||||
|
||||
await correlator.cancel(
|
||||
{
|
||||
legacyCronId: 15,
|
||||
pid: 170,
|
||||
atMs: nextTime(),
|
||||
scope: 'one',
|
||||
reason: 'user',
|
||||
},
|
||||
['manual'],
|
||||
);
|
||||
const eventCount = (await repository.listEvents(active.reference.runId))
|
||||
.length;
|
||||
const late = await correlator.callback(
|
||||
{
|
||||
legacyCronId: 15,
|
||||
pid: 170,
|
||||
atMs: nextTime(),
|
||||
phase: 'finished',
|
||||
exitCode: 0,
|
||||
},
|
||||
['manual'],
|
||||
);
|
||||
|
||||
assert.equal(late.matched, 0);
|
||||
assert.equal(
|
||||
(await repository.findRunById(active.reference.runId)).status,
|
||||
'cancelled',
|
||||
);
|
||||
assert.equal(
|
||||
(await repository.listEvents(active.reference.runId)).length,
|
||||
eventCount,
|
||||
);
|
||||
assert.equal(failures[0].reason, 'unmatched');
|
||||
});
|
||||
|
||||
test('continues cancel-all correlation after an individual shadow write fails', async () => {
|
||||
const writes = [];
|
||||
const failures = [];
|
||||
const locator = {
|
||||
async listActiveByLegacyCron() {
|
||||
return {
|
||||
truncated: false,
|
||||
candidates: [
|
||||
{
|
||||
runId: 'run-a',
|
||||
attemptId: 'attempt-a',
|
||||
origin: 'manual',
|
||||
runStatus: 'running',
|
||||
attemptStatus: 'running',
|
||||
createdAtMs: 1,
|
||||
},
|
||||
{
|
||||
runId: 'run-b',
|
||||
attemptId: 'attempt-b',
|
||||
origin: 'manual',
|
||||
runStatus: 'running',
|
||||
attemptStatus: 'running',
|
||||
createdAtMs: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
};
|
||||
const writer = {
|
||||
async cancelled(reference) {
|
||||
writes.push(reference.runId);
|
||||
if (reference.runId === 'run-a') throw new Error('write failed');
|
||||
},
|
||||
};
|
||||
const correlator = new LegacyShadowRunCorrelator(locator, writer, {
|
||||
failure: (failure) => failures.push(failure),
|
||||
});
|
||||
|
||||
const result = await correlator.cancel(
|
||||
{
|
||||
legacyCronId: 15,
|
||||
atMs: 4,
|
||||
scope: 'all',
|
||||
reason: 'user',
|
||||
},
|
||||
['manual'],
|
||||
);
|
||||
assert.deepEqual(writes, ['run-a', 'run-b']);
|
||||
assert.equal(result.matched, 1);
|
||||
assert.equal(failures[0].reason, 'write_failed');
|
||||
});
|
||||
Reference in New Issue
Block a user