mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
192 lines
4.8 KiB
JavaScript
192 lines
4.8 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
ClusterWorkflowSchedulerCoordinator,
|
|
} = require('@qinglong/cluster-control/workflow-scheduler');
|
|
|
|
const DIGEST_A = 'a'.repeat(64);
|
|
const DIGEST_B = 'b'.repeat(64);
|
|
|
|
function schedulerSummary() {
|
|
return Object.freeze({
|
|
firstClaimAcquiredAtMs: null,
|
|
lastClaimAcquiredAtMs: null,
|
|
claimed: 0,
|
|
initialized: 0,
|
|
skipped: 0,
|
|
admitted: 0,
|
|
raced: 0,
|
|
saturated: false,
|
|
});
|
|
}
|
|
|
|
test('advances Workflow frontier and admits Tasks inside one existing scheduler cadence', async () => {
|
|
const calls = [];
|
|
const scheduler = {
|
|
async scheduleOnce() {
|
|
calls.push('schedule');
|
|
return schedulerSummary();
|
|
},
|
|
};
|
|
const frontier = {
|
|
async listCandidates(query) {
|
|
calls.push(`frontier:list:${query.after?.planDigest ?? 'start'}`);
|
|
if (!query.after) {
|
|
return {
|
|
candidates: [
|
|
{ runId: 'run-a' },
|
|
{ runId: 'run-b' },
|
|
],
|
|
truncated: true,
|
|
next: { admittedAtMs: 2, planDigest: DIGEST_A },
|
|
};
|
|
}
|
|
return {
|
|
candidates: [{ runId: 'run-c' }],
|
|
truncated: false,
|
|
};
|
|
},
|
|
async advance(runId) {
|
|
calls.push(`frontier:advance:${runId}`);
|
|
return { status: 'advanced' };
|
|
},
|
|
};
|
|
const taskAttempts = {
|
|
async listCandidates(query) {
|
|
calls.push(`attempt:list:${query.after?.stepRunId ?? 'start'}`);
|
|
if (!query.after) {
|
|
return {
|
|
candidates: [
|
|
{ runId: 'run-a', stepRunId: 'step-a' },
|
|
{ runId: 'run-b', stepRunId: 'step-b' },
|
|
],
|
|
truncated: true,
|
|
next: { readyAtMs: 3, stepRunId: 'step-b' },
|
|
};
|
|
}
|
|
return {
|
|
candidates: [{ runId: 'run-c', stepRunId: 'step-c' }],
|
|
truncated: false,
|
|
};
|
|
},
|
|
async admit(runId, stepRunId) {
|
|
calls.push(`attempt:admit:${runId}:${stepRunId}`);
|
|
return {
|
|
status: stepRunId === 'step-b' ? 'existing' : 'created',
|
|
receipt: {},
|
|
};
|
|
},
|
|
};
|
|
const coordinator = new ClusterWorkflowSchedulerCoordinator(
|
|
scheduler,
|
|
frontier,
|
|
taskAttempts,
|
|
{
|
|
frontierPageSize: 2,
|
|
frontierMaxPages: 2,
|
|
taskAttemptPageSize: 2,
|
|
taskAttemptMaxPages: 2,
|
|
},
|
|
);
|
|
|
|
assert.deepEqual(await coordinator.scheduleOnce(), schedulerSummary());
|
|
assert.deepEqual(coordinator.latestWorkflowSummary(), {
|
|
frontierPages: 2,
|
|
frontierScanned: 3,
|
|
frontierAdvanced: 3,
|
|
frontierTruncated: false,
|
|
taskAttemptPages: 2,
|
|
taskAttemptsScanned: 3,
|
|
taskAttemptsCreated: 2,
|
|
taskAttemptsExisting: 1,
|
|
taskAttemptsTruncated: false,
|
|
});
|
|
assert.deepEqual(calls, [
|
|
'schedule',
|
|
'frontier:list:start',
|
|
'frontier:advance:run-a',
|
|
'frontier:advance:run-b',
|
|
`frontier:list:${DIGEST_A}`,
|
|
'frontier:advance:run-c',
|
|
'attempt:list:start',
|
|
'attempt:admit:run-a:step-a',
|
|
'attempt:admit:run-b:step-b',
|
|
'attempt:list:step-b',
|
|
'attempt:admit:run-c:step-c',
|
|
]);
|
|
});
|
|
|
|
test('coalesces overlapping cycles and fails closed on a stale continuation', async () => {
|
|
let release;
|
|
let schedulerCalls = 0;
|
|
const scheduled = new Promise((resolve) => {
|
|
release = resolve;
|
|
});
|
|
const coordinator = new ClusterWorkflowSchedulerCoordinator(
|
|
{
|
|
async scheduleOnce() {
|
|
schedulerCalls += 1;
|
|
await scheduled;
|
|
return schedulerSummary();
|
|
},
|
|
},
|
|
{
|
|
async listCandidates() {
|
|
return {
|
|
candidates: [{ runId: 'run-a' }],
|
|
truncated: true,
|
|
next: { admittedAtMs: 1, planDigest: DIGEST_B },
|
|
};
|
|
},
|
|
async advance() {
|
|
return { status: 'advanced' };
|
|
},
|
|
},
|
|
{
|
|
async listCandidates() {
|
|
return { candidates: [], truncated: false };
|
|
},
|
|
async admit() {
|
|
throw new Error('unexpected admission');
|
|
},
|
|
},
|
|
{
|
|
frontierPageSize: 1,
|
|
frontierMaxPages: 2,
|
|
taskAttemptPageSize: 1,
|
|
taskAttemptMaxPages: 1,
|
|
},
|
|
);
|
|
|
|
const first = coordinator.scheduleOnce();
|
|
const second = coordinator.scheduleOnce();
|
|
assert.equal(first, second);
|
|
release();
|
|
await assert.rejects(
|
|
first,
|
|
/frontier continuation did not advance/,
|
|
);
|
|
assert.equal(schedulerCalls, 1);
|
|
assert.equal(coordinator.latestWorkflowSummary(), undefined);
|
|
});
|
|
|
|
test('rejects invalid bounds before touching the production scheduler', () => {
|
|
assert.throws(
|
|
() =>
|
|
new ClusterWorkflowSchedulerCoordinator(
|
|
{ scheduleOnce() {} },
|
|
{ listCandidates() {}, advance() {} },
|
|
{ listCandidates() {}, admit() {} },
|
|
{
|
|
frontierPageSize: 65,
|
|
frontierMaxPages: 1,
|
|
taskAttemptPageSize: 1,
|
|
taskAttemptMaxPages: 1,
|
|
},
|
|
),
|
|
/frontier page size must be between 1 and 64/,
|
|
);
|
|
});
|