mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
258 lines
6.9 KiB
JavaScript
258 lines
6.9 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { test } = require('node:test');
|
|
const {
|
|
resolveLocalScheduleDecision,
|
|
} = require('@qinglong/runtime-core/local-scheduler');
|
|
const {
|
|
migrateLocalSqlitePath,
|
|
openLocalSqliteRuntimeDatabase,
|
|
} = require('../dist');
|
|
|
|
function nextMinute(schedule, afterMs) {
|
|
if (schedule.expression !== '* * * * *' || schedule.timezone !== 'UTC') {
|
|
throw new Error('unsupported test schedule');
|
|
}
|
|
return Math.floor(afterMs / 60_000 + 1) * 60_000;
|
|
}
|
|
|
|
function id(value) {
|
|
return `019f7400-0000-4000-8000-${String(value).padStart(12, '0')}`;
|
|
}
|
|
|
|
async function fixture(t) {
|
|
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-schedule-'));
|
|
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
|
|
const databasePath = path.join(directory, 'qinglong3.sqlite');
|
|
await migrateLocalSqlitePath({ databasePath, profile: 'edge' });
|
|
const runtime = await openLocalSqliteRuntimeDatabase({
|
|
databasePath,
|
|
profile: 'edge',
|
|
});
|
|
t.after(() => runtime.close());
|
|
const task = (
|
|
await runtime.taskDefinitions.appendTaskDefinitionRevision({
|
|
projectId: 'default',
|
|
taskId: 'task-1',
|
|
expectedRevision: null,
|
|
mutationId: '019f7410-0000-7000-8000-000000000001',
|
|
name: 'Scheduled task',
|
|
kind: 'command',
|
|
spec: {
|
|
schema: 'qinglong/command@v1',
|
|
config: {
|
|
command: { kind: 'argv', file: '/bin/echo', args: ['scheduled'] },
|
|
},
|
|
},
|
|
labels: {},
|
|
enabled: true,
|
|
occurredAtMs: 1,
|
|
})
|
|
).definition;
|
|
await runtime.triggers.appendTriggerRevision({
|
|
projectId: 'default',
|
|
triggerId: 'trigger-1',
|
|
expectedRevision: null,
|
|
mutationId: '019f7420-0000-7000-8000-000000000001',
|
|
taskId: task.taskId,
|
|
taskRevision: task.revision,
|
|
taskContentDigest: task.contentDigest,
|
|
spec: {
|
|
schema: 'qinglong/cron@v1',
|
|
config: {
|
|
expression: '* * * * *',
|
|
timezone: 'UTC',
|
|
misfirePolicy: 'skip',
|
|
},
|
|
},
|
|
enabled: true,
|
|
occurredAtMs: 1,
|
|
});
|
|
return runtime;
|
|
}
|
|
|
|
test('atomically advances one due Trigger into a queued Run aggregate', async (t) => {
|
|
const runtime = await fixture(t);
|
|
const page = await runtime.schedules.listLocalScheduleCandidates({
|
|
observedAtMs: 61_000,
|
|
limit: 4,
|
|
});
|
|
assert.equal(page.candidates.length, 1);
|
|
assert.equal(page.truncated, false);
|
|
assert.equal(page.candidates[0].nextFireAtMs, null);
|
|
const decision = resolveLocalScheduleDecision(
|
|
page.candidates[0],
|
|
61_000,
|
|
5_000,
|
|
nextMinute,
|
|
);
|
|
const admitted = await runtime.schedules.commitLocalScheduleDecision({
|
|
decision,
|
|
runId: id(1),
|
|
attemptId: id(2),
|
|
createdEventId: id(3),
|
|
queuedEventId: id(4),
|
|
});
|
|
assert.deepEqual(admitted, {
|
|
status: 'admitted',
|
|
disposition: 'admit',
|
|
runId: id(1),
|
|
attemptId: id(2),
|
|
});
|
|
const run = await runtime.runRepository.findRunById(id(1));
|
|
assert.equal(run.status, 'queued');
|
|
assert.equal(run.executionOwner, 'runtime');
|
|
assert.equal(run.executionOrigin, 'scheduled_system');
|
|
assert.equal(run.triggerId, 'trigger-1');
|
|
assert.equal(run.scheduledForMs, 60_000);
|
|
assert.match(run.taskRevision, /^qltd:v1:1:[a-f0-9]{64}$/);
|
|
assert.equal(
|
|
(await runtime.runRepository.findAttemptById(id(2))).status,
|
|
'claimed',
|
|
);
|
|
assert.deepEqual(
|
|
(await runtime.runRepository.listEvents(id(1))).map((event) => event.type),
|
|
['run.created', 'run.queued'],
|
|
);
|
|
assert.equal(
|
|
(
|
|
await runtime.schedules.listLocalScheduleCandidates({
|
|
observedAtMs: 61_000,
|
|
limit: 4,
|
|
})
|
|
).candidates.length,
|
|
0,
|
|
);
|
|
assert.deepEqual(
|
|
await runtime.schedules.commitLocalScheduleDecision({
|
|
decision,
|
|
runId: id(5),
|
|
attemptId: id(6),
|
|
createdEventId: id(7),
|
|
queuedEventId: id(8),
|
|
}),
|
|
{ status: 'raced' },
|
|
);
|
|
});
|
|
|
|
test('fences a stale Task head before schedule discovery and final Run commit', async (t) => {
|
|
const runtime = await fixture(t);
|
|
const page = await runtime.schedules.listLocalScheduleCandidates({
|
|
observedAtMs: 61_000,
|
|
limit: 4,
|
|
});
|
|
const decision = resolveLocalScheduleDecision(
|
|
page.candidates[0],
|
|
61_000,
|
|
5_000,
|
|
nextMinute,
|
|
);
|
|
const task = await runtime.taskDefinitions.findCurrentTaskDefinition(
|
|
'default',
|
|
'task-1',
|
|
);
|
|
const disabled = (
|
|
await runtime.taskDefinitions.appendTaskDefinitionRevision({
|
|
projectId: task.projectId,
|
|
taskId: task.taskId,
|
|
expectedRevision: task.revision,
|
|
mutationId: '019f7410-0000-7000-8000-000000000002',
|
|
name: task.name,
|
|
kind: task.kind,
|
|
spec: task.spec,
|
|
labels: task.labels,
|
|
enabled: false,
|
|
occurredAtMs: 2,
|
|
})
|
|
).definition;
|
|
|
|
assert.equal(
|
|
(
|
|
await runtime.schedules.listLocalScheduleCandidates({
|
|
observedAtMs: 61_000,
|
|
limit: 4,
|
|
})
|
|
).candidates.length,
|
|
0,
|
|
);
|
|
assert.deepEqual(
|
|
await runtime.schedules.commitLocalScheduleDecision({
|
|
decision,
|
|
runId: id(21),
|
|
attemptId: id(22),
|
|
createdEventId: id(23),
|
|
queuedEventId: id(24),
|
|
}),
|
|
{ status: 'raced' },
|
|
);
|
|
assert.equal(await runtime.runRepository.findRunById(id(21)), null);
|
|
|
|
await assert.rejects(
|
|
runtime.triggers.appendTriggerRevision({
|
|
projectId: 'default',
|
|
triggerId: 'trigger-1',
|
|
expectedRevision: 1,
|
|
mutationId: '019f7420-0000-7000-8000-000000000002',
|
|
taskId: task.taskId,
|
|
taskRevision: task.revision,
|
|
taskContentDigest: task.contentDigest,
|
|
spec: {
|
|
schema: 'qinglong/cron@v1',
|
|
config: {
|
|
expression: '* * * * *',
|
|
timezone: 'UTC',
|
|
misfirePolicy: 'skip',
|
|
},
|
|
},
|
|
enabled: true,
|
|
occurredAtMs: 3,
|
|
}),
|
|
{ code: 'TRIGGER_CONFLICT' },
|
|
);
|
|
|
|
const enabled = (
|
|
await runtime.taskDefinitions.appendTaskDefinitionRevision({
|
|
projectId: disabled.projectId,
|
|
taskId: disabled.taskId,
|
|
expectedRevision: disabled.revision,
|
|
mutationId: '019f7410-0000-7000-8000-000000000003',
|
|
name: disabled.name,
|
|
kind: disabled.kind,
|
|
spec: disabled.spec,
|
|
labels: disabled.labels,
|
|
enabled: true,
|
|
occurredAtMs: 4,
|
|
})
|
|
).definition;
|
|
await runtime.triggers.appendTriggerRevision({
|
|
projectId: 'default',
|
|
triggerId: 'trigger-1',
|
|
expectedRevision: 1,
|
|
mutationId: '019f7420-0000-7000-8000-000000000003',
|
|
taskId: enabled.taskId,
|
|
taskRevision: enabled.revision,
|
|
taskContentDigest: enabled.contentDigest,
|
|
spec: {
|
|
schema: 'qinglong/cron@v1',
|
|
config: {
|
|
expression: '* * * * *',
|
|
timezone: 'UTC',
|
|
misfirePolicy: 'skip',
|
|
},
|
|
},
|
|
enabled: true,
|
|
occurredAtMs: 5,
|
|
});
|
|
assert.equal(
|
|
(
|
|
await runtime.schedules.listLocalScheduleCandidates({
|
|
observedAtMs: 61_000,
|
|
limit: 4,
|
|
})
|
|
).candidates.length,
|
|
1,
|
|
);
|
|
});
|