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,290 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WorkerRemoteExecutionHeadlessLifecycle,
|
||||
} = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
|
||||
const session = Object.freeze({
|
||||
workerId: 'edge-1',
|
||||
sessionId: '018f0000-0000-7000-8000-000000000001',
|
||||
generation: 2,
|
||||
status: 'available',
|
||||
leaseExpiresAtMs: 20_000,
|
||||
});
|
||||
|
||||
function record(offerId, state) {
|
||||
return { state, offer: { offerId } };
|
||||
}
|
||||
|
||||
function fixture(overrides = {}) {
|
||||
const calls = [];
|
||||
const journal = overrides.journal ?? {
|
||||
async acquireOwnership() { calls.push('acquire'); },
|
||||
async releaseOwnership() { calls.push('release'); },
|
||||
async listOffers() { calls.push('list'); return { records: [] }; },
|
||||
};
|
||||
const offers = overrides.offers ?? {
|
||||
async pull() {
|
||||
calls.push('pull');
|
||||
return {
|
||||
status: 'idle',
|
||||
reason: 'no_candidates',
|
||||
stats: {
|
||||
pages: 0,
|
||||
candidates: 0,
|
||||
plansUnavailable: 0,
|
||||
placementMismatches: 0,
|
||||
claimAttempts: 0,
|
||||
claimRaces: 0,
|
||||
},
|
||||
truncated: false,
|
||||
};
|
||||
},
|
||||
};
|
||||
const processor = overrides.processor ?? {
|
||||
async process(offerId) {
|
||||
calls.push(`process:${offerId}`);
|
||||
return { status: 'running', offerId, executorHandle: `handle:${offerId}` };
|
||||
},
|
||||
};
|
||||
const control = overrides.control ?? {
|
||||
async reconcile(offerId) {
|
||||
calls.push(`control:${offerId}`);
|
||||
return { status: 'renewed', offerId, leaseVersion: 1, expiresAtMs: 30_000 };
|
||||
},
|
||||
};
|
||||
const lifecycle = new WorkerRemoteExecutionHeadlessLifecycle({
|
||||
journal,
|
||||
offers,
|
||||
processor,
|
||||
control,
|
||||
currentSession: overrides.currentSession ?? (() => session),
|
||||
maximumRecordsPerTick: overrides.maximumRecordsPerTick ?? 2,
|
||||
now: () => 10_000,
|
||||
});
|
||||
return { calls, lifecycle };
|
||||
}
|
||||
|
||||
test('is inert until explicit start and releases the single journal owner', async () => {
|
||||
const { calls, lifecycle } = fixture();
|
||||
assert.deepEqual(calls, []);
|
||||
await assert.rejects(lifecycle.tick(), /inactive/);
|
||||
assert.equal(await lifecycle.start(), 'started');
|
||||
assert.equal(await lifecycle.start(), 'already_started');
|
||||
assert.deepEqual(calls, ['acquire']);
|
||||
assert.deepEqual(await lifecycle.tick(), {
|
||||
status: 'reconciled',
|
||||
processed: 0,
|
||||
});
|
||||
await lifecycle.stop();
|
||||
assert.deepEqual(calls, ['acquire', 'list', 'release']);
|
||||
await assert.rejects(lifecycle.tick(), /inactive/);
|
||||
});
|
||||
|
||||
test('finishes bounded startup reconciliation before pulling and processing', async () => {
|
||||
const pages = [
|
||||
{
|
||||
records: [record('offer-1', 'accepted'), record('offer-2', 'running_acknowledged')],
|
||||
nextAfterOfferId: 'offer-2',
|
||||
},
|
||||
{ records: [record('offer-3', 'completion_acknowledged')] },
|
||||
];
|
||||
const { calls, lifecycle } = fixture({
|
||||
journal: {
|
||||
async acquireOwnership() { calls.push('acquire'); },
|
||||
async releaseOwnership() { calls.push('release'); },
|
||||
async listOffers() { calls.push('list'); return pages.shift() ?? { records: [] }; },
|
||||
},
|
||||
offers: {
|
||||
async pull() {
|
||||
calls.push('pull');
|
||||
return {
|
||||
status: 'accepted',
|
||||
offerId: 'offer-new',
|
||||
stats: {
|
||||
pages: 1,
|
||||
candidates: 1,
|
||||
plansUnavailable: 0,
|
||||
placementMismatches: 0,
|
||||
claimAttempts: 1,
|
||||
claimRaces: 0,
|
||||
},
|
||||
truncated: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
await lifecycle.start();
|
||||
assert.deepEqual(await lifecycle.tick(), {
|
||||
status: 'reconciling',
|
||||
processed: 1,
|
||||
nextAfterOfferId: 'offer-2',
|
||||
});
|
||||
assert.equal(calls.includes('pull'), false);
|
||||
assert.deepEqual(await lifecycle.tick(), {
|
||||
status: 'reconciled',
|
||||
processed: 0,
|
||||
});
|
||||
const pulled = await lifecycle.tick();
|
||||
assert.equal(pulled.status, 'processed');
|
||||
assert.equal(pulled.offerId, 'offer-new');
|
||||
assert.deepEqual(calls.filter((call) => call.startsWith('process:')), [
|
||||
'process:offer-1',
|
||||
'process:offer-new',
|
||||
]);
|
||||
await lifecycle.stop();
|
||||
});
|
||||
|
||||
test('supervises a bounded active page before pulling new work', async () => {
|
||||
let lists = 0;
|
||||
const { calls, lifecycle } = fixture({
|
||||
journal: {
|
||||
async acquireOwnership() { calls.push('acquire'); },
|
||||
async releaseOwnership() { calls.push('release'); },
|
||||
async listOffers() {
|
||||
lists += 1;
|
||||
if (lists === 1) return { records: [] };
|
||||
return {
|
||||
records: [
|
||||
record('offer-running', 'running_acknowledged'),
|
||||
record('offer-complete', 'completion_acknowledged'),
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
await lifecycle.start();
|
||||
await lifecycle.tick();
|
||||
const result = await lifecycle.tick();
|
||||
assert.equal(result.status, 'pull_result');
|
||||
assert.deepEqual(calls.slice(-2), ['control:offer-running', 'pull']);
|
||||
await lifecycle.stop();
|
||||
});
|
||||
|
||||
test('fails closed before Pull when active supervision records lease loss', async () => {
|
||||
let lists = 0;
|
||||
const { calls, lifecycle } = fixture({
|
||||
journal: {
|
||||
async acquireOwnership() { calls.push('acquire'); },
|
||||
async releaseOwnership() { calls.push('release'); },
|
||||
async listOffers() {
|
||||
lists += 1;
|
||||
return lists === 1
|
||||
? { records: [] }
|
||||
: { records: [record('offer-lost', 'running_acknowledged')] };
|
||||
},
|
||||
},
|
||||
control: {
|
||||
async reconcile(offerId) {
|
||||
calls.push(`control:${offerId}`);
|
||||
return {
|
||||
status: 'lease_expired', offerId,
|
||||
stop: { status: 'stopped', signal: 'SIGTERM' },
|
||||
recoveryReason: 'lease_lost_local_execution_stopped',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
await lifecycle.start();
|
||||
await lifecycle.tick();
|
||||
assert.deepEqual(await lifecycle.tick(), {
|
||||
status: 'recovery_required', offerId: 'offer-lost',
|
||||
});
|
||||
assert.equal(calls.includes('pull'), false);
|
||||
assert.deepEqual(await lifecycle.tick(), {
|
||||
status: 'recovery_required', offerId: 'offer-lost',
|
||||
});
|
||||
await lifecycle.stop();
|
||||
});
|
||||
|
||||
test('fails closed on durable recovery evidence and never pulls again', async () => {
|
||||
const { calls, lifecycle } = fixture({
|
||||
journal: {
|
||||
async acquireOwnership() { calls.push('acquire'); },
|
||||
async releaseOwnership() { calls.push('release'); },
|
||||
async listOffers() {
|
||||
calls.push('list');
|
||||
return { records: [record('offer-unsafe', 'recovery_required')] };
|
||||
},
|
||||
},
|
||||
});
|
||||
await lifecycle.start();
|
||||
assert.deepEqual(await lifecycle.tick(), {
|
||||
status: 'recovery_required',
|
||||
offerId: 'offer-unsafe',
|
||||
});
|
||||
assert.deepEqual(await lifecycle.tick(), {
|
||||
status: 'recovery_required',
|
||||
offerId: 'offer-unsafe',
|
||||
});
|
||||
assert.equal(calls.includes('pull'), false);
|
||||
await lifecycle.stop();
|
||||
});
|
||||
|
||||
test('coalesces ticks and aborts a pending pull before releasing ownership', async () => {
|
||||
const events = [];
|
||||
const { lifecycle } = fixture({
|
||||
journal: {
|
||||
async acquireOwnership() { events.push('acquire'); },
|
||||
async releaseOwnership() { events.push('release'); },
|
||||
async listOffers() { return { records: [] }; },
|
||||
},
|
||||
offers: {
|
||||
pull(_session, signal) {
|
||||
events.push('pull');
|
||||
return new Promise((resolve, reject) => {
|
||||
signal.addEventListener('abort', () => {
|
||||
events.push('aborted');
|
||||
reject(signal.reason);
|
||||
}, { once: true });
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
await lifecycle.start();
|
||||
await lifecycle.tick();
|
||||
const first = lifecycle.tick();
|
||||
const second = lifecycle.tick();
|
||||
assert.strictEqual(first, second);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
const stopping = lifecycle.stop();
|
||||
await assert.rejects(first, /stopping/);
|
||||
await stopping;
|
||||
assert.deepEqual(events, ['acquire', 'pull', 'aborted', 'release']);
|
||||
});
|
||||
|
||||
test('draining aborts Pull but keeps ownership until final stop', async () => {
|
||||
const events = [];
|
||||
const { lifecycle } = fixture({
|
||||
journal: {
|
||||
async acquireOwnership() { events.push('acquire'); },
|
||||
async releaseOwnership() { events.push('release'); },
|
||||
async listOffers() { events.push('list'); return { records: [] }; },
|
||||
},
|
||||
offers: {
|
||||
pull(_session, signal) {
|
||||
events.push('pull');
|
||||
return new Promise((resolve, reject) => {
|
||||
signal.addEventListener('abort', () => {
|
||||
events.push('aborted');
|
||||
reject(signal.reason);
|
||||
}, { once: true });
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
await lifecycle.start();
|
||||
await lifecycle.tick();
|
||||
const pulling = lifecycle.tick();
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
await lifecycle.beginDrain();
|
||||
await assert.rejects(pulling, /draining/);
|
||||
assert.equal(events.includes('release'), false);
|
||||
assert.deepEqual(await lifecycle.tick(), { status: 'draining' });
|
||||
assert.equal(events.filter((event) => event === 'pull').length, 1);
|
||||
await lifecycle.beginDrain();
|
||||
await lifecycle.stop();
|
||||
assert.equal(events.at(-1), 'release');
|
||||
});
|
||||
Reference in New Issue
Block a user