Files
qinglong/test/back/primaryRunOrchestrator.test.cjs

1184 lines
37 KiB
JavaScript

require('ts-node/register/transpile-only');
const assert = require('node:assert/strict');
const { afterEach, 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 {
runRetryPolicyMigration,
} = require('../../back/migrations/0011-run-retry-policy');
const { runMigrations } = require('../../back/migrations/runner');
const {
LegacySequelizeRunRepository,
} = require('../../back/runtime/adapters/legacy-sequelize/runRepository');
const {
LegacySequelizePrimaryRunIdempotencyLookup,
} = require('../../back/runtime/adapters/legacy-sequelize/primaryRunIdempotencyLookup');
const {
PrimaryRunDuplicateRequestError,
PrimaryRunIdempotencyUnavailableError,
PrimaryRunLaunchError,
PrimaryRunNotActiveError,
PrimaryRunOrchestrator,
PrimaryClaimedRunRejectedError,
PrimaryRunRetryPolicyAuthorityError,
} = require('../../back/runtime/application/primaryRunOrchestrator');
const {
InvalidRunRetryPolicyError,
} = require('../../back/runtime/domain/runRetryPolicy');
const {
PrimaryRunCreator,
} = require('../../back/runtime/application/primaryRunCreator');
const {
RunLostRetryService,
} = require('../../back/runtime/application/runLostRetryService');
const {
RunCommandService,
} = require('../../back/runtime/application/runCommandService');
const {
PrimaryRunCompletionService,
hashPrimaryCompletionToken,
} = require('../../back/runtime/application/primaryRunCompletionService');
const databases = [];
const BASE_TIME = 1_750_000_000_000;
let idSequence = 500;
function nextId() {
idSequence += 1;
return `019f70e0-0000-7000-8000-${String(idSequence).padStart(12, '0')}`;
}
function deferred() {
let resolve;
let reject;
const promise = new Promise((resolvePromise, rejectPromise) => {
resolve = resolvePromise;
reject = rejectPromise;
});
return { promise, resolve, reject };
}
async function createRepository() {
return (await createRepositoryWithDatabase()).repository;
}
async function createRepositoryWithDatabase() {
const database = new Sequelize({
dialect: 'sqlite',
storage: ':memory:',
logging: false,
});
const migrationModel = defineSchemaMigrationModel(database);
await runMigrations({
database,
migrationModel,
migrations: [
runSchemaMigration,
runCancellationRequestMigration,
runAttemptDeadlineMigration,
runRetryPolicyMigration,
],
logger: { info() {} },
});
databases.push(database);
return {
database,
repository: new LegacySequelizeRunRepository(database),
};
}
class FakeExecutor {
type = 'local_process';
starts = [];
stops = [];
completion = deferred();
startError = undefined;
handleOverrides = {};
onStart = async () => undefined;
capabilities() {
return {
timeout: true,
processGroupTermination: true,
workingDirectory: true,
isolatedEnvironment: true,
memoryLimit: 'none',
cpuLimit: 'none',
filesystemIsolation: 'none',
networkIsolation: 'none',
};
}
async start(spec, context) {
this.starts.push({ spec, context });
await this.onStart(spec, context);
if (this.startError) throw this.startError;
return {
id: nextId(),
executorType: this.type,
runId: spec.runId,
attemptId: spec.attemptId,
startedAtMs: BASE_TIME + 10,
pid: 4321,
completion: this.completion.promise,
...this.handleOverrides,
};
}
async stop(handle, reason) {
this.stops.push({ handle, reason });
return {
status: 'termination_requested',
termSignalSent: true,
killSignalSent: false,
};
}
async inspect() {
return { status: 'running' };
}
}
class FailingTransactionRepository {
transactionCount = 0;
constructor(repository, failAt) {
this.repository = repository;
this.failAt = failAt;
}
findRunById(runId) {
return this.repository.findRunById(runId);
}
findAttemptById(attemptId) {
return this.repository.findAttemptById(attemptId);
}
listEvents(runId, options) {
return this.repository.listEvents(runId, options);
}
transaction(work) {
this.transactionCount += 1;
if (this.transactionCount === this.failAt) {
return Promise.reject(new Error('injected transaction failure'));
}
return this.repository.transaction(work);
}
}
class CompletionConflictRepository {
conflict = false;
constructor(repository) {
this.repository = repository;
}
findRunById(runId) {
return this.repository.findRunById(runId);
}
findAttemptById(attemptId) {
return this.repository.findAttemptById(attemptId);
}
listEvents(runId, options) {
return this.repository.listEvents(runId, options);
}
listCancellationRequested(options) {
return this.repository.listCancellationRequested(options);
}
transaction(work) {
return this.repository.transaction((transaction) =>
work(
new Proxy(transaction, {
get: (target, property) => {
if (property === 'compareAndSetRun' && this.conflict) {
return async () => {
this.conflict = false;
return false;
};
}
const value = Reflect.get(target, property, target);
return typeof value === 'function' ? value.bind(target) : value;
},
}),
),
);
}
}
function startCommand(overrides = {}) {
return {
definition: {
projectId: 'default',
taskId: 'legacy-cron:7',
taskRevision: 'revision-7',
taskName: 'primary manual run',
legacyCronId: 7,
triggerType: 'manual',
executionOrigin: 'manual',
acceptedAtMs: BASE_TIME,
actor: { type: 'user', id: 'user:1' },
},
createSpec(reference) {
return {
runId: reference.run.id,
attemptId: reference.attempt.id,
projectId: reference.run.projectId,
taskId: reference.run.taskId,
taskRevision: reference.run.taskRevision,
command: { kind: 'argv', file: '/bin/true', args: [] },
environmentPolicy: 'isolated',
...(overrides.timeoutMs === undefined
? {}
: { timeoutMs: overrides.timeoutMs }),
terminationGraceMs: 100,
};
},
context: {
environment: {},
output: { async write() {} },
},
...overrides,
};
}
function result(outcome, overrides = {}) {
return {
outcome,
startedAtMs: BASE_TIME + 10,
finishedAtMs: BASE_TIME + 20,
...overrides,
};
}
afterEach(async () => {
await Promise.all(databases.splice(0).map((database) => database.close()));
});
test('persists only a trusted retry policy resolved from immutable task identity', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const requests = [];
executor.onStart = async (spec) => {
assert.deepEqual(await repository.findRetryPolicyByRunId(spec.runId), {
runId: spec.runId,
maxAttempts: 3,
retryOnLost: true,
safety: 'idempotent',
backoffBaseMs: 1_000,
backoffMaxMs: 30_000,
version: 0,
createdAtMs: BASE_TIME,
updatedAtMs: BASE_TIME,
});
};
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
retryPolicyAdmission: {
async resolve(request) {
requests.push(request);
assert.equal(Object.isFrozen(request), true);
return {
maxAttempts: 3,
retryOnLost: true,
safety: 'idempotent',
backoffBaseMs: 1_000,
backoffMaxMs: 30_000,
};
},
},
});
const active = await orchestrator.start(startCommand());
assert.deepEqual(requests, [
{
projectId: 'default',
taskId: 'legacy-cron:7',
taskRevision: 'revision-7',
triggerType: 'manual',
executionOrigin: 'manual',
},
]);
assert.equal('actor' in requests[0], false);
assert.equal('taskName' in requests[0], false);
executor.completion.resolve(result('succeeded', { exitCode: 0 }));
await active.completion;
});
test('creates no retry policy when no trusted admission provider is configured', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const active = await orchestrator.start(startCommand());
assert.equal(await repository.findRetryPolicyByRunId(active.run.id), null);
executor.completion.resolve(result('succeeded', { exitCode: 0 }));
await active.completion;
});
test('rejects request-supplied retry safety before creating or spawning a Run', async () => {
const { database, repository } = await createRepositoryWithDatabase();
const executor = new FakeExecutor();
const admissionRequests = [];
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
retryPolicyAdmission: {
async resolve(request) {
admissionRequests.push(request);
return undefined;
},
},
});
const command = startCommand();
command.definition.retryPolicy = {
maxAttempts: 3,
retryOnLost: true,
safety: 'idempotent',
backoffBaseMs: 1_000,
backoffMaxMs: 30_000,
};
await assert.rejects(
orchestrator.start(command),
PrimaryRunRetryPolicyAuthorityError,
);
assert.deepEqual(admissionRequests, []);
assert.equal(executor.starts.length, 0);
assert.equal(
Number(
(await database.query('SELECT COUNT(*) AS count FROM Runs'))[0][0].count,
),
0,
);
});
test('rejects unsafe admitted automatic retry before creating or spawning a Run', async () => {
const { database, repository } = await createRepositoryWithDatabase();
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
retryPolicyAdmission: {
async resolve() {
return {
maxAttempts: 2,
retryOnLost: true,
safety: 'unknown',
backoffBaseMs: 1_000,
backoffMaxMs: 30_000,
};
},
},
});
await assert.rejects(
orchestrator.start(startCommand()),
InvalidRunRetryPolicyError,
);
assert.equal(executor.starts.length, 0);
assert.equal(
Number(
(await database.query('SELECT COUNT(*) AS count FROM Runs'))[0][0].count,
),
0,
);
});
test('activates a recovered claimed Attempt N+1 without creating another Run', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
retryPolicyAdmission: {
async resolve() {
return {
maxAttempts: 2,
retryOnLost: true,
safety: 'idempotent',
backoffBaseMs: 0,
backoffMaxMs: 0,
};
},
},
});
const first = await orchestrator.start(startCommand());
executor.completion.resolve(result('lost'));
assert.equal((await first.completion).run.status, 'lost');
await new Promise((resolve) => setImmediate(resolve));
const retry = new RunLostRetryService(repository, {
clock: { now: () => BASE_TIME + 30 },
createId: nextId,
});
assert.equal((await retry.reconcile(first.run.id)).status, 'scheduled');
const requeued = await retry.reconcile(first.run.id);
assert.equal(requeued.status, 'requeued');
assert.equal(requeued.attempt.attempt, 2);
await assert.rejects(
orchestrator.activateClaimed({
runId: first.run.id,
attemptId: first.attempt.id,
createSpec: startCommand().createSpec,
context: { environment: {}, output: { async write() {} } },
}),
(error) =>
error instanceof PrimaryClaimedRunRejectedError &&
error.reason === 'not_latest_attempt',
);
assert.equal(executor.starts.length, 1);
executor.completion = deferred();
executor.onStart = async (spec, context) => {
assert.equal(spec.runId, first.run.id);
assert.equal(spec.attemptId, requeued.attempt.id);
assert.equal(context.completionCallback.callbackSequence, 1);
assert.equal(
(await repository.findAttemptById(first.attempt.id)).status,
'lost',
);
};
const active = await orchestrator.activateClaimed({
runId: first.run.id,
attemptId: requeued.attempt.id,
createSpec: startCommand().createSpec,
context: { environment: {}, output: { async write() {} } },
});
assert.equal(active.run.id, first.run.id);
assert.equal(active.attempt.id, requeued.attempt.id);
assert.equal(active.attempt.attempt, 2);
executor.completion.resolve(result('succeeded', { exitCode: 0 }));
const completed = await active.completion;
assert.equal(completed.run.status, 'succeeded');
assert.equal(completed.attempt.status, 'succeeded');
assert.equal(
(await repository.findLatestAttemptByRunId(first.run.id)).id,
requeued.attempt.id,
);
});
test('serializes claimed activation before spawn so only one caller can execute', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const reference = await new PrimaryRunCreator(repository, nextId).create(
{
projectId: 'default',
taskId: 'legacy-cron:7',
taskRevision: 'revision-7',
triggerType: 'manual',
executionOrigin: 'manual',
acceptedAtMs: BASE_TIME,
actor: { type: 'scheduler' },
},
executor.type,
);
const enteredSpawn = deferred();
const allowSpawn = deferred();
executor.onStart = async () => {
enteredSpawn.resolve();
await allowSpawn.promise;
};
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const command = {
runId: reference.run.id,
attemptId: reference.attempt.id,
createSpec: startCommand().createSpec,
context: { environment: {}, output: { async write() {} } },
};
const first = orchestrator.activateClaimed(command);
await enteredSpawn.promise;
await assert.rejects(
orchestrator.activateClaimed(command),
(error) =>
error instanceof PrimaryClaimedRunRejectedError &&
error.reason === 'not_queued',
);
assert.equal(executor.starts.length, 1);
allowSpawn.resolve();
const active = await first;
executor.completion.resolve(result('succeeded', { exitCode: 0 }));
await active.completion;
});
test('rejects a claimed Attempt owned by another executor before side effects', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const reference = await new PrimaryRunCreator(repository, nextId).create(
{
projectId: 'default',
taskId: 'task:remote',
taskRevision: 'revision-remote',
triggerType: 'manual',
executionOrigin: 'manual',
acceptedAtMs: BASE_TIME,
actor: { type: 'scheduler' },
},
'remote_worker',
);
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
await assert.rejects(
orchestrator.activateClaimed({
runId: reference.run.id,
attemptId: reference.attempt.id,
createSpec: startCommand().createSpec,
context: { environment: {}, output: { async write() {} } },
}),
(error) =>
error instanceof PrimaryClaimedRunRejectedError &&
error.reason === 'executor_mismatch',
);
assert.equal(executor.starts.length, 0);
assert.equal(
(await repository.findRunById(reference.run.id)).status,
'queued',
);
});
test('rejects stale execution authority on a claimed Attempt before side effects', async () => {
const persisted = await createRepository();
const executor = new FakeExecutor();
const reference = await new PrimaryRunCreator(persisted, nextId).create(
{
projectId: 'default',
taskId: 'task:contaminated',
taskRevision: 'revision-contaminated',
triggerType: 'manual',
executionOrigin: 'manual',
acceptedAtMs: BASE_TIME,
actor: { type: 'scheduler' },
},
executor.type,
);
const contaminated = {
...reference.attempt,
callbackTokenHash: 'a'.repeat(64),
};
const repository = {
findRunById: persisted.findRunById.bind(persisted),
async findAttemptById() {
return contaminated;
},
async findLatestAttemptByRunId() {
return contaminated;
},
findRetryPolicyByRunId: persisted.findRetryPolicyByRunId.bind(persisted),
listEvents: persisted.listEvents.bind(persisted),
listCancellationRequested:
persisted.listCancellationRequested.bind(persisted),
transaction: persisted.transaction.bind(persisted),
};
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
await assert.rejects(
orchestrator.activateClaimed({
runId: reference.run.id,
attemptId: reference.attempt.id,
createSpec: startCommand().createSpec,
context: { environment: {}, output: { async write() {} } },
}),
(error) =>
error instanceof PrimaryClaimedRunRejectedError &&
error.reason === 'stale_execution_authority',
);
assert.equal(executor.starts.length, 0);
});
test('persists the Primary Run and starting Attempt before spawning', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const registrations = [];
executor.onStart = async (spec, context) => {
const [run, attempt, events] = await Promise.all([
repository.findRunById(spec.runId),
repository.findAttemptById(spec.attemptId),
repository.listEvents(spec.runId),
]);
assert.equal(run.status, 'dispatching');
assert.equal(run.executionOwner, 'runtime');
assert.equal(attempt.status, 'starting');
assert.equal(attempt.deadlineAtMs, BASE_TIME + 5 + 5_000);
assert.equal(context.completionCallback.callbackSequence, 1);
assert.equal(
attempt.callbackTokenHash,
hashPrimaryCompletionToken(context.completionCallback.token),
);
assert.doesNotMatch(
JSON.stringify(attempt),
new RegExp(context.completionCallback.token),
);
assert.deepEqual(
events.map((event) => event.type),
['run.created', 'run.queued', 'run.dispatching', 'attempt.starting'],
);
assert.deepEqual(registrations, [
{
runId: spec.runId,
attemptId: spec.attemptId,
registeredAtMs: BASE_TIME,
},
]);
};
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
completionReceiptJournal: {
async register(command) {
registrations.push(command);
},
},
});
const active = await orchestrator.start(startCommand({ timeoutMs: 5_000 }));
assert.equal(orchestrator.isActive(active.run.id), true);
assert.equal(active.run.status, 'running');
assert.equal(active.attempt.status, 'running');
assert.equal(active.attempt.executorHandle, active.handle.id);
assert.equal(active.attempt.pid, 4321);
assert.equal(active.attempt.deadlineAtMs, BASE_TIME + 5 + 5_000);
executor.completion.resolve(result('succeeded', { exitCode: 0 }));
const completed = await active.completion;
assert.equal(completed.run.status, 'succeeded');
assert.equal(completed.attempt.status, 'succeeded');
assert.equal(completed.attempt.callbackSequence, 1);
assert.equal(orchestrator.isActive(active.run.id), false);
assert.deepEqual(
(await repository.listEvents(active.run.id)).map((event) => event.type),
[
'run.created',
'run.queued',
'run.dispatching',
'attempt.starting',
'attempt.running',
'run.running',
'attempt.succeeded',
'run.succeeded',
],
);
});
test('persists a claimed Attempt Artifact before spawn and validates it before state changes', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const reference = await new PrimaryRunCreator(repository, nextId).create(
{
projectId: 'default',
taskId: 'task:artifact',
taskRevision: 'revision-artifact',
triggerType: 'manual',
executionOrigin: 'manual',
acceptedAtMs: BASE_TIME,
actor: { type: 'scheduler' },
},
executor.type,
);
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const logArtifactId = `local-${'b'.repeat(30)}`;
executor.onStart = async () => {
const starting = await repository.findAttemptById(reference.attempt.id);
assert.equal(starting.status, 'starting');
assert.equal(starting.logArtifactId, logArtifactId);
};
await assert.rejects(
orchestrator.activateClaimed({
runId: reference.run.id,
attemptId: reference.attempt.id,
createSpec: startCommand().createSpec,
context: { environment: {}, output: { async write() {} } },
logArtifactId: '../escape',
}),
/logArtifactId is invalid/,
);
assert.equal(
(await repository.findRunById(reference.run.id)).status,
'queued',
);
assert.equal(executor.starts.length, 0);
const active = await orchestrator.activateClaimed({
runId: reference.run.id,
attemptId: reference.attempt.id,
createSpec: startCommand().createSpec,
context: { environment: {}, output: { async write() {} } },
logArtifactId,
});
assert.equal(active.attempt.logArtifactId, logArtifactId);
executor.completion.resolve(result('succeeded', { exitCode: 0 }));
const completed = await active.completion;
assert.equal(completed.attempt.logArtifactId, logArtifactId);
});
test('persists a durable Executor handle instead of its process-local id', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
executor.handleOverrides = { durableHandle: 'ql3lp1.test-durable-handle' };
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const active = await orchestrator.start(startCommand());
assert.equal(active.attempt.executorHandle, 'ql3lp1.test-durable-handle');
assert.notEqual(active.attempt.executorHandle, active.handle.id);
executor.completion.resolve(result('succeeded', { exitCode: 0 }));
await active.completion;
});
test('records a safe terminal failure when Executor.start rejects', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
executor.startError = new Error('secret command and path');
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
let launchError;
try {
await orchestrator.start(startCommand());
} catch (error) {
launchError = error;
}
assert.ok(launchError instanceof PrimaryRunLaunchError);
const run = await repository.findRunById(launchError.reference.run.id);
const attempt = await repository.findAttemptById(
launchError.reference.attempt.id,
);
assert.equal(run.status, 'failed');
assert.equal(attempt.status, 'failed');
assert.equal(run.errorCode, 'EXECUTOR_START_FAILED');
assert.doesNotMatch(run.errorSummary, /secret|path/);
assert.equal(orchestrator.isActive(run.id), false);
});
test('does not spawn when completion receipt journal registration fails', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
completionReceiptJournal: {
async register() {
throw new Error('journal unavailable');
},
},
});
let launchError;
try {
await orchestrator.start(startCommand());
} catch (error) {
launchError = error;
}
assert.ok(launchError instanceof PrimaryRunLaunchError);
assert.equal(executor.starts.length, 0);
assert.equal(
(await repository.findRunById(launchError.reference.run.id)).status,
'failed',
);
assert.equal(
(await repository.findAttemptById(launchError.reference.attempt.id)).status,
'failed',
);
});
test('cancels through the owning Executor and persists its eventual result', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const active = await orchestrator.start(startCommand());
const stopExecutor = executor.stop.bind(executor);
executor.stop = async (handle, reason) => {
const [run, events] = await Promise.all([
repository.findRunById(active.run.id),
repository.listEvents(active.run.id),
]);
assert.equal(run.cancelRequestedAtMs, BASE_TIME + 15);
assert.equal(run.cancelReason, 'user');
assert.equal(events.at(-1).type, 'run.cancel_requested');
return stopExecutor(handle, reason);
};
const stop = await active.cancel({
kind: 'user',
requestedAtMs: BASE_TIME + 15,
});
assert.equal(stop.status, 'termination_requested');
assert.equal(executor.stops.length, 1);
executor.completion.resolve(result('cancelled'));
const completed = await active.completion;
assert.equal(completed.run.status, 'cancelled');
assert.equal(completed.attempt.status, 'cancelled');
assert.deepEqual(
(await repository.listEvents(active.run.id)).map((event) => event.type),
[
'run.created',
'run.queued',
'run.dispatching',
'attempt.starting',
'attempt.running',
'run.running',
'run.cancel_requested',
'attempt.cancelled',
'run.cancelled',
],
);
await assert.rejects(
orchestrator.cancel(active.run.id, {
kind: 'user',
requestedAtMs: BASE_TIME + 30,
}),
PrimaryRunNotActiveError,
);
});
test('keeps the first cancel request and lets it win over a late success', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const active = await orchestrator.start(startCommand());
await active.cancel({ kind: 'user', requestedAtMs: BASE_TIME + 15 });
await active.cancel({ kind: 'policy', requestedAtMs: BASE_TIME + 16 });
assert.equal(executor.stops.length, 2);
const requested = await repository.findRunById(active.run.id);
assert.equal(requested.cancelRequestedAtMs, BASE_TIME + 15);
assert.equal(requested.cancelReason, 'user');
assert.equal(
(await repository.listEvents(active.run.id)).filter(
(event) => event.type === 'run.cancel_requested',
).length,
1,
);
executor.completion.resolve(result('succeeded', { exitCode: 0 }));
const completed = await active.completion;
assert.equal(completed.result.outcome, 'succeeded');
assert.equal(completed.run.status, 'cancelled');
assert.equal(completed.attempt.status, 'cancelled');
assert.equal(completed.attempt.exitCode, 0);
});
test('retries the atomic completion transaction after a CAS conflict', async () => {
const persisted = await createRepository();
const repository = new CompletionConflictRepository(persisted);
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const active = await orchestrator.start(startCommand());
repository.conflict = true;
executor.completion.resolve(result('succeeded', { exitCode: 0 }));
const completed = await active.completion;
assert.equal(completed.result.outcome, 'succeeded');
assert.equal(completed.run.status, 'succeeded');
assert.equal(completed.attempt.status, 'succeeded');
assert.equal(
(await persisted.listEvents(active.run.id)).filter(
(event) => event.type === 'attempt.succeeded',
).length,
1,
);
});
test('does not signal the Executor when persisting cancellation fails', async () => {
const persisted = await createRepository();
const repository = new FailingTransactionRepository(persisted, 6);
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const active = await orchestrator.start(startCommand());
await assert.rejects(
active.cancel({ kind: 'user', requestedAtMs: BASE_TIME + 15 }),
/injected transaction failure/,
);
assert.equal(executor.stops.length, 0);
const run = await persisted.findRunById(active.run.id);
assert.equal(run.cancelRequestedAtMs, undefined);
assert.equal(
(await persisted.listEvents(active.run.id)).some(
(event) => event.type === 'run.cancel_requested',
),
false,
);
executor.completion.resolve(result('succeeded', { exitCode: 0 }));
assert.equal((await active.completion).run.status, 'succeeded');
});
test('does not signal after the Attempt completed before cancellation', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const active = await orchestrator.start(startCommand());
await new PrimaryRunCompletionService(repository, nextId).complete({
runId: active.run.id,
attemptId: active.attempt.id,
callbackSequence: 1,
result: result('succeeded', { exitCode: 0 }),
source: { kind: 'executor', executorType: executor.type },
});
const stop = await active.cancel({
kind: 'user',
requestedAtMs: BASE_TIME + 21,
});
assert.deepEqual(stop, {
status: 'already_exited',
termSignalSent: false,
killSignalSent: false,
});
assert.equal(executor.stops.length, 0);
assert.equal(
(await repository.listEvents(active.run.id)).some(
(event) => event.type === 'run.cancel_requested',
),
false,
);
executor.completion.resolve(result('succeeded', { exitCode: 0 }));
assert.equal((await active.completion).run.status, 'succeeded');
});
test('compensates a mismatched spawned handle with stop and lost state', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
executor.handleOverrides = { attemptId: nextId() };
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
let launchError;
try {
await orchestrator.start(startCommand());
} catch (error) {
launchError = error;
}
assert.ok(launchError instanceof PrimaryRunLaunchError);
assert.equal(executor.stops.length, 1);
assert.equal(executor.stops[0].reason.kind, 'reconcile');
assert.equal(launchError.reference.run.status, 'lost');
assert.equal(launchError.reference.attempt.status, 'lost');
});
test('stops the process when persisting running ownership fails', async () => {
const persisted = await createRepository();
// create, dispatching, starting, attempt.running, then run.running fails.
const repository = new FailingTransactionRepository(persisted, 5);
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
let launchError;
try {
await orchestrator.start(startCommand());
} catch (error) {
launchError = error;
}
assert.ok(launchError instanceof PrimaryRunLaunchError);
assert.equal(executor.stops.length, 1);
assert.equal(executor.stops[0].reason.kind, 'reconcile');
assert.equal(launchError.reference.run.status, 'lost');
assert.equal(launchError.reference.attempt.status, 'lost');
assert.equal(repository.transactionCount, 7);
});
test('turns a rejected Executor completion channel into a durable lost state', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const active = await orchestrator.start(startCommand());
executor.completion.reject(new Error('raw completion secret'));
const completed = await active.completion;
assert.equal(completed.run.status, 'lost');
assert.equal(completed.attempt.status, 'lost');
assert.equal(completed.run.errorCode, 'EXECUTION_LOST');
assert.doesNotMatch(completed.run.errorSummary, /raw|secret/);
assert.equal(orchestrator.isActive(active.run.id), false);
});
test('maps every non-success Executor outcome without persisting raw details', async () => {
const cases = [
['failed', 'failed', 'EXECUTION_FAILED'],
['timed_out', 'timed_out', 'EXECUTION_TIMED_OUT'],
['lost', 'lost', 'EXECUTION_LOST'],
];
for (const [outcome, status, errorCode] of cases) {
const repository = await createRepository();
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const active = await orchestrator.start(startCommand());
executor.completion.resolve(
result(outcome, {
errorCode: 'UNTRUSTED_CODE',
errorSummary: 'raw executor secret',
}),
);
const completed = await active.completion;
assert.equal(completed.run.status, status);
assert.equal(completed.attempt.status, status);
assert.equal(completed.run.errorCode, errorCode);
assert.doesNotMatch(completed.run.errorSummary, /raw|secret/);
}
});
test('requires an explicit lookup when a Primary idempotency key is supplied', async () => {
const repository = await createRepository();
const executor = new FakeExecutor();
const orchestrator = new PrimaryRunOrchestrator(repository, executor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
});
const command = startCommand();
command.definition.idempotencyKey = 'manual-request-7';
await assert.rejects(
orchestrator.start(command),
PrimaryRunIdempotencyUnavailableError,
);
assert.equal(executor.starts.length, 0);
});
test('returns the existing Run for a duplicate idempotent request without spawning', async () => {
const { database, repository } = await createRepositoryWithDatabase();
const lookup = new LegacySequelizePrimaryRunIdempotencyLookup(database);
const firstExecutor = new FakeExecutor();
const first = new PrimaryRunOrchestrator(repository, firstExecutor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
idempotencyLookup: lookup,
});
const command = startCommand();
command.definition.idempotencyKey = 'manual-request-7';
const active = await first.start(command);
const secondExecutor = new FakeExecutor();
const second = new PrimaryRunOrchestrator(repository, secondExecutor, {
clock: { now: () => BASE_TIME + 6 },
createId: nextId,
idempotencyLookup: lookup,
});
let duplicate;
try {
await second.start(command);
} catch (error) {
duplicate = error;
}
assert.ok(duplicate instanceof PrimaryRunDuplicateRequestError);
assert.equal(duplicate.existingRunId, active.run.id);
assert.equal(secondExecutor.starts.length, 0);
firstExecutor.completion.resolve(result('succeeded', { exitCode: 0 }));
await active.completion;
});
test('uses the unique index to close an idempotency preflight race', async () => {
const { database, repository } = await createRepositoryWithDatabase();
const lookup = new LegacySequelizePrimaryRunIdempotencyLookup(database);
const firstExecutor = new FakeExecutor();
const first = new PrimaryRunOrchestrator(repository, firstExecutor, {
clock: { now: () => BASE_TIME + 5 },
createId: nextId,
idempotencyLookup: lookup,
});
const command = startCommand();
command.definition.idempotencyKey = 'racing-request-7';
const active = await first.start(command);
let lookupCalls = 0;
const staleOnceLookup = {
async findRunId(projectId, idempotencyKey) {
lookupCalls += 1;
if (lookupCalls === 1) return null;
return lookup.findRunId(projectId, idempotencyKey);
},
};
const secondExecutor = new FakeExecutor();
const second = new PrimaryRunOrchestrator(repository, secondExecutor, {
clock: { now: () => BASE_TIME + 6 },
createId: nextId,
idempotencyLookup: staleOnceLookup,
});
let duplicate;
try {
await second.start(command);
} catch (error) {
duplicate = error;
}
assert.ok(duplicate instanceof PrimaryRunDuplicateRequestError);
assert.equal(duplicate.existingRunId, active.run.id);
assert.equal(lookupCalls, 2);
assert.equal(secondExecutor.starts.length, 0);
firstExecutor.completion.resolve(result('succeeded', { exitCode: 0 }));
await active.completion;
});