mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
271 lines
8.0 KiB
JavaScript
271 lines
8.0 KiB
JavaScript
require('ts-node/register/transpile-only');
|
|
|
|
const assert = require('node:assert/strict');
|
|
const { 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 {
|
|
workerRegistryMigration,
|
|
} = require('../../back/migrations/0008-worker-registry');
|
|
const {
|
|
runDispatchLeaseMigration,
|
|
} = require('../../back/migrations/0009-run-dispatch-lease');
|
|
const {
|
|
RUN_DISPATCH_CANDIDATE_ATTEMPT_INDEX,
|
|
RUN_DISPATCH_CANDIDATE_RUN_INDEX,
|
|
runDispatchCandidateMigration,
|
|
} = require('../../back/migrations/0010-run-dispatch-candidates');
|
|
const { runMigrations } = require('../../back/migrations/runner');
|
|
const {
|
|
LegacySequelizeRunDispatchCandidateSource,
|
|
} = require('../../back/runtime/adapters/legacy-sequelize/runDispatchCandidateSource');
|
|
const {
|
|
LegacySequelizeRunDispatchLeaseRepository,
|
|
} = require('../../back/runtime/adapters/legacy-sequelize/runDispatchLeaseRepository');
|
|
const {
|
|
LegacySequelizeRunRepository,
|
|
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
|
|
const {
|
|
LegacySequelizeWorkerRegistryRepository,
|
|
} = require('../../back/runtime/adapters/legacy-sequelize/workerRegistryRepository');
|
|
const {
|
|
PrimaryRunCreator,
|
|
} = require('../../back/runtime/application/primaryRunCreator');
|
|
const {
|
|
RunCommandService,
|
|
} = require('../../back/runtime/application/runCommandService');
|
|
const {
|
|
RunDispatchLeaseService,
|
|
} = require('../../back/runtime/application/runDispatchLeaseService');
|
|
const {
|
|
BoundWorkerControlPlaneClient,
|
|
WorkerControlService,
|
|
} = require('../../back/runtime/application/workerControlService');
|
|
|
|
const START = 1_760_000_000_000;
|
|
const SESSION = '019f7a00-0000-7000-8000-000000000001';
|
|
const TOKEN = 'candidate_lease_token_abcdefghijklmnopqrstuvwxyz0123456789';
|
|
let idSequence = 2_000;
|
|
|
|
function nextId() {
|
|
idSequence += 1;
|
|
return `019f7a00-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
|
|
}
|
|
|
|
async function fixture(t) {
|
|
const database = new Sequelize({
|
|
dialect: 'sqlite',
|
|
storage: ':memory:',
|
|
logging: false,
|
|
});
|
|
t.after(() => database.close());
|
|
await runMigrations({
|
|
database,
|
|
migrationModel: defineSchemaMigrationModel(database),
|
|
migrations: [
|
|
runSchemaMigration,
|
|
runCancellationRequestMigration,
|
|
runAttemptDeadlineMigration,
|
|
workerRegistryMigration,
|
|
runDispatchLeaseMigration,
|
|
runDispatchCandidateMigration,
|
|
],
|
|
logger: { info() {} },
|
|
});
|
|
const runs = new LegacySequelizeRunRepository(database);
|
|
const source = new LegacySequelizeRunDispatchCandidateSource(database);
|
|
let nowMs = START;
|
|
return {
|
|
database,
|
|
runs,
|
|
source,
|
|
setNow(value) {
|
|
nowMs = value;
|
|
},
|
|
getNow() {
|
|
return nowMs;
|
|
},
|
|
};
|
|
}
|
|
|
|
async function createRun(context, { priority, atMs }) {
|
|
return new PrimaryRunCreator(context.runs, nextId).create(
|
|
{
|
|
projectId: 'default',
|
|
taskId: `candidate:${nextId()}`,
|
|
taskRevision: 'candidate-v1',
|
|
triggerType: 'manual',
|
|
executionOrigin: 'manual',
|
|
priority,
|
|
acceptedAtMs: atMs,
|
|
actor: { type: 'user', id: 'user:1' },
|
|
},
|
|
'remote_worker',
|
|
);
|
|
}
|
|
|
|
function cursor(candidate) {
|
|
return {
|
|
priority: candidate.priority,
|
|
queuedAtMs: candidate.queuedAtMs,
|
|
attemptCreatedAtMs: candidate.attemptCreatedAtMs,
|
|
attemptId: candidate.attemptId,
|
|
};
|
|
}
|
|
|
|
test('lists a bounded, stable priority/FIFO candidate page', async (t) => {
|
|
const context = await fixture(t);
|
|
const low = await createRun(context, { priority: 0, atMs: START });
|
|
const highOld = await createRun(context, { priority: 10, atMs: START + 1 });
|
|
const highNew = await createRun(context, { priority: 10, atMs: START + 2 });
|
|
|
|
const first = await context.source.listCandidates({
|
|
observedAtMs: START + 10,
|
|
limit: 2,
|
|
});
|
|
assert.deepEqual(
|
|
first.map((candidate) => candidate.runId),
|
|
[highOld.run.id, highNew.run.id],
|
|
);
|
|
const second = await context.source.listCandidates({
|
|
observedAtMs: START + 10,
|
|
after: cursor(first[1]),
|
|
limit: 2,
|
|
});
|
|
assert.deepEqual(
|
|
second.map((candidate) => candidate.runId),
|
|
[low.run.id],
|
|
);
|
|
assert.equal(first[0].executorType, 'remote_worker');
|
|
assert.deepEqual(
|
|
{
|
|
projectId: first[0].projectId,
|
|
taskId: first[0].taskId,
|
|
taskRevision: first[0].taskRevision,
|
|
},
|
|
{
|
|
projectId: highOld.run.projectId,
|
|
taskId: highOld.run.taskId,
|
|
taskRevision: highOld.run.taskRevision,
|
|
},
|
|
);
|
|
await assert.rejects(
|
|
context.source.listCandidates({ observedAtMs: START, limit: 65 }),
|
|
/between 1 and 64/,
|
|
);
|
|
});
|
|
|
|
test('discovery never grants ownership and only expired/released claims reappear', async (t) => {
|
|
const context = await fixture(t);
|
|
const reference = await createRun(context, { priority: 1, atMs: START });
|
|
const workerService = new WorkerControlService(
|
|
new LegacySequelizeWorkerRegistryRepository(context.database),
|
|
{ leaseDurationMs: 60_000, clock: { now: () => context.getNow() } },
|
|
);
|
|
const worker = await new BoundWorkerControlPlaneClient(workerService, {
|
|
workerId: 'worker-a',
|
|
}).register({
|
|
workerId: 'worker-a',
|
|
sessionId: SESSION,
|
|
capabilities: {
|
|
architecture: 'arm64',
|
|
operatingSystem: 'linux',
|
|
executors: ['remote_worker'],
|
|
runtimes: [{ name: 'node', version: '24.14.0' }],
|
|
labels: {},
|
|
capacity: { cpuCores: 1, memoryBytes: 256 * 1024 * 1024 },
|
|
features: [],
|
|
},
|
|
maxConcurrentRuns: 1,
|
|
availableSlots: 1,
|
|
});
|
|
const leaseService = new RunDispatchLeaseService(
|
|
new LegacySequelizeRunDispatchLeaseRepository(context.database),
|
|
{
|
|
leaseDurationMs: 5_000,
|
|
clock: { now: () => context.getNow() },
|
|
createEventId: nextId,
|
|
},
|
|
);
|
|
assert.equal(
|
|
(await context.source.listCandidates({ observedAtMs: START })).length,
|
|
1,
|
|
);
|
|
const claimed = await leaseService.claim(
|
|
{ workerId: worker.id },
|
|
{
|
|
runId: reference.run.id,
|
|
attemptId: reference.attempt.id,
|
|
workerId: worker.id,
|
|
workerSessionId: worker.sessionId,
|
|
workerGeneration: worker.generation,
|
|
leaseToken: TOKEN,
|
|
},
|
|
);
|
|
assert.equal(claimed.status, 'claimed');
|
|
assert.deepEqual(
|
|
await context.source.listCandidates({ observedAtMs: START + 4_999 }),
|
|
[],
|
|
);
|
|
context.setNow(START + 5_000);
|
|
assert.deepEqual(
|
|
(await context.source.listCandidates({ observedAtMs: START + 5_000 })).map(
|
|
(candidate) => candidate.attemptId,
|
|
),
|
|
[reference.attempt.id],
|
|
);
|
|
});
|
|
|
|
test('excludes cancellation requests and owns supporting indexes', async (t) => {
|
|
const context = await fixture(t);
|
|
const reference = await createRun(context, { priority: 1, atMs: START });
|
|
const commands = new RunCommandService(context.runs, nextId);
|
|
await commands.requestCancellation({
|
|
runId: reference.run.id,
|
|
attemptId: reference.attempt.id,
|
|
expectedVersion: reference.run.version,
|
|
reason: 'user',
|
|
atMs: START + 1,
|
|
actor: { type: 'user', id: 'user:1' },
|
|
});
|
|
assert.deepEqual(
|
|
await context.source.listCandidates({ observedAtMs: START + 1 }),
|
|
[],
|
|
);
|
|
const runIndexes = await context.database
|
|
.getQueryInterface()
|
|
.showIndex('Runs');
|
|
const attemptIndexes = await context.database
|
|
.getQueryInterface()
|
|
.showIndex('RunAttempts');
|
|
assert.ok(
|
|
runIndexes.some((index) => index.name === RUN_DISPATCH_CANDIDATE_RUN_INDEX),
|
|
);
|
|
assert.ok(
|
|
attemptIndexes.some(
|
|
(index) => index.name === RUN_DISPATCH_CANDIDATE_ATTEMPT_INDEX,
|
|
),
|
|
);
|
|
});
|
|
|
|
test('rejects accidental cluster-control use of the SQLite source', () => {
|
|
assert.throws(
|
|
() =>
|
|
new LegacySequelizeRunDispatchCandidateSource({
|
|
getDialect() {
|
|
return 'postgres';
|
|
},
|
|
}),
|
|
/SQLite-only/,
|
|
);
|
|
});
|