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,262 @@
|
||||
require('ts-node/register/transpile-only');
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
PrimaryCompletionReceiptLifecycle,
|
||||
} = require('../../back/runtime/application/primaryCompletionReceiptLifecycle');
|
||||
const {
|
||||
PrimaryCompletionReceiptScanner,
|
||||
} = require('../../back/runtime/application/primaryCompletionReceiptScanner');
|
||||
const {
|
||||
PrimaryCompletionReceiptSupervisor,
|
||||
} = require('../../back/runtime/application/primaryCompletionReceiptSupervisor');
|
||||
|
||||
function candidate(runId, attemptId, executorType = 'local_process') {
|
||||
return {
|
||||
runId,
|
||||
attempts: attemptId ? [{ attemptId, executorType }] : [],
|
||||
};
|
||||
}
|
||||
|
||||
function page(overrides = {}) {
|
||||
return {
|
||||
candidates: [],
|
||||
truncated: false,
|
||||
unsafeAttemptOverflow: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function scan(overrides = {}) {
|
||||
return {
|
||||
scanned: 1,
|
||||
applied: 0,
|
||||
alreadyTerminal: 0,
|
||||
quarantined: 0,
|
||||
purgedQuarantines: 0,
|
||||
expiredMissing: 0,
|
||||
missing: 1,
|
||||
cleanupPending: 0,
|
||||
skipped: 0,
|
||||
ambiguous: 0,
|
||||
failed: 0,
|
||||
truncated: false,
|
||||
unsafeAttemptOverflow: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('scans only database-discovered local Attempts and isolates failures', async () => {
|
||||
const consumed = [];
|
||||
const scanner = new PrimaryCompletionReceiptScanner(
|
||||
{
|
||||
async listCandidates() {
|
||||
return page({
|
||||
candidates: [
|
||||
candidate('run-1', 'applied'),
|
||||
candidate('run-2', 'terminal'),
|
||||
candidate('run-3', 'missing'),
|
||||
candidate('run-4', 'cleanup-pending'),
|
||||
candidate('run-5', 'failed'),
|
||||
candidate('run-6', 'quarantined'),
|
||||
candidate('run-7', 'remote', 'remote_worker'),
|
||||
candidate('run-8'),
|
||||
{
|
||||
runId: 'run-9',
|
||||
attempts: [
|
||||
{ attemptId: 'one', executorType: 'local_process' },
|
||||
{ attemptId: 'two', executorType: 'local_process' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
async consume(attemptId) {
|
||||
consumed.push(attemptId);
|
||||
if (attemptId === 'failed') throw new Error('invalid receipt');
|
||||
if (attemptId === 'missing') {
|
||||
return { status: 'missing', cleaned: false };
|
||||
}
|
||||
if (attemptId === 'terminal') {
|
||||
return { status: 'already_terminal', cleaned: true };
|
||||
}
|
||||
if (attemptId === 'quarantined') {
|
||||
return {
|
||||
status: 'quarantined',
|
||||
cleaned: true,
|
||||
quarantineRef: '.quarantine/quarantined.json',
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 'applied',
|
||||
cleaned: attemptId !== 'cleanup-pending',
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(await scanner.scanBatch({ limit: 8 }), {
|
||||
scanned: 9,
|
||||
applied: 2,
|
||||
alreadyTerminal: 1,
|
||||
quarantined: 1,
|
||||
purgedQuarantines: 0,
|
||||
expiredMissing: 0,
|
||||
missing: 1,
|
||||
cleanupPending: 1,
|
||||
skipped: 1,
|
||||
ambiguous: 2,
|
||||
failed: 1,
|
||||
truncated: false,
|
||||
unsafeAttemptOverflow: false,
|
||||
});
|
||||
assert.deepEqual(consumed, [
|
||||
'applied',
|
||||
'terminal',
|
||||
'missing',
|
||||
'cleanup-pending',
|
||||
'failed',
|
||||
'quarantined',
|
||||
]);
|
||||
});
|
||||
|
||||
test('supervisor aggregates bounded pages and exposes unsafe pagination', async () => {
|
||||
const cursors = [];
|
||||
const responses = [
|
||||
scan({
|
||||
applied: 1,
|
||||
missing: 0,
|
||||
truncated: true,
|
||||
nextCursor: { createdAtMs: 10, runId: 'run-1' },
|
||||
}),
|
||||
scan({ alreadyTerminal: 1, cleanupPending: 1 }),
|
||||
];
|
||||
const supervisor = new PrimaryCompletionReceiptSupervisor({
|
||||
async scanBatch(options) {
|
||||
cursors.push(options.cursor);
|
||||
return responses.shift();
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await supervisor.run({ pageSize: 8, maxPages: 4 }), {
|
||||
pages: 2,
|
||||
scanned: 2,
|
||||
applied: 1,
|
||||
alreadyTerminal: 1,
|
||||
quarantined: 0,
|
||||
purgedQuarantines: 0,
|
||||
expiredMissing: 0,
|
||||
missing: 1,
|
||||
cleanupPending: 1,
|
||||
skipped: 0,
|
||||
ambiguous: 0,
|
||||
failed: 0,
|
||||
stopReason: 'complete',
|
||||
remaining: false,
|
||||
});
|
||||
assert.deepEqual(cursors, [undefined, { createdAtMs: 10, runId: 'run-1' }]);
|
||||
|
||||
const overflow = new PrimaryCompletionReceiptSupervisor({
|
||||
async scanBatch() {
|
||||
return scan({ unsafeAttemptOverflow: true, truncated: true });
|
||||
},
|
||||
});
|
||||
assert.equal((await overflow.run()).stopReason, 'unsafe_attempt_overflow');
|
||||
await assert.rejects(supervisor.run({ pageSize: 0 }), RangeError);
|
||||
await assert.rejects(supervisor.run({ maxPages: 65 }), RangeError);
|
||||
});
|
||||
|
||||
test('lifecycle is explicit, unrefed, and never overlaps a slow scan', async () => {
|
||||
const scheduled = [];
|
||||
const scheduler = {
|
||||
setTimeout(callback, delayMs) {
|
||||
const timer = {
|
||||
callback,
|
||||
delayMs,
|
||||
unrefCalled: false,
|
||||
unref() {
|
||||
this.unrefCalled = true;
|
||||
},
|
||||
};
|
||||
scheduled.push(timer);
|
||||
return timer;
|
||||
},
|
||||
clearTimeout(timer) {
|
||||
timer.cleared = true;
|
||||
},
|
||||
};
|
||||
let release;
|
||||
let runs = 0;
|
||||
const inFlight = new Promise((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const lifecycle = new PrimaryCompletionReceiptLifecycle(
|
||||
{
|
||||
async run() {
|
||||
runs += 1;
|
||||
await inFlight;
|
||||
return {};
|
||||
},
|
||||
},
|
||||
{ intervalMs: 1_000, scheduler },
|
||||
);
|
||||
|
||||
assert.equal(lifecycle.start(), true);
|
||||
assert.equal(lifecycle.start(), false);
|
||||
assert.equal(scheduled.length, 1);
|
||||
assert.equal(scheduled[0].unrefCalled, true);
|
||||
scheduled[0].callback();
|
||||
scheduled[0].callback();
|
||||
assert.equal(runs, 1);
|
||||
release();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
assert.equal(scheduled.length, 2);
|
||||
assert.equal(await lifecycle.stop(), 'drained');
|
||||
assert.equal(scheduled[1].cleared, true);
|
||||
});
|
||||
|
||||
test('lifecycle resumes after a page limit and resets after reaching the tail', async () => {
|
||||
const scheduled = [];
|
||||
const scheduler = {
|
||||
setTimeout(callback) {
|
||||
const timer = { callback, unref() {} };
|
||||
scheduled.push(timer);
|
||||
return timer;
|
||||
},
|
||||
clearTimeout() {},
|
||||
};
|
||||
const cursors = [];
|
||||
let runs = 0;
|
||||
const lifecycle = new PrimaryCompletionReceiptLifecycle(
|
||||
{
|
||||
async run(options) {
|
||||
runs += 1;
|
||||
cursors.push(options.cursor);
|
||||
if (runs === 1) {
|
||||
return {
|
||||
remaining: true,
|
||||
nextCursor: { createdAtMs: 10, runId: 'run-10' },
|
||||
};
|
||||
}
|
||||
return { remaining: false };
|
||||
},
|
||||
},
|
||||
{ intervalMs: 1_000, scheduler },
|
||||
);
|
||||
|
||||
lifecycle.start();
|
||||
scheduled.shift().callback();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
scheduled.shift().callback();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
scheduled.shift().callback();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await lifecycle.stop();
|
||||
assert.deepEqual(cursors, [
|
||||
undefined,
|
||||
{ createdAtMs: 10, runId: 'run-10' },
|
||||
undefined,
|
||||
]);
|
||||
});
|
||||
Reference in New Issue
Block a user