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,127 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { createHash, X509Certificate } = require('node:crypto');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
generateWorkerCertificateEnrollment,
|
||||
} = require('../dist/credential/workerCertificateEnrollment');
|
||||
const {
|
||||
validateWorkerCertificateIdentity,
|
||||
} = require('../dist/credential/workerCertificateIdentity');
|
||||
const {
|
||||
createCertificateAuthority,
|
||||
} = require('./helpers/certificateAuthority.cjs');
|
||||
|
||||
const HOUR_MS = 60 * 60_000;
|
||||
|
||||
async function identityFixture(options = {}) {
|
||||
const now = options.now ?? Date.now();
|
||||
const ca = await createCertificateAuthority({ now });
|
||||
const enrollment = await generateWorkerCertificateEnrollment({
|
||||
workerId: 'worker-identity-01',
|
||||
});
|
||||
const certificateChainPem = await ca.issue(
|
||||
enrollment.certificateSigningRequestPem,
|
||||
options.issue,
|
||||
);
|
||||
return { now, ca, enrollment, certificateChainPem };
|
||||
}
|
||||
|
||||
test('validates the leaf identity independently of PEM chain formatting', async () => {
|
||||
const fixture = await identityFixture();
|
||||
try {
|
||||
const summary = validateWorkerCertificateIdentity({
|
||||
privateKeyPem: fixture.enrollment.privateKeyPem,
|
||||
certificateChainPem: `${fixture.certificateChainPem}\n`,
|
||||
trustAnchors: [fixture.ca.certificatePem],
|
||||
now: fixture.now,
|
||||
minimumRemainingValidityMs: HOUR_MS,
|
||||
});
|
||||
const leaf = new X509Certificate(fixture.certificateChainPem);
|
||||
|
||||
assert.equal(
|
||||
summary.certificateSha256,
|
||||
createHash('sha256').update(leaf.raw).digest('hex'),
|
||||
);
|
||||
assert.equal(
|
||||
summary.publicKeySpkiSha256,
|
||||
fixture.enrollment.publicKeySpkiSha256,
|
||||
);
|
||||
} finally {
|
||||
fixture.enrollment.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('fails closed for an untrusted issuer', async () => {
|
||||
const fixture = await identityFixture();
|
||||
const otherCa = await createCertificateAuthority({ now: fixture.now });
|
||||
try {
|
||||
assert.throws(
|
||||
() =>
|
||||
validateWorkerCertificateIdentity({
|
||||
privateKeyPem: fixture.enrollment.privateKeyPem,
|
||||
certificateChainPem: fixture.certificateChainPem,
|
||||
trustAnchors: [otherCa.certificatePem],
|
||||
now: fixture.now,
|
||||
}),
|
||||
(error) => error.reason === 'untrusted',
|
||||
);
|
||||
} finally {
|
||||
fixture.enrollment.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects expired, short-lived and non-client-auth leaves', async () => {
|
||||
const now = Date.now();
|
||||
const expired = await identityFixture({
|
||||
now,
|
||||
issue: { notBeforeMs: now - 2 * HOUR_MS, notAfterMs: now - HOUR_MS },
|
||||
});
|
||||
const shortLived = await identityFixture({
|
||||
now,
|
||||
issue: { notAfterMs: now + 2 * HOUR_MS },
|
||||
});
|
||||
const wrongUsage = await identityFixture({
|
||||
now,
|
||||
issue: { clientAuth: false },
|
||||
});
|
||||
try {
|
||||
assert.throws(
|
||||
() =>
|
||||
validateWorkerCertificateIdentity({
|
||||
privateKeyPem: expired.enrollment.privateKeyPem,
|
||||
certificateChainPem: expired.certificateChainPem,
|
||||
trustAnchors: [expired.ca.certificatePem],
|
||||
now,
|
||||
}),
|
||||
(error) => error.reason === 'expired',
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
validateWorkerCertificateIdentity({
|
||||
privateKeyPem: shortLived.enrollment.privateKeyPem,
|
||||
certificateChainPem: shortLived.certificateChainPem,
|
||||
trustAnchors: [shortLived.ca.certificatePem],
|
||||
now,
|
||||
minimumRemainingValidityMs: 3 * HOUR_MS,
|
||||
}),
|
||||
(error) => error.reason === 'insufficient_validity',
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
validateWorkerCertificateIdentity({
|
||||
privateKeyPem: wrongUsage.enrollment.privateKeyPem,
|
||||
certificateChainPem: wrongUsage.certificateChainPem,
|
||||
trustAnchors: [wrongUsage.ca.certificatePem],
|
||||
now,
|
||||
}),
|
||||
(error) => error.reason === 'not_client_auth',
|
||||
);
|
||||
} finally {
|
||||
expired.enrollment.dispose();
|
||||
shortLived.enrollment.dispose();
|
||||
wrongUsage.enrollment.dispose();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
access,
|
||||
chmod,
|
||||
lstat,
|
||||
mkdtemp,
|
||||
readdir,
|
||||
rm,
|
||||
} = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
generateWorkerCertificateEnrollment,
|
||||
} = require('../dist/credential/workerCertificateEnrollment');
|
||||
const {
|
||||
WorkerCertificateFileStore,
|
||||
} = require('../dist/credential/workerCertificateStore');
|
||||
const {
|
||||
createCertificateAuthority,
|
||||
} = require('./helpers/certificateAuthority.cjs');
|
||||
|
||||
async function temporaryStore(t, retainedGenerations = 2) {
|
||||
const parent = await mkdtemp(path.join(os.tmpdir(), 'ql3-worker-store-'));
|
||||
t.after(() => rm(parent, { recursive: true, force: true }));
|
||||
return new WorkerCertificateFileStore({
|
||||
rootDirectory: path.join(parent, 'identity'),
|
||||
retainedGenerations,
|
||||
});
|
||||
}
|
||||
|
||||
async function issueIdentity(ca, workerId, now) {
|
||||
const enrollment = await generateWorkerCertificateEnrollment({ workerId });
|
||||
try {
|
||||
return {
|
||||
privateKeyPem: Buffer.from(enrollment.privateKeyPem),
|
||||
certificateChainPem: await ca.issue(
|
||||
enrollment.certificateSigningRequestPem,
|
||||
),
|
||||
trustAnchors: [ca.certificatePem],
|
||||
now,
|
||||
};
|
||||
} finally {
|
||||
enrollment.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
test('atomically installs and revalidates a private Worker identity', async (t) => {
|
||||
const now = Date.now();
|
||||
const ca = await createCertificateAuthority({ now });
|
||||
const store = await temporaryStore(t);
|
||||
const input = await issueIdentity(ca, 'worker-01', now);
|
||||
|
||||
try {
|
||||
const installed = await store.install(input);
|
||||
const active = await store.readActive([ca.certificatePem], now);
|
||||
|
||||
assert.equal(active.certificateSha256, installed.certificateSha256);
|
||||
assert.equal(active.publicKeySpkiSha256, installed.publicKeySpkiSha256);
|
||||
assert.equal((await lstat(active.privateKeyFile)).mode & 0o777, 0o600);
|
||||
assert.equal(
|
||||
(await lstat(path.dirname(active.privateKeyFile))).mode & 0o777,
|
||||
0o700,
|
||||
);
|
||||
} finally {
|
||||
input.privateKeyPem.fill(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('retains only the configured number of complete generations', async (t) => {
|
||||
const now = Date.now();
|
||||
const ca = await createCertificateAuthority({ now });
|
||||
const store = await temporaryStore(t, 1);
|
||||
const first = await issueIdentity(ca, 'worker-02', now);
|
||||
const second = await issueIdentity(ca, 'worker-02', now + 1_000);
|
||||
|
||||
try {
|
||||
const firstInstalled = await store.install(first);
|
||||
const secondInstalled = await store.install(second);
|
||||
const generations = await readdir(
|
||||
path.join(path.dirname(secondInstalled.privateKeyFile), '..'),
|
||||
);
|
||||
|
||||
assert.deepEqual(generations, [secondInstalled.generationId]);
|
||||
await assert.rejects(access(path.dirname(firstInstalled.privateKeyFile)));
|
||||
} finally {
|
||||
first.privateKeyPem.fill(0);
|
||||
second.privateKeyPem.fill(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects a certificate that does not match its private key', async (t) => {
|
||||
const now = Date.now();
|
||||
const ca = await createCertificateAuthority({ now });
|
||||
const store = await temporaryStore(t);
|
||||
const left = await issueIdentity(ca, 'worker-left', now);
|
||||
const right = await issueIdentity(ca, 'worker-right', now);
|
||||
|
||||
try {
|
||||
await assert.rejects(
|
||||
store.install({
|
||||
...left,
|
||||
privateKeyPem: right.privateKeyPem,
|
||||
}),
|
||||
/install failed/,
|
||||
);
|
||||
assert.equal(await store.readActiveSummary(), undefined);
|
||||
} finally {
|
||||
left.privateKeyPem.fill(0);
|
||||
right.privateKeyPem.fill(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects active identity files whose private permissions drift', async (t) => {
|
||||
const now = Date.now();
|
||||
const ca = await createCertificateAuthority({ now });
|
||||
const store = await temporaryStore(t);
|
||||
const input = await issueIdentity(ca, 'worker-permissions', now);
|
||||
|
||||
try {
|
||||
const installed = await store.install(input);
|
||||
await chmod(installed.certificateChainFile, 0o644);
|
||||
await assert.rejects(
|
||||
store.readActive([ca.certificatePem], now),
|
||||
/file metadata is unsafe/,
|
||||
);
|
||||
} finally {
|
||||
input.privateKeyPem.fill(0);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { createPublicKey } = require('node:crypto');
|
||||
const test = require('node:test');
|
||||
|
||||
require('reflect-metadata');
|
||||
const { Pkcs10CertificateRequest } = require('@peculiar/x509');
|
||||
const {
|
||||
generateWorkerCertificateEnrollment,
|
||||
} = require('../dist/credential/workerCertificateEnrollment');
|
||||
|
||||
test('generates a verifiable P-256 CSR and disposable PKCS#8 key', async () => {
|
||||
const enrollment = await generateWorkerCertificateEnrollment({
|
||||
workerId: 'worker.edge-01',
|
||||
});
|
||||
|
||||
try {
|
||||
assert.equal(enrollment.algorithm, 'ECDSA_P256_SHA256');
|
||||
assert.equal(enrollment.workerId, 'worker.edge-01');
|
||||
assert.match(
|
||||
enrollment.certificateSigningRequestPem,
|
||||
/BEGIN CERTIFICATE REQUEST/,
|
||||
);
|
||||
assert.equal(enrollment.publicKeySpkiSha256.length, 64);
|
||||
assert.equal(
|
||||
createPublicKey(enrollment.privateKeyPem).asymmetricKeyType,
|
||||
'ec',
|
||||
);
|
||||
|
||||
const request = new Pkcs10CertificateRequest(
|
||||
enrollment.certificateSigningRequestPem,
|
||||
);
|
||||
assert.equal(await request.verify(), true);
|
||||
assert.equal(request.subject, 'CN=worker.edge-01');
|
||||
} finally {
|
||||
enrollment.dispose();
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
enrollment.privateKeyPem.every((byte) => byte === 0),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects unbounded or unsafe worker identifiers', async () => {
|
||||
await assert.rejects(
|
||||
generateWorkerCertificateEnrollment({ workerId: '../worker' }),
|
||||
/workerId is invalid/,
|
||||
);
|
||||
await assert.rejects(
|
||||
generateWorkerCertificateEnrollment({ workerId: 'x'.repeat(129) }),
|
||||
/workerId is invalid/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const test = require('node:test');
|
||||
|
||||
test('main entrypoint leaves enrollment PKI out of steady-state memory', () => {
|
||||
const before = new Set(Object.keys(require.cache));
|
||||
const runtime = require('../dist');
|
||||
const loaded = Object.keys(require.cache).filter((file) => !before.has(file));
|
||||
|
||||
assert.equal(typeof runtime.WorkerCertificateFileStore, 'function');
|
||||
assert.equal(typeof runtime.WorkerCertificateRenewalCoordinator, 'function');
|
||||
assert.equal(
|
||||
loaded.some(
|
||||
(file) =>
|
||||
file.includes('/@peculiar/x509/') || file.includes('/@peculiar+x509@'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
loaded.some((file) => file.includes('/ql3-runtime-core/')),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('offer delivery subpath avoids runtime root and cluster/database modules', () => {
|
||||
const before = new Set(Object.keys(require.cache));
|
||||
const delivery = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
const loaded = Object.keys(require.cache).filter((file) => !before.has(file));
|
||||
|
||||
assert.equal(typeof delivery.WorkerRemoteOfferPullCoordinator, 'function');
|
||||
assert.equal(typeof delivery.WorkerRemoteOfferHttpsTransport, 'function');
|
||||
assert.equal(typeof delivery.WorkerRemoteSecretHttpsProvider, 'function');
|
||||
assert.equal(
|
||||
loaded.some((file) => /ql3-runtime-core\/dist\/index\.js$/.test(file)),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
loaded.some(
|
||||
(file) =>
|
||||
file.includes('/ql3-cluster-') ||
|
||||
file.includes('/pg/') ||
|
||||
file.includes('/drizzle-orm/') ||
|
||||
file.includes('/croner/') ||
|
||||
file.includes('/semver/'),
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,246 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
createClusterTaskExecutionRevision,
|
||||
} = require('@qinglong/runtime-core/cluster-execution-revision');
|
||||
const {
|
||||
createClusterRemoteExecutionOffer,
|
||||
} = require('@qinglong/runtime-core/remote-dispatch');
|
||||
const {
|
||||
digestRunDispatchLeaseToken,
|
||||
} = require('@qinglong/runtime-core/run-dispatch-lease');
|
||||
const {
|
||||
createSecretRef,
|
||||
} = require('@qinglong/runtime-core/secret-reference');
|
||||
const {
|
||||
BoundedWorkerRemoteExecutionContextMaterializer,
|
||||
} = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
|
||||
const SESSION_ID = '018f0000-0000-7000-8000-000000000001';
|
||||
const SOURCE_DIGEST = 'a'.repeat(64);
|
||||
const TASK_REVISION = `qltd:v1:1:${SOURCE_DIGEST}`;
|
||||
const LEASE_TOKEN = 'worker_generated_lease_capability_0000000000000001';
|
||||
|
||||
function secret(name) {
|
||||
return createSecretRef({ projectId: 'project-1', name });
|
||||
}
|
||||
|
||||
function offer(environment) {
|
||||
const executionRevision = createClusterTaskExecutionRevision({
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
taskRevision: TASK_REVISION,
|
||||
sourceRevision: 1,
|
||||
sourceContentDigest: SOURCE_DIGEST,
|
||||
executorType: 'remote_worker',
|
||||
planSchema: 'qinglong/command-execution@v1',
|
||||
command: { kind: 'argv', file: '/bin/true', args: [] },
|
||||
environment,
|
||||
createdAtMs: 1,
|
||||
});
|
||||
return createClusterRemoteExecutionOffer({
|
||||
offerId: 'offer-materializer-1',
|
||||
deliveryKind: 'new_claim',
|
||||
executionDigest: executionRevision.contentDigest,
|
||||
candidate: {
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
taskRevision: TASK_REVISION,
|
||||
priority: 1,
|
||||
queuedAtMs: 10,
|
||||
attemptCreatedAtMs: 11,
|
||||
attemptNumber: 1,
|
||||
executorType: 'remote_worker',
|
||||
},
|
||||
worker: { workerId: 'edge-1', sessionId: SESSION_ID, generation: 2 },
|
||||
lease: {
|
||||
attemptId: 'attempt-1',
|
||||
runId: 'run-1',
|
||||
status: 'leased',
|
||||
version: 0,
|
||||
leaseGeneration: 1,
|
||||
workerId: 'edge-1',
|
||||
workerSessionId: SESSION_ID,
|
||||
workerGeneration: 2,
|
||||
leaseTokenDigest: digestRunDispatchLeaseToken(LEASE_TOKEN),
|
||||
acquiredAtMs: 20,
|
||||
renewedAtMs: 20,
|
||||
expiresAtMs: 30_020,
|
||||
updatedAtMs: 20,
|
||||
},
|
||||
leaseToken: LEASE_TOKEN,
|
||||
executionRevision,
|
||||
placementScore: 0,
|
||||
});
|
||||
}
|
||||
|
||||
test('resolves deduplicated Secrets before allocating one Attempt log', async () => {
|
||||
const secretRef = secret('shared');
|
||||
const acceptedOffer = offer([
|
||||
{ name: 'PUBLIC', kind: 'public', value: 'visible' },
|
||||
{ name: 'SECRET_A', kind: 'secret', secretRef },
|
||||
{ name: 'SECRET_B', kind: 'secret', secretRef },
|
||||
]);
|
||||
const events = [];
|
||||
let secretRequest;
|
||||
let artifactRequest;
|
||||
const output = {
|
||||
logArtifactId: 'remote-log-1',
|
||||
async write() {},
|
||||
async close() {},
|
||||
};
|
||||
const materializer = new BoundedWorkerRemoteExecutionContextMaterializer({
|
||||
secrets: {
|
||||
async resolve(request) {
|
||||
events.push('secrets');
|
||||
secretRequest = request;
|
||||
return {
|
||||
values: [{ secretRef, value: 'resolved-value' }],
|
||||
dispose() { events.push('dispose-secrets'); },
|
||||
};
|
||||
},
|
||||
},
|
||||
artifacts: {
|
||||
async prepare(request) {
|
||||
events.push('artifact');
|
||||
artifactRequest = request;
|
||||
return {
|
||||
logArtifactId: 'remote-log-1',
|
||||
takeOutput() { events.push('take-output'); return output; },
|
||||
release() { events.push('release-artifact'); },
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const context = await materializer.prepare({
|
||||
offer: acceptedOffer,
|
||||
completionCallback: { sequence: 1, token: Buffer.alloc(32) },
|
||||
});
|
||||
assert.deepEqual(secretRequest, {
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
taskRevision: TASK_REVISION,
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
offerId: 'offer-materializer-1',
|
||||
executionDigest: acceptedOffer.executionDigest,
|
||||
secretRefs: [secretRef],
|
||||
});
|
||||
assert.deepEqual(artifactRequest, {
|
||||
projectId: 'project-1',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
offerId: 'offer-materializer-1',
|
||||
});
|
||||
assert.equal(JSON.stringify([secretRequest, artifactRequest]).includes(LEASE_TOKEN), false);
|
||||
assert.deepEqual(context.environment, [
|
||||
{ name: 'PUBLIC', value: 'visible' },
|
||||
{ name: 'SECRET_A', value: 'resolved-value' },
|
||||
{ name: 'SECRET_B', value: 'resolved-value' },
|
||||
]);
|
||||
assert.equal(context.logArtifactId, 'remote-log-1');
|
||||
assert.deepEqual(events, ['secrets', 'artifact']);
|
||||
assert.equal(context.takeOutput(), output);
|
||||
assert.throws(() => context.takeOutput(), /artifact_response_invalid/);
|
||||
await context.dispose();
|
||||
await context.dispose();
|
||||
assert.deepEqual(events.slice(2).sort(), [
|
||||
'dispose-secrets', 'release-artifact', 'take-output',
|
||||
]);
|
||||
});
|
||||
|
||||
test('fails before Artifact allocation when Secret authority is unavailable', async () => {
|
||||
let artifacts = 0;
|
||||
const materializer = new BoundedWorkerRemoteExecutionContextMaterializer({
|
||||
artifacts: { async prepare() { artifacts += 1; } },
|
||||
});
|
||||
await assert.rejects(
|
||||
materializer.prepare({
|
||||
offer: offer([{ name: 'SECRET', kind: 'secret', secretRef: secret('one') }]),
|
||||
}),
|
||||
/secret_unavailable/,
|
||||
);
|
||||
assert.equal(artifacts, 0);
|
||||
});
|
||||
|
||||
test('disposes malformed Secret and Artifact responses without exposing values', async () => {
|
||||
let disposedSecrets = 0;
|
||||
let releasedArtifact = 0;
|
||||
const secretRef = secret('one');
|
||||
const malformedSecrets = new BoundedWorkerRemoteExecutionContextMaterializer({
|
||||
secrets: {
|
||||
async resolve() {
|
||||
return {
|
||||
values: [
|
||||
{ secretRef, value: 'first' },
|
||||
{ secretRef, value: 'duplicate' },
|
||||
],
|
||||
dispose() { disposedSecrets += 1; },
|
||||
};
|
||||
},
|
||||
},
|
||||
artifacts: { async prepare() { throw new Error('must not allocate'); } },
|
||||
});
|
||||
await assert.rejects(
|
||||
malformedSecrets.prepare({
|
||||
offer: offer([
|
||||
{ name: 'A', kind: 'secret', secretRef },
|
||||
{ name: 'B', kind: 'secret', secretRef: secret('two') },
|
||||
]),
|
||||
}),
|
||||
/secret_response_invalid/,
|
||||
);
|
||||
assert.equal(disposedSecrets, 1);
|
||||
|
||||
const malformedArtifact = new BoundedWorkerRemoteExecutionContextMaterializer({
|
||||
artifacts: {
|
||||
async prepare() {
|
||||
return {
|
||||
logArtifactId: 'x'.repeat(37),
|
||||
release() { releasedArtifact += 1; },
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
malformedArtifact.prepare({
|
||||
offer: offer([{ name: 'PUBLIC', kind: 'public', value: 'visible' }]),
|
||||
}),
|
||||
/artifact_response_invalid/,
|
||||
);
|
||||
assert.equal(releasedArtifact, 1);
|
||||
});
|
||||
|
||||
test('enforces the resolved environment byte budget before Artifact allocation', async () => {
|
||||
const bindings = Array.from({ length: 5 }, (_, index) => ({
|
||||
name: `SECRET_${index}`,
|
||||
kind: 'secret',
|
||||
secretRef: secret(`item-${index}`),
|
||||
}));
|
||||
let disposed = 0;
|
||||
let artifacts = 0;
|
||||
const materializer = new BoundedWorkerRemoteExecutionContextMaterializer({
|
||||
secrets: {
|
||||
async resolve(request) {
|
||||
return {
|
||||
values: request.secretRefs.map((secretRef) => ({
|
||||
secretRef,
|
||||
value: 'x'.repeat(16 * 1024),
|
||||
})),
|
||||
dispose() { disposed += 1; },
|
||||
};
|
||||
},
|
||||
},
|
||||
artifacts: { async prepare() { artifacts += 1; } },
|
||||
});
|
||||
await assert.rejects(
|
||||
materializer.prepare({ offer: offer(bindings) }),
|
||||
/environment_budget_exceeded/,
|
||||
);
|
||||
assert.equal(disposed, 1);
|
||||
assert.equal(artifacts, 0);
|
||||
});
|
||||
@@ -0,0 +1,566 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
createClusterTaskExecutionRevision,
|
||||
} = require('@qinglong/runtime-core/cluster-execution-revision');
|
||||
const {
|
||||
createClusterRemoteExecutionOffer,
|
||||
} = require('@qinglong/runtime-core/remote-dispatch');
|
||||
const {
|
||||
digestRunDispatchLeaseToken,
|
||||
} = require('@qinglong/runtime-core/run-dispatch-lease');
|
||||
const {
|
||||
createSecretRef,
|
||||
} = require('@qinglong/runtime-core/secret-reference');
|
||||
const {
|
||||
WorkerRemoteExecutionInboxProcessor,
|
||||
assertWorkerRemoteExecutionInboxTransition,
|
||||
createWorkerRemoteExecutionInboxRecord,
|
||||
} = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
const {
|
||||
WorkerInboxExecutionSpawnBarrier,
|
||||
} = require('../dist/execution/workerPosixExecutionExecutor');
|
||||
|
||||
const SESSION_ID = '018f0000-0000-7000-8000-000000000001';
|
||||
const SOURCE_DIGEST = 'a'.repeat(64);
|
||||
const TASK_REVISION = `qltd:v1:1:${SOURCE_DIGEST}`;
|
||||
const LEASE_TOKEN = 'worker_generated_lease_capability_0000000000000001';
|
||||
|
||||
function offer(timeoutMs) {
|
||||
const executionRevision = createClusterTaskExecutionRevision({
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
taskRevision: TASK_REVISION,
|
||||
sourceRevision: 1,
|
||||
sourceContentDigest: SOURCE_DIGEST,
|
||||
executorType: 'remote_worker',
|
||||
planSchema: 'qinglong/command-execution@v1',
|
||||
command: { kind: 'argv', file: '/bin/true', args: [] },
|
||||
environment: [
|
||||
{ name: 'PUBLIC_VALUE', kind: 'public', value: 'visible' },
|
||||
{
|
||||
name: 'SECRET_VALUE',
|
||||
kind: 'secret',
|
||||
secretRef: createSecretRef({ projectId: 'project-1', name: 'item-1' }),
|
||||
},
|
||||
],
|
||||
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
||||
createdAtMs: 1,
|
||||
});
|
||||
return createClusterRemoteExecutionOffer({
|
||||
offerId: 'offer-processor-1',
|
||||
deliveryKind: 'new_claim',
|
||||
executionDigest: executionRevision.contentDigest,
|
||||
candidate: {
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
taskRevision: TASK_REVISION,
|
||||
priority: 1,
|
||||
queuedAtMs: 10,
|
||||
attemptCreatedAtMs: 11,
|
||||
attemptNumber: 1,
|
||||
executorType: 'remote_worker',
|
||||
},
|
||||
worker: {
|
||||
workerId: 'edge-1',
|
||||
sessionId: SESSION_ID,
|
||||
generation: 2,
|
||||
},
|
||||
lease: {
|
||||
attemptId: 'attempt-1',
|
||||
runId: 'run-1',
|
||||
status: 'leased',
|
||||
version: 0,
|
||||
leaseGeneration: 1,
|
||||
workerId: 'edge-1',
|
||||
workerSessionId: SESSION_ID,
|
||||
workerGeneration: 2,
|
||||
leaseTokenDigest: digestRunDispatchLeaseToken(LEASE_TOKEN),
|
||||
acquiredAtMs: 20,
|
||||
renewedAtMs: 20,
|
||||
expiresAtMs: 30_020,
|
||||
updatedAtMs: 20,
|
||||
},
|
||||
leaseToken: LEASE_TOKEN,
|
||||
executionRevision,
|
||||
placementScore: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function inboxFixture(initial = createWorkerRemoteExecutionInboxRecord(offer(), 100)) {
|
||||
let record = initial;
|
||||
const states = [];
|
||||
return {
|
||||
inbox: {
|
||||
async readOffer(offerId) {
|
||||
return record?.offer.offerId === offerId ? record : undefined;
|
||||
},
|
||||
async replaceOffer(next, expectedRevision) {
|
||||
assert.equal(record.revision, expectedRevision);
|
||||
assertWorkerRemoteExecutionInboxTransition(record, next);
|
||||
record = next;
|
||||
states.push(next.state);
|
||||
},
|
||||
async listOffers() { return { records: record ? [record] : [] }; },
|
||||
},
|
||||
states,
|
||||
record: () => record,
|
||||
};
|
||||
}
|
||||
|
||||
function snapshot(overrides = {}) {
|
||||
return {
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
runStatus: 'dispatching',
|
||||
attemptStatus: 'starting',
|
||||
leaseVersion: 0,
|
||||
leaseGeneration: 1,
|
||||
callbackSequence: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function outputSink(logArtifactId = 'log-1', onClose = () => undefined) {
|
||||
return {
|
||||
logArtifactId,
|
||||
async write() {},
|
||||
async close() { onClose(); },
|
||||
};
|
||||
}
|
||||
|
||||
function options(fixture, overrides = {}) {
|
||||
let event = 0;
|
||||
const activation = overrides.activation ?? {
|
||||
async acknowledgeStarting() {
|
||||
return { status: 'already_starting', snapshot: snapshot() };
|
||||
},
|
||||
async acknowledgeRunning(command) {
|
||||
return {
|
||||
status: 'applied',
|
||||
snapshot: snapshot({
|
||||
runStatus: 'running',
|
||||
attemptStatus: 'running',
|
||||
callbackSequence: command.callbackSequence,
|
||||
executorHandle: command.executorHandle,
|
||||
}),
|
||||
};
|
||||
},
|
||||
async failStart() {
|
||||
return {
|
||||
status: 'applied',
|
||||
snapshot: snapshot({
|
||||
runStatus: 'failed',
|
||||
attemptStatus: 'failed',
|
||||
leaseVersion: 1,
|
||||
callbackSequence: 1,
|
||||
}),
|
||||
};
|
||||
},
|
||||
};
|
||||
return {
|
||||
inbox: fixture.inbox,
|
||||
activation,
|
||||
currentSession: () => ({
|
||||
workerId: 'edge-1',
|
||||
sessionId: SESSION_ID,
|
||||
generation: 2,
|
||||
status: 'available',
|
||||
leaseExpiresAtMs: 30_000,
|
||||
}),
|
||||
now: () => 1_000,
|
||||
randomCapability: () => Buffer.alloc(32, 7),
|
||||
eventId: () => `event-${++event}`,
|
||||
materializer: overrides.materializer ?? {
|
||||
async prepare() {
|
||||
const output = outputSink();
|
||||
let taken = false;
|
||||
return {
|
||||
environment: [
|
||||
{ name: 'SECRET_VALUE', value: 'resolved-secret' },
|
||||
{ name: 'PUBLIC_VALUE', value: 'visible' },
|
||||
],
|
||||
logArtifactId: 'log-1',
|
||||
takeOutput() {
|
||||
assert.equal(taken, false);
|
||||
taken = true;
|
||||
return output;
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
executor: overrides.executor ?? {
|
||||
async start(launch) {
|
||||
return {
|
||||
status: 'started', executorHandle: 'process-1',
|
||||
executorStartedAtMs: launch.executorStartedAtMs,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('persists every ACK and spawn barrier in the one delivery inbox', async () => {
|
||||
const fixture = inboxFixture();
|
||||
let startingCalls = 0;
|
||||
let launchedToken;
|
||||
let runningCommand;
|
||||
const activation = {
|
||||
async acknowledgeStarting() {
|
||||
startingCalls += 1;
|
||||
return {
|
||||
status: startingCalls === 1 ? 'applied' : 'already_starting',
|
||||
snapshot: snapshot(),
|
||||
};
|
||||
},
|
||||
async acknowledgeRunning(command) {
|
||||
runningCommand = command;
|
||||
assert.equal(fixture.record().state, 'started');
|
||||
return {
|
||||
status: 'applied',
|
||||
snapshot: snapshot({
|
||||
runStatus: 'running',
|
||||
attemptStatus: 'running',
|
||||
callbackSequence: command.callbackSequence,
|
||||
executorHandle: command.executorHandle,
|
||||
}),
|
||||
};
|
||||
},
|
||||
async failStart() { throw new Error('must not fail'); },
|
||||
};
|
||||
const processor = new WorkerRemoteExecutionInboxProcessor(options(fixture, {
|
||||
activation,
|
||||
executor: {
|
||||
async start(launch) {
|
||||
assert.equal(fixture.record().state, 'launching');
|
||||
assert.deepEqual(
|
||||
launch.environment.map((entry) => entry.name),
|
||||
['PUBLIC_VALUE', 'SECRET_VALUE'],
|
||||
);
|
||||
assert.equal(launch.logArtifactId, 'log-1');
|
||||
assert.equal(launch.output.logArtifactId, launch.logArtifactId);
|
||||
launchedToken = launch.completionCallback.token;
|
||||
return {
|
||||
status: 'started', executorHandle: 'process-1',
|
||||
executorStartedAtMs: launch.executorStartedAtMs,
|
||||
};
|
||||
},
|
||||
},
|
||||
}));
|
||||
const result = await processor.process('offer-processor-1');
|
||||
assert.equal(result.status, 'running');
|
||||
assert.deepEqual(fixture.states, [
|
||||
'starting_acknowledged',
|
||||
'launching',
|
||||
'started',
|
||||
'running_acknowledged',
|
||||
]);
|
||||
assert.equal(startingCalls, 2);
|
||||
assert.equal(runningCommand.callbackSequence, 1);
|
||||
assert.match(runningCommand.callbackTokenDigest, /^[a-f0-9]{64}$/);
|
||||
assert.ok([...launchedToken].every((value) => value === 0));
|
||||
});
|
||||
|
||||
test('passes timeout to the Executor only with durable starting deadline authority', async () => {
|
||||
const fixture = inboxFixture(
|
||||
createWorkerRemoteExecutionInboxRecord(offer(5_000), 100),
|
||||
);
|
||||
let launch;
|
||||
const base = options(fixture, {
|
||||
activation: {
|
||||
async acknowledgeStarting() {
|
||||
return {
|
||||
status: 'already_starting',
|
||||
snapshot: snapshot({ deadlineAtMs: 6_000 }),
|
||||
};
|
||||
},
|
||||
async acknowledgeRunning(command) {
|
||||
return {
|
||||
status: 'applied',
|
||||
snapshot: snapshot({
|
||||
runStatus: 'running', attemptStatus: 'running',
|
||||
callbackSequence: command.callbackSequence,
|
||||
executorHandle: command.executorHandle,
|
||||
deadlineAtMs: 6_000,
|
||||
}),
|
||||
};
|
||||
},
|
||||
async failStart() { throw new Error('must not fail'); },
|
||||
},
|
||||
executor: {
|
||||
async start(value) {
|
||||
launch = value;
|
||||
return {
|
||||
status: 'started', executorHandle: 'process-1',
|
||||
executorStartedAtMs: value.executorStartedAtMs,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await new WorkerRemoteExecutionInboxProcessor(base)
|
||||
.process('offer-processor-1');
|
||||
assert.equal(result.status, 'running');
|
||||
assert.equal(launch.timeoutMs, 5_000);
|
||||
assert.equal(launch.executionDeadlineAtMs, 6_000);
|
||||
});
|
||||
|
||||
test('fails closed before spawn when timeout revision lacks durable deadline authority', async () => {
|
||||
const fixture = inboxFixture(
|
||||
createWorkerRemoteExecutionInboxRecord(offer(5_000), 100),
|
||||
);
|
||||
let starts = 0;
|
||||
const base = options(fixture, {
|
||||
executor: { async start() { starts += 1; return { status: 'rejected' }; } },
|
||||
});
|
||||
await assert.rejects(
|
||||
new WorkerRemoteExecutionInboxProcessor(base).process('offer-processor-1'),
|
||||
/activation_response_invalid/,
|
||||
);
|
||||
assert.equal(starts, 0);
|
||||
});
|
||||
|
||||
test('treats an ambiguous executor error as recovery, never start failure', async () => {
|
||||
const fixture = inboxFixture();
|
||||
let failCalls = 0;
|
||||
let closes = 0;
|
||||
const base = options(fixture, {
|
||||
materializer: {
|
||||
async prepare() {
|
||||
return {
|
||||
environment: [
|
||||
{ name: 'SECRET_VALUE', value: 'resolved-secret' },
|
||||
{ name: 'PUBLIC_VALUE', value: 'visible' },
|
||||
],
|
||||
logArtifactId: 'log-1',
|
||||
takeOutput: () => outputSink('log-1', () => { closes += 1; }),
|
||||
};
|
||||
},
|
||||
},
|
||||
executor: { async start() { throw new Error('response lost after spawn'); } },
|
||||
});
|
||||
base.activation.failStart = async () => {
|
||||
failCalls += 1;
|
||||
throw new Error('must not be called');
|
||||
};
|
||||
const result = await new WorkerRemoteExecutionInboxProcessor(base)
|
||||
.process('offer-processor-1');
|
||||
assert.equal(result.status, 'recovery_required');
|
||||
assert.equal(result.recoveryReason, 'launch_outcome_unknown');
|
||||
assert.equal(failCalls, 0);
|
||||
assert.equal(closes, 0);
|
||||
assert.equal(fixture.record().state, 'recovery_required');
|
||||
});
|
||||
|
||||
test('reports only an explicit no-spawn rejection as start failure', async () => {
|
||||
const fixture = inboxFixture();
|
||||
let failureCommand;
|
||||
let closes = 0;
|
||||
const base = options(fixture, {
|
||||
materializer: {
|
||||
async prepare() {
|
||||
return {
|
||||
environment: [
|
||||
{ name: 'SECRET_VALUE', value: 'resolved-secret' },
|
||||
{ name: 'PUBLIC_VALUE', value: 'visible' },
|
||||
],
|
||||
logArtifactId: 'log-1',
|
||||
takeOutput: () => outputSink('log-1', () => { closes += 1; }),
|
||||
};
|
||||
},
|
||||
},
|
||||
executor: { async start() { return { status: 'rejected' }; } },
|
||||
});
|
||||
base.activation.failStart = async (command) => {
|
||||
failureCommand = command;
|
||||
return {
|
||||
status: 'applied',
|
||||
snapshot: snapshot({
|
||||
runStatus: 'failed',
|
||||
attemptStatus: 'failed',
|
||||
leaseVersion: 1,
|
||||
callbackSequence: 1,
|
||||
}),
|
||||
};
|
||||
};
|
||||
const result = await new WorkerRemoteExecutionInboxProcessor(base)
|
||||
.process('offer-processor-1');
|
||||
assert.equal(result.status, 'start_failed');
|
||||
assert.equal(failureCommand.offerId, 'offer-processor-1');
|
||||
assert.equal(closes, 1);
|
||||
assert.equal(fixture.record().state, 'start_failure_acknowledged');
|
||||
});
|
||||
|
||||
test('takes output only after the durable launching barrier', async () => {
|
||||
const fixture = inboxFixture();
|
||||
let starts = 0;
|
||||
const base = options(fixture, {
|
||||
materializer: {
|
||||
async prepare() {
|
||||
return {
|
||||
environment: [
|
||||
{ name: 'SECRET_VALUE', value: 'resolved-secret' },
|
||||
{ name: 'PUBLIC_VALUE', value: 'visible' },
|
||||
],
|
||||
logArtifactId: 'log-1',
|
||||
takeOutput() {
|
||||
assert.equal(fixture.record().state, 'launching');
|
||||
return outputSink();
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
executor: {
|
||||
async start(launch) {
|
||||
starts += 1;
|
||||
return {
|
||||
status: 'started', executorHandle: 'process-1',
|
||||
executorStartedAtMs: launch.executorStartedAtMs,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await new WorkerRemoteExecutionInboxProcessor(base)
|
||||
.process('offer-processor-1');
|
||||
assert.equal(result.status, 'running');
|
||||
assert.equal(starts, 1);
|
||||
});
|
||||
|
||||
test('fails without spawning when the handed-off output identity drifts', async () => {
|
||||
const fixture = inboxFixture();
|
||||
let starts = 0;
|
||||
let closes = 0;
|
||||
const base = options(fixture, {
|
||||
materializer: {
|
||||
async prepare() {
|
||||
return {
|
||||
environment: [
|
||||
{ name: 'SECRET_VALUE', value: 'resolved-secret' },
|
||||
{ name: 'PUBLIC_VALUE', value: 'visible' },
|
||||
],
|
||||
logArtifactId: 'log-1',
|
||||
takeOutput: () => outputSink('different-log', () => { closes += 1; }),
|
||||
};
|
||||
},
|
||||
},
|
||||
executor: {
|
||||
async start() {
|
||||
starts += 1;
|
||||
return {
|
||||
status: 'started', executorHandle: 'process-1', executorStartedAtMs: 900,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await new WorkerRemoteExecutionInboxProcessor(base)
|
||||
.process('offer-processor-1');
|
||||
assert.equal(result.status, 'start_failed');
|
||||
assert.equal(starts, 0);
|
||||
assert.equal(closes, 1);
|
||||
});
|
||||
|
||||
test('never respawns a restart-visible launching record', async () => {
|
||||
const accepted = createWorkerRemoteExecutionInboxRecord(offer(), 100);
|
||||
const starting = {
|
||||
...accepted,
|
||||
revision: 1,
|
||||
state: 'starting_acknowledged',
|
||||
updatedAtMs: 101,
|
||||
};
|
||||
assertWorkerRemoteExecutionInboxTransition(accepted, starting);
|
||||
const launching = {
|
||||
...starting,
|
||||
revision: 2,
|
||||
state: 'launching',
|
||||
updatedAtMs: 102,
|
||||
executorStartedAtMs: 102,
|
||||
logArtifactId: 'log-1',
|
||||
completionReceiptCallbackSequence: 1,
|
||||
completionReceiptTokenDigest: 'b'.repeat(64),
|
||||
};
|
||||
assertWorkerRemoteExecutionInboxTransition(starting, launching);
|
||||
const fixture = inboxFixture(launching);
|
||||
let sideEffects = 0;
|
||||
const base = options(fixture);
|
||||
base.activation.acknowledgeStarting = async () => { sideEffects += 1; };
|
||||
base.executor.start = async () => { sideEffects += 1; };
|
||||
const result = await new WorkerRemoteExecutionInboxProcessor(base)
|
||||
.process('offer-processor-1');
|
||||
assert.equal(result.status, 'recovery_required');
|
||||
assert.equal(sideEffects, 0);
|
||||
assert.equal(fixture.record().state, 'recovery_required');
|
||||
});
|
||||
|
||||
test('revalidates the exact durable log and callback barrier before POSIX spawn', async () => {
|
||||
const accepted = createWorkerRemoteExecutionInboxRecord(offer(), 100);
|
||||
const starting = {
|
||||
...accepted,
|
||||
revision: 1,
|
||||
state: 'starting_acknowledged',
|
||||
updatedAtMs: 101,
|
||||
};
|
||||
const launching = {
|
||||
...starting,
|
||||
revision: 2,
|
||||
state: 'launching',
|
||||
updatedAtMs: 102,
|
||||
executorStartedAtMs: 102,
|
||||
logArtifactId: 'log-1',
|
||||
completionReceiptCallbackSequence: 1,
|
||||
completionReceiptTokenDigest: 'b'.repeat(64),
|
||||
};
|
||||
assertWorkerRemoteExecutionInboxTransition(accepted, starting);
|
||||
assertWorkerRemoteExecutionInboxTransition(starting, launching);
|
||||
const barrier = new WorkerInboxExecutionSpawnBarrier({
|
||||
async readOffer() { return launching; },
|
||||
});
|
||||
const exact = {
|
||||
offerId: 'offer-processor-1',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
callbackSequence: 1,
|
||||
callbackTokenDigest: 'b'.repeat(64),
|
||||
logArtifactId: 'log-1',
|
||||
executorStartedAtMs: 102,
|
||||
};
|
||||
await barrier.verify(exact);
|
||||
await assert.rejects(
|
||||
barrier.verify({ ...exact, logArtifactId: 'log-2' }),
|
||||
/authority drifted/,
|
||||
);
|
||||
await assert.rejects(
|
||||
barrier.verify({ ...exact, callbackTokenDigest: 'c'.repeat(64) }),
|
||||
/authority drifted/,
|
||||
);
|
||||
});
|
||||
|
||||
test('reports failure before the spawn barrier when materialized public data drifts', async () => {
|
||||
const fixture = inboxFixture();
|
||||
let starts = 0;
|
||||
const base = options(fixture, {
|
||||
materializer: {
|
||||
async prepare() {
|
||||
return {
|
||||
environment: [
|
||||
{ name: 'PUBLIC_VALUE', value: 'tampered' },
|
||||
{ name: 'SECRET_VALUE', value: 'resolved-secret' },
|
||||
],
|
||||
};
|
||||
},
|
||||
},
|
||||
executor: {
|
||||
async start() {
|
||||
starts += 1;
|
||||
return { status: 'started', executorHandle: 'x', executorStartedAtMs: 900 };
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await new WorkerRemoteExecutionInboxProcessor(base)
|
||||
.process('offer-processor-1');
|
||||
assert.equal(result.status, 'start_failed');
|
||||
assert.equal(starts, 0);
|
||||
assert.equal(fixture.record().state, 'start_failure_acknowledged');
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
'use strict';
|
||||
|
||||
require('reflect-metadata');
|
||||
|
||||
const { randomBytes, webcrypto } = require('node:crypto');
|
||||
const {
|
||||
BasicConstraintsExtension,
|
||||
ExtendedKeyUsage,
|
||||
ExtendedKeyUsageExtension,
|
||||
KeyUsageFlags,
|
||||
KeyUsagesExtension,
|
||||
Pkcs10CertificateRequest,
|
||||
SubjectKeyIdentifierExtension,
|
||||
X509CertificateGenerator,
|
||||
} = require('@peculiar/x509');
|
||||
|
||||
const algorithm = Object.freeze({
|
||||
name: 'ECDSA',
|
||||
namedCurve: 'P-256',
|
||||
hash: 'SHA-256',
|
||||
});
|
||||
|
||||
async function createCertificateAuthority(options = {}) {
|
||||
const now = options.now ?? Date.now();
|
||||
const keys = await webcrypto.subtle.generateKey(algorithm, true, [
|
||||
'sign',
|
||||
'verify',
|
||||
]);
|
||||
const name = 'CN=QingLong Worker Test CA';
|
||||
const certificate = await X509CertificateGenerator.createSelfSigned(
|
||||
{
|
||||
serialNumber: randomBytes(16).toString('hex'),
|
||||
name,
|
||||
notBefore: new Date(now - 60_000),
|
||||
notAfter: new Date(now + 365 * 24 * 60 * 60_000),
|
||||
signingAlgorithm: algorithm,
|
||||
keys,
|
||||
extensions: [
|
||||
new BasicConstraintsExtension(true, 1, true),
|
||||
new KeyUsagesExtension(
|
||||
KeyUsageFlags.keyCertSign | KeyUsageFlags.cRLSign,
|
||||
true,
|
||||
),
|
||||
await SubjectKeyIdentifierExtension.create(
|
||||
keys.publicKey,
|
||||
false,
|
||||
webcrypto,
|
||||
),
|
||||
],
|
||||
},
|
||||
webcrypto,
|
||||
);
|
||||
|
||||
return Object.freeze({
|
||||
certificatePem: certificate.toString('pem'),
|
||||
async issue(certificateSigningRequestPem, issueOptions = {}) {
|
||||
const request = new Pkcs10CertificateRequest(
|
||||
certificateSigningRequestPem,
|
||||
);
|
||||
if (!(await request.verify(webcrypto))) {
|
||||
throw new Error('test CSR signature is invalid');
|
||||
}
|
||||
const notBeforeMs = issueOptions.notBeforeMs ?? now - 60_000;
|
||||
const notAfterMs = issueOptions.notAfterMs ?? now + 30 * 24 * 60 * 60_000;
|
||||
const extensions = [
|
||||
new BasicConstraintsExtension(false, undefined, true),
|
||||
new KeyUsagesExtension(KeyUsageFlags.digitalSignature, true),
|
||||
await SubjectKeyIdentifierExtension.create(
|
||||
request.publicKey,
|
||||
false,
|
||||
webcrypto,
|
||||
),
|
||||
];
|
||||
if (issueOptions.clientAuth !== false) {
|
||||
extensions.splice(
|
||||
1,
|
||||
0,
|
||||
new ExtendedKeyUsageExtension([ExtendedKeyUsage.clientAuth], true),
|
||||
);
|
||||
}
|
||||
const leaf = await X509CertificateGenerator.create(
|
||||
{
|
||||
serialNumber: randomBytes(16).toString('hex'),
|
||||
subject: request.subject,
|
||||
issuer: name,
|
||||
notBefore: new Date(notBeforeMs),
|
||||
notAfter: new Date(notAfterMs),
|
||||
signingAlgorithm: algorithm,
|
||||
publicKey: request.publicKey,
|
||||
signingKey: keys.privateKey,
|
||||
extensions,
|
||||
},
|
||||
webcrypto,
|
||||
);
|
||||
return leaf.toString('pem');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { createCertificateAuthority };
|
||||
@@ -0,0 +1,209 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
createProductionWorkerHeadlessExecutionStack,
|
||||
startProductionWorkerHeadlessApplication,
|
||||
} = require('@qinglong/worker-runtime/production');
|
||||
|
||||
const SESSION_ID = '018f0000-0000-7000-8000-000000000001';
|
||||
|
||||
async function temporaryStorage() {
|
||||
const root = await fs.mkdtemp(
|
||||
path.join(os.tmpdir(), 'ql3-worker-production-'),
|
||||
);
|
||||
return {
|
||||
root,
|
||||
storage: {
|
||||
journalRoot: path.join(root, 'journal'),
|
||||
logRoot: path.join(root, 'logs'),
|
||||
receiptRoot: path.join(root, 'receipts'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function options(storage, session, overrides = {}) {
|
||||
return {
|
||||
enabled: true,
|
||||
profile: 'worker',
|
||||
capacityProfile: 'edge',
|
||||
origin: 'https://worker-control.invalid',
|
||||
credentials: {
|
||||
async load() {
|
||||
throw new Error('credentials must remain lazy');
|
||||
},
|
||||
},
|
||||
session,
|
||||
storage,
|
||||
cadenceMs: 60_000,
|
||||
drainTimeoutMs: 1_000,
|
||||
drainPollMs: 25,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function sessionLifecycle() {
|
||||
let status = 'available';
|
||||
let drains = 0;
|
||||
return {
|
||||
current() {
|
||||
return {
|
||||
workerId: 'worker-1',
|
||||
sessionId: SESSION_ID,
|
||||
generation: 1,
|
||||
status,
|
||||
leaseExpiresAtMs: Date.now() + 60_000,
|
||||
};
|
||||
},
|
||||
async beginDrain() {
|
||||
drains += 1;
|
||||
status = 'draining';
|
||||
},
|
||||
drains() {
|
||||
return drains;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('disabled production Worker is resource-free before option access', async () => {
|
||||
const candidate = { enabled: false };
|
||||
Object.defineProperty(candidate, 'profile', {
|
||||
get() {
|
||||
throw new Error('disabled path inspected profile');
|
||||
},
|
||||
});
|
||||
const application = await startProductionWorkerHeadlessApplication(candidate);
|
||||
assert.equal(application.status, 'disabled');
|
||||
assert.equal(await application.stop(), 'stopped');
|
||||
});
|
||||
|
||||
test('the concrete execution factory requires an explicit enabled authority', () => {
|
||||
assert.throws(
|
||||
() => createProductionWorkerHeadlessExecutionStack({ enabled: false }),
|
||||
/invalid_configuration/,
|
||||
);
|
||||
});
|
||||
|
||||
test('assembles one concrete execution plane and drains before owner release', async () => {
|
||||
const temporary = await temporaryStorage();
|
||||
const session = sessionLifecycle();
|
||||
try {
|
||||
const application = await startProductionWorkerHeadlessApplication(
|
||||
options(temporary.storage, session),
|
||||
);
|
||||
assert.equal(application.status, 'active');
|
||||
const journal = await fs.stat(temporary.storage.journalRoot);
|
||||
const offers = await fs.stat(
|
||||
path.join(temporary.storage.journalRoot, 'offers'),
|
||||
);
|
||||
assert.equal(journal.isDirectory(), true);
|
||||
assert.equal(offers.isDirectory(), true);
|
||||
await assert.rejects(fs.stat(temporary.storage.logRoot), {
|
||||
code: 'ENOENT',
|
||||
});
|
||||
await assert.rejects(fs.stat(temporary.storage.receiptRoot), {
|
||||
code: 'ENOENT',
|
||||
});
|
||||
assert.equal(await application.stop(), 'stopped');
|
||||
assert.equal(await application.stop(), 'stopped');
|
||||
assert.equal(session.drains(), 1);
|
||||
} finally {
|
||||
await fs.rm(temporary.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects wrong Profile and overlapping authorities before filesystem use', async () => {
|
||||
const temporary = await temporaryStorage();
|
||||
const session = sessionLifecycle();
|
||||
try {
|
||||
await assert.rejects(
|
||||
startProductionWorkerHeadlessApplication(
|
||||
options(temporary.storage, session, { profile: 'cluster-control' }),
|
||||
),
|
||||
/invalid_configuration/,
|
||||
);
|
||||
const overlapping = {
|
||||
journalRoot: path.join(temporary.root, 'state'),
|
||||
logRoot: path.join(temporary.root, 'state', 'logs'),
|
||||
receiptRoot: path.join(temporary.root, 'receipts'),
|
||||
};
|
||||
await assert.rejects(
|
||||
startProductionWorkerHeadlessApplication(options(overlapping, session)),
|
||||
/invalid_configuration/,
|
||||
);
|
||||
await assert.rejects(fs.stat(overlapping.journalRoot), { code: 'ENOENT' });
|
||||
} finally {
|
||||
await fs.rm(temporary.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('a failed Session drain keeps the application owned and retryable', async () => {
|
||||
const temporary = await temporaryStorage();
|
||||
let attempts = 0;
|
||||
let status = 'available';
|
||||
const session = {
|
||||
current() {
|
||||
return {
|
||||
workerId: 'worker-1',
|
||||
sessionId: SESSION_ID,
|
||||
generation: 1,
|
||||
status,
|
||||
leaseExpiresAtMs: Date.now() + 60_000,
|
||||
};
|
||||
},
|
||||
async beginDrain() {
|
||||
attempts += 1;
|
||||
if (attempts === 1) throw new Error('drain unavailable');
|
||||
status = 'draining';
|
||||
},
|
||||
};
|
||||
try {
|
||||
const application = await startProductionWorkerHeadlessApplication(
|
||||
options(temporary.storage, session),
|
||||
);
|
||||
await assert.rejects(application.stop(), /drain unavailable/);
|
||||
assert.equal(await application.stop(), 'stopped');
|
||||
assert.equal(attempts, 2);
|
||||
} finally {
|
||||
await fs.rm(temporary.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('production execution graph is reachable only through its explicit subpath', () => {
|
||||
const packageDirectory = path.resolve(__dirname, '..');
|
||||
const inspect = (specifier) => {
|
||||
const script = `
|
||||
const exported = require(${JSON.stringify(specifier)});
|
||||
const loaded = Object.keys(require.cache).map((file) => file.replaceAll('\\\\', '/'));
|
||||
process.stdout.write(JSON.stringify({
|
||||
hasProduction: typeof exported.startProductionWorkerHeadlessApplication === 'function',
|
||||
loadedJournal: loaded.some((file) => file.includes('/remoteOfferFileJournal.js')),
|
||||
loadedLock: loaded.some((file) => file.includes('/proper-lockfile/')),
|
||||
loadedCluster: loaded.some((file) => file.includes('/ql3-cluster-')),
|
||||
}));
|
||||
`;
|
||||
const result = spawnSync(process.execPath, ['-e', script], {
|
||||
cwd: packageDirectory,
|
||||
encoding: 'utf8',
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
return JSON.parse(result.stdout);
|
||||
};
|
||||
assert.deepEqual(inspect('@qinglong/worker-runtime'), {
|
||||
hasProduction: false,
|
||||
loadedJournal: false,
|
||||
loadedLock: false,
|
||||
loadedCluster: false,
|
||||
});
|
||||
assert.deepEqual(inspect('@qinglong/worker-runtime/production'), {
|
||||
hasProduction: true,
|
||||
loadedJournal: true,
|
||||
loadedLock: true,
|
||||
loadedCluster: false,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,639 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const https = require('node:https');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
startProductionWorkerApplication,
|
||||
} = require('@qinglong/worker-runtime/product');
|
||||
const {
|
||||
startProductionWorkerHeadlessApplicationWithStack,
|
||||
} = require('@qinglong/worker-runtime/production');
|
||||
|
||||
const AUTHORIZATION = `Worker ql3w_worker_primary_${Buffer.alloc(
|
||||
32,
|
||||
7,
|
||||
).toString('base64url')}`;
|
||||
const fixtures = path.resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls',
|
||||
);
|
||||
|
||||
async function material(name) {
|
||||
return fs.readFile(path.join(fixtures, name));
|
||||
}
|
||||
|
||||
async function temporaryStorage() {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-worker-product-'));
|
||||
return {
|
||||
root,
|
||||
storage: {
|
||||
journalRoot: path.join(root, 'journal'),
|
||||
logRoot: path.join(root, 'logs'),
|
||||
receiptRoot: path.join(root, 'receipts'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function capabilities() {
|
||||
return {
|
||||
architecture: 'x64',
|
||||
operatingSystem: 'linux',
|
||||
executors: ['local_process'],
|
||||
runtimes: [{ name: 'node', version: '24.14.0' }],
|
||||
labels: {},
|
||||
capacity: { cpuCores: 1, memoryBytes: 256 * 1024 * 1024 },
|
||||
features: [],
|
||||
};
|
||||
}
|
||||
|
||||
test('owns one TLS Agent across register, drain and offline', async () => {
|
||||
const [ca, serverCertificate, serverKey, clientCertificate, clientKey] =
|
||||
await Promise.all([
|
||||
material('ca-cert.pem'),
|
||||
material('server-cert.pem'),
|
||||
material('server-key.pem'),
|
||||
material('client-cert.pem'),
|
||||
material('client-key.pem'),
|
||||
]);
|
||||
const temporary = await temporaryStorage();
|
||||
const observations = [];
|
||||
const sockets = new Set();
|
||||
let version = -1;
|
||||
const server = https.createServer(
|
||||
{
|
||||
ca,
|
||||
cert: serverCertificate,
|
||||
key: serverKey,
|
||||
minVersion: 'TLSv1.3',
|
||||
maxVersion: 'TLSv1.3',
|
||||
requestCert: true,
|
||||
rejectUnauthorized: true,
|
||||
},
|
||||
(request, response) => {
|
||||
const chunks = [];
|
||||
request.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
request.on('end', () => {
|
||||
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
||||
const match = request.url.match(
|
||||
/^\/api\/v3\/worker-ingress\/workers\/edge-1\/sessions\/([^/]+)\/(register|transition)$/,
|
||||
);
|
||||
assert.ok(match);
|
||||
sockets.add(request.socket);
|
||||
observations.push({
|
||||
operation: match[2],
|
||||
status: body.status,
|
||||
availableSlots: body.availableSlots,
|
||||
authorized: request.socket.authorized,
|
||||
protocol: request.socket.getProtocol(),
|
||||
});
|
||||
version += 1;
|
||||
const status = match[2] === 'register' ? 'online' : body.status;
|
||||
const payload = {
|
||||
schema: body.schema,
|
||||
workerId: 'edge-1',
|
||||
sessionId: match[1],
|
||||
generation: 1,
|
||||
version,
|
||||
status,
|
||||
leaseExpiresAtMs: 46_000,
|
||||
...(match[2] === 'register' ? { replacedSession: false } : {}),
|
||||
};
|
||||
const encoded = JSON.stringify(payload);
|
||||
response.writeHead(200, {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(encoded)),
|
||||
});
|
||||
response.end(encoded);
|
||||
});
|
||||
},
|
||||
);
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
assert.ok(address && typeof address === 'object');
|
||||
try {
|
||||
const application = await startProductionWorkerApplication({
|
||||
enabled: true,
|
||||
profile: 'worker',
|
||||
capacityProfile: 'edge',
|
||||
origin: `https://127.0.0.1:${address.port}`,
|
||||
credentials: {
|
||||
async load() {
|
||||
return {
|
||||
authorization: AUTHORIZATION,
|
||||
certificateChainPem: clientCertificate,
|
||||
privateKeyPem: clientKey,
|
||||
trustAnchors: [ca],
|
||||
};
|
||||
},
|
||||
},
|
||||
workerId: 'edge-1',
|
||||
capabilities: capabilities(),
|
||||
maxConcurrentRuns: 2,
|
||||
storage: temporary.storage,
|
||||
cadenceMs: 60_000,
|
||||
drainTimeoutMs: 1_000,
|
||||
drainPollMs: 25,
|
||||
now: () => 1_000,
|
||||
});
|
||||
assert.equal(application.status, 'active');
|
||||
assert.equal(await application.stop(), 'stopped');
|
||||
assert.equal(await application.stop(), 'stopped');
|
||||
assert.equal(sockets.size, 1);
|
||||
assert.deepEqual(observations, [
|
||||
{
|
||||
operation: 'register',
|
||||
status: undefined,
|
||||
availableSlots: 2,
|
||||
authorized: true,
|
||||
protocol: 'TLSv1.3',
|
||||
},
|
||||
{
|
||||
operation: 'transition',
|
||||
status: 'draining',
|
||||
availableSlots: undefined,
|
||||
authorized: true,
|
||||
protocol: 'TLSv1.3',
|
||||
},
|
||||
{
|
||||
operation: 'transition',
|
||||
status: 'offline',
|
||||
availableSlots: undefined,
|
||||
authorized: true,
|
||||
protocol: 'TLSv1.3',
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
await fs.rm(temporary.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects an unsettled startup journal before Session registration', async () => {
|
||||
const temporary = await temporaryStorage();
|
||||
let registers = 0;
|
||||
let releases = 0;
|
||||
const options = {
|
||||
enabled: true,
|
||||
profile: 'worker',
|
||||
capacityProfile: 'edge',
|
||||
origin: 'https://worker-control.invalid',
|
||||
credentials: {
|
||||
async load() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
session: {
|
||||
current() {
|
||||
return undefined;
|
||||
},
|
||||
async register() {
|
||||
registers += 1;
|
||||
},
|
||||
async beginDrain() {},
|
||||
},
|
||||
storage: temporary.storage,
|
||||
};
|
||||
const stack = {
|
||||
journal: {
|
||||
async listOffers() {
|
||||
return {
|
||||
records: [{ state: 'accepted', offer: { offerId: 'offer-1' } }],
|
||||
};
|
||||
},
|
||||
},
|
||||
lifecycle: {
|
||||
async start() {
|
||||
return 'started';
|
||||
},
|
||||
async stop() {
|
||||
releases += 1;
|
||||
},
|
||||
},
|
||||
client: { close() {} },
|
||||
offerTransport: { close() {} },
|
||||
ownsClient: false,
|
||||
};
|
||||
try {
|
||||
await assert.rejects(
|
||||
startProductionWorkerHeadlessApplicationWithStack(options, stack),
|
||||
/startup_recovery_required/,
|
||||
);
|
||||
assert.equal(registers, 0);
|
||||
assert.equal(releases, 1);
|
||||
} finally {
|
||||
await fs.rm(temporary.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects unavailable startup identity before Session registration', async () => {
|
||||
const temporary = await temporaryStorage();
|
||||
let registers = 0;
|
||||
let fences = 0;
|
||||
let releases = 0;
|
||||
let transportCloses = 0;
|
||||
const options = {
|
||||
enabled: true,
|
||||
profile: 'worker',
|
||||
capacityProfile: 'edge',
|
||||
origin: 'https://worker-control.invalid',
|
||||
credentials: {
|
||||
async load() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
certificateRenewal: {
|
||||
async run() {
|
||||
return { status: 'unavailable', nextAttemptAtMs: 2_000 };
|
||||
},
|
||||
},
|
||||
session: {
|
||||
current() {
|
||||
return undefined;
|
||||
},
|
||||
async register() {
|
||||
registers += 1;
|
||||
},
|
||||
failClosed() {
|
||||
fences += 1;
|
||||
},
|
||||
async beginDrain() {},
|
||||
},
|
||||
storage: temporary.storage,
|
||||
};
|
||||
const stack = {
|
||||
journal: {
|
||||
async listOffers() {
|
||||
return { records: [] };
|
||||
},
|
||||
},
|
||||
lifecycle: {
|
||||
async start() {},
|
||||
async tick() {
|
||||
return { status: 'reconciled', processed: 0 };
|
||||
},
|
||||
async stop() {
|
||||
releases += 1;
|
||||
},
|
||||
},
|
||||
client: { close() {} },
|
||||
offerTransport: {
|
||||
close() {
|
||||
transportCloses += 1;
|
||||
},
|
||||
},
|
||||
ownsClient: false,
|
||||
};
|
||||
try {
|
||||
await assert.rejects(
|
||||
startProductionWorkerHeadlessApplicationWithStack(options, stack),
|
||||
/certificate_unavailable/,
|
||||
);
|
||||
assert.equal(registers, 0);
|
||||
assert.equal(fences, 1);
|
||||
assert.equal(releases, 1);
|
||||
assert.equal(transportCloses, 1);
|
||||
} finally {
|
||||
await fs.rm(temporary.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('drives Session and execution in one cadence and releases ownership last', async () => {
|
||||
const temporary = await temporaryStorage();
|
||||
const events = [];
|
||||
let status;
|
||||
let draining = false;
|
||||
const options = {
|
||||
enabled: true,
|
||||
profile: 'worker',
|
||||
capacityProfile: 'edge',
|
||||
origin: 'https://worker-control.invalid',
|
||||
credentials: {
|
||||
async load() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
session: {
|
||||
current() {
|
||||
return status === undefined
|
||||
? undefined
|
||||
: {
|
||||
workerId: 'edge-1',
|
||||
sessionId: '018f0000-0000-7000-8000-000000000001',
|
||||
generation: 1,
|
||||
status,
|
||||
leaseExpiresAtMs: Date.now() + 60_000,
|
||||
};
|
||||
},
|
||||
async register() {
|
||||
events.push('session:register');
|
||||
status = 'available';
|
||||
},
|
||||
async tick() {
|
||||
events.push('session:tick');
|
||||
},
|
||||
async beginDrain() {
|
||||
events.push('session:drain');
|
||||
status = 'draining';
|
||||
},
|
||||
async disconnect() {
|
||||
events.push('session:offline');
|
||||
status = 'offline';
|
||||
},
|
||||
},
|
||||
storage: temporary.storage,
|
||||
cadenceMs: 60_000,
|
||||
drainTimeoutMs: 1_000,
|
||||
drainPollMs: 25,
|
||||
};
|
||||
let startup = true;
|
||||
const stack = {
|
||||
journal: {
|
||||
async listOffers() {
|
||||
return { records: [] };
|
||||
},
|
||||
},
|
||||
lifecycle: {
|
||||
async start() {
|
||||
events.push('execution:start');
|
||||
},
|
||||
async tick() {
|
||||
if (startup) {
|
||||
startup = false;
|
||||
events.push('execution:reconcile');
|
||||
return { status: 'reconciled', processed: 0 };
|
||||
}
|
||||
events.push('execution:tick');
|
||||
return draining
|
||||
? { status: 'draining' }
|
||||
: { status: 'session_unavailable' };
|
||||
},
|
||||
async beginDrain() {
|
||||
events.push('execution:drain');
|
||||
draining = true;
|
||||
},
|
||||
async stop() {
|
||||
events.push('execution:release');
|
||||
},
|
||||
},
|
||||
client: {
|
||||
close() {
|
||||
events.push('client:close');
|
||||
},
|
||||
},
|
||||
offerTransport: {
|
||||
close() {
|
||||
events.push('transport:close');
|
||||
},
|
||||
},
|
||||
ownsClient: false,
|
||||
};
|
||||
try {
|
||||
const application = await startProductionWorkerHeadlessApplicationWithStack(
|
||||
options,
|
||||
stack,
|
||||
);
|
||||
await application.tick();
|
||||
assert.equal(await application.stop(), 'stopped');
|
||||
assert.deepEqual(events, [
|
||||
'execution:start',
|
||||
'execution:reconcile',
|
||||
'session:register',
|
||||
'session:tick',
|
||||
'execution:tick',
|
||||
'execution:drain',
|
||||
'session:drain',
|
||||
'session:tick',
|
||||
'execution:tick',
|
||||
'session:offline',
|
||||
'execution:release',
|
||||
'transport:close',
|
||||
]);
|
||||
} finally {
|
||||
await fs.rm(temporary.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('runs certificate renewal in the existing cadence and fences admission', async () => {
|
||||
const temporary = await temporaryStorage();
|
||||
const events = [];
|
||||
const diagnostics = [];
|
||||
let renewalRuns = 0;
|
||||
let sessionAvailable = true;
|
||||
const options = {
|
||||
enabled: true,
|
||||
profile: 'worker',
|
||||
capacityProfile: 'edge',
|
||||
origin: 'https://worker-control.invalid',
|
||||
credentials: {
|
||||
async load() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
certificateRenewal: {
|
||||
async run() {
|
||||
renewalRuns += 1;
|
||||
events.push(`certificate:${renewalRuns}`);
|
||||
if (renewalRuns === 1) {
|
||||
return { status: 'not_due', identity: {}, renewAtMs: 10_000 };
|
||||
}
|
||||
return { status: 'unavailable', nextAttemptAtMs: 20_000 };
|
||||
},
|
||||
},
|
||||
session: {
|
||||
current() {
|
||||
return sessionAvailable
|
||||
? {
|
||||
workerId: 'edge-1',
|
||||
sessionId: '018f0000-0000-7000-8000-000000000001',
|
||||
generation: 1,
|
||||
status: 'available',
|
||||
leaseExpiresAtMs: Date.now() + 60_000,
|
||||
}
|
||||
: undefined;
|
||||
},
|
||||
async register() {
|
||||
events.push('session:register');
|
||||
},
|
||||
async tick() {
|
||||
events.push('session:tick');
|
||||
},
|
||||
failClosed() {
|
||||
events.push('session:fail-closed');
|
||||
sessionAvailable = false;
|
||||
},
|
||||
async beginDrain() {
|
||||
events.push('session:drain');
|
||||
},
|
||||
},
|
||||
storage: temporary.storage,
|
||||
cadenceMs: 60_000,
|
||||
drainTimeoutMs: 1_000,
|
||||
drainPollMs: 25,
|
||||
diagnostic(fact) {
|
||||
diagnostics.push(fact.code);
|
||||
},
|
||||
};
|
||||
let startup = true;
|
||||
const stack = {
|
||||
journal: {
|
||||
async listOffers() {
|
||||
return { records: [] };
|
||||
},
|
||||
},
|
||||
lifecycle: {
|
||||
async start() {
|
||||
events.push('execution:start');
|
||||
},
|
||||
async tick() {
|
||||
if (startup) {
|
||||
startup = false;
|
||||
events.push('execution:reconcile');
|
||||
return { status: 'reconciled', processed: 0 };
|
||||
}
|
||||
events.push('execution:tick');
|
||||
return { status: 'session_unavailable' };
|
||||
},
|
||||
async beginDrain() {
|
||||
events.push('execution:drain');
|
||||
},
|
||||
async stop() {
|
||||
events.push('execution:release');
|
||||
},
|
||||
},
|
||||
client: { close() {} },
|
||||
offerTransport: {
|
||||
close() {
|
||||
events.push('transport:close');
|
||||
},
|
||||
},
|
||||
ownsClient: false,
|
||||
};
|
||||
try {
|
||||
const application = await startProductionWorkerHeadlessApplicationWithStack(
|
||||
options,
|
||||
stack,
|
||||
);
|
||||
assert.deepEqual(events.slice(0, 4), [
|
||||
'execution:start',
|
||||
'execution:reconcile',
|
||||
'certificate:1',
|
||||
'session:register',
|
||||
]);
|
||||
assert.deepEqual(await application.tick(), {
|
||||
status: 'session_unavailable',
|
||||
});
|
||||
assert.equal(events.includes('session:tick'), false);
|
||||
assert.deepEqual(events.slice(4), [
|
||||
'certificate:2',
|
||||
'session:fail-closed',
|
||||
'execution:tick',
|
||||
]);
|
||||
assert.deepEqual(await application.tick(), {
|
||||
status: 'session_unavailable',
|
||||
});
|
||||
assert.equal(
|
||||
events.filter((event) => event === 'session:fail-closed').length,
|
||||
1,
|
||||
);
|
||||
assert.deepEqual(diagnostics, ['certificate_unavailable']);
|
||||
assert.equal(await application.stop(), 'stopped');
|
||||
} finally {
|
||||
await fs.rm(temporary.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('retries owner release after Session is already durably offline', async () => {
|
||||
const temporary = await temporaryStorage();
|
||||
let status;
|
||||
let releases = 0;
|
||||
let transportCloses = 0;
|
||||
const options = {
|
||||
enabled: true,
|
||||
profile: 'worker',
|
||||
capacityProfile: 'edge',
|
||||
origin: 'https://worker-control.invalid',
|
||||
credentials: {
|
||||
async load() {
|
||||
throw new Error('not used');
|
||||
},
|
||||
},
|
||||
session: {
|
||||
current() {
|
||||
return status === undefined
|
||||
? undefined
|
||||
: {
|
||||
workerId: 'edge-1',
|
||||
sessionId: '018f0000-0000-7000-8000-000000000001',
|
||||
generation: 1,
|
||||
status,
|
||||
leaseExpiresAtMs: Date.now() + 60_000,
|
||||
};
|
||||
},
|
||||
async register() {
|
||||
status = 'available';
|
||||
},
|
||||
async tick() {},
|
||||
async beginDrain() {
|
||||
if (status !== 'offline') status = 'draining';
|
||||
},
|
||||
async disconnect() {
|
||||
if (status !== 'offline') status = 'offline';
|
||||
},
|
||||
},
|
||||
storage: temporary.storage,
|
||||
cadenceMs: 60_000,
|
||||
drainTimeoutMs: 1_000,
|
||||
drainPollMs: 25,
|
||||
};
|
||||
let startup = true;
|
||||
const stack = {
|
||||
journal: {
|
||||
async listOffers() {
|
||||
return { records: [] };
|
||||
},
|
||||
},
|
||||
lifecycle: {
|
||||
async start() {},
|
||||
async tick() {
|
||||
if (startup) {
|
||||
startup = false;
|
||||
return { status: 'reconciled', processed: 0 };
|
||||
}
|
||||
return { status: 'draining' };
|
||||
},
|
||||
async beginDrain() {},
|
||||
async stop() {
|
||||
releases += 1;
|
||||
if (releases === 1) throw new Error('owner release unavailable');
|
||||
},
|
||||
},
|
||||
client: { close() {} },
|
||||
offerTransport: {
|
||||
close() {
|
||||
transportCloses += 1;
|
||||
},
|
||||
},
|
||||
ownsClient: false,
|
||||
};
|
||||
try {
|
||||
const application = await startProductionWorkerHeadlessApplicationWithStack(
|
||||
options,
|
||||
stack,
|
||||
);
|
||||
await assert.rejects(application.stop(), /owner release unavailable/);
|
||||
assert.equal(status, 'offline');
|
||||
assert.equal(transportCloses, 0);
|
||||
assert.equal(await application.stop(), 'stopped');
|
||||
assert.equal(releases, 2);
|
||||
assert.equal(transportCloses, 1);
|
||||
} finally {
|
||||
await fs.rm(temporary.root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,220 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { EventEmitter } = require('node:events');
|
||||
const { PassThrough } = require('node:stream');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WorkerIngressHttpsClient,
|
||||
WorkerRemoteExecutionHttpsActivationClient,
|
||||
WorkerRemoteOfferHttpsTransport,
|
||||
} = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
|
||||
const SESSION_ID = '018f0000-0000-7000-8000-000000000001';
|
||||
const AUTHORIZATION =
|
||||
`Worker ql3w_worker_primary_${Buffer.alloc(32, 7).toString('base64url')}`;
|
||||
|
||||
function response(body) {
|
||||
const serialized = JSON.stringify(body);
|
||||
const stream = new PassThrough();
|
||||
stream.statusCode = 200;
|
||||
stream.headers = {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(serialized)),
|
||||
};
|
||||
queueMicrotask(() => stream.end(serialized));
|
||||
return stream;
|
||||
}
|
||||
|
||||
function responseBody(overrides = {}) {
|
||||
return {
|
||||
schema: 'qinglong/remote-run-activation@v1',
|
||||
status: 'applied',
|
||||
snapshot: {
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
runStatus: 'dispatching',
|
||||
attemptStatus: 'starting',
|
||||
leaseVersion: 4,
|
||||
leaseGeneration: 3,
|
||||
callbackSequence: 0,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function responseBodyForPath(requestPath) {
|
||||
if (requestPath.endsWith('/offers')) return { status: 'idle' };
|
||||
if (requestPath.endsWith('/running')) {
|
||||
return responseBody({
|
||||
snapshot: {
|
||||
...responseBody().snapshot,
|
||||
runStatus: 'running',
|
||||
attemptStatus: 'running',
|
||||
callbackSequence: 1,
|
||||
executorHandle: 'remote:handle-1',
|
||||
startedAtMs: 20_000,
|
||||
},
|
||||
});
|
||||
}
|
||||
if (requestPath.endsWith('/start-failure')) {
|
||||
return responseBody({
|
||||
snapshot: {
|
||||
...responseBody().snapshot,
|
||||
runStatus: 'failed',
|
||||
attemptStatus: 'failed',
|
||||
leaseVersion: 5,
|
||||
callbackSequence: 1,
|
||||
finishedAtMs: 20_000,
|
||||
errorCode: 'EXECUTOR_START_FAILED',
|
||||
},
|
||||
});
|
||||
}
|
||||
return responseBody();
|
||||
}
|
||||
|
||||
function requestFactory(observations, responseFactory = responseBodyForPath) {
|
||||
return (options, callback) => {
|
||||
const request = new EventEmitter();
|
||||
request.setTimeout = () => request;
|
||||
request.destroy = (error) => {
|
||||
if (error) queueMicrotask(() => request.emit('error', error));
|
||||
};
|
||||
request.end = (body) => {
|
||||
observations.push({
|
||||
agent: options.agent,
|
||||
path: options.path,
|
||||
body: JSON.parse(Buffer.from(body).toString('utf8')),
|
||||
});
|
||||
queueMicrotask(() => callback(response(responseFactory(options.path))));
|
||||
};
|
||||
return request;
|
||||
};
|
||||
}
|
||||
|
||||
function credentials() {
|
||||
return {
|
||||
authorization: AUTHORIZATION,
|
||||
certificateChainPem: 'client certificate',
|
||||
privateKeyPem: 'client private key',
|
||||
trustAnchors: ['trusted ca'],
|
||||
};
|
||||
}
|
||||
|
||||
function command() {
|
||||
return {
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
workerId: 'edge-1',
|
||||
workerSessionId: SESSION_ID,
|
||||
workerGeneration: 2,
|
||||
offerId: 'offer-1',
|
||||
leaseGeneration: 3,
|
||||
leaseToken: 'worker_generated_lease_capability_0000000000000001',
|
||||
expectedLeaseVersion: 4,
|
||||
};
|
||||
}
|
||||
|
||||
test('shares one Agent and credential authority across offer and activation calls', async () => {
|
||||
const observations = [];
|
||||
const shared = new WorkerIngressHttpsClient({
|
||||
origin: 'https://cluster.example:7443',
|
||||
credentials: { async load() { return credentials(); } },
|
||||
requestFactory: requestFactory(observations),
|
||||
});
|
||||
const offers = new WorkerRemoteOfferHttpsTransport({ client: shared });
|
||||
const activation = new WorkerRemoteExecutionHttpsActivationClient({
|
||||
client: shared,
|
||||
});
|
||||
try {
|
||||
await offers.exchange({
|
||||
path: `/api/v3/worker-ingress/workers/edge-1/sessions/${SESSION_ID}/offers`,
|
||||
body: {
|
||||
workerGeneration: 2,
|
||||
offerId: 'offer-1',
|
||||
leaseToken: command().leaseToken,
|
||||
},
|
||||
maximumResponseBytes: 1024,
|
||||
});
|
||||
offers.close();
|
||||
await activation.acknowledgeStarting({
|
||||
...command(),
|
||||
eventId: '018f0000-0000-7000-8000-000000000002',
|
||||
});
|
||||
await activation.acknowledgeRunning({
|
||||
...command(),
|
||||
attemptEventId: '018f0000-0000-7000-8000-000000000003',
|
||||
runEventId: '018f0000-0000-7000-8000-000000000004',
|
||||
executorHandle: 'remote:handle-1',
|
||||
callbackSequence: 1,
|
||||
callbackTokenDigest: 'a'.repeat(64),
|
||||
});
|
||||
await activation.failStart({
|
||||
...command(),
|
||||
attemptEventId: '018f0000-0000-7000-8000-000000000005',
|
||||
runEventId: '018f0000-0000-7000-8000-000000000006',
|
||||
});
|
||||
await shared.postJson({
|
||||
path: `/api/v3/worker-ingress/workers/edge-1/sessions/${SESSION_ID}/secrets`,
|
||||
body: { probe: true },
|
||||
maximumResponseBytes: 16 * 1024,
|
||||
});
|
||||
assert.equal(new Set(observations.map((item) => item.agent)).size, 1);
|
||||
assert.deepEqual(observations.map((item) => item.path.split('/').at(-1)), [
|
||||
'offers', 'starting', 'running', 'start-failure', 'secrets',
|
||||
]);
|
||||
assert.equal('eventId' in observations[1].body, false);
|
||||
assert.equal('workerId' in observations[1].body, false);
|
||||
assert.equal('workerSessionId' in observations[1].body, false);
|
||||
assert.equal(observations[2].body.logArtifactId, null);
|
||||
} finally {
|
||||
shared.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects a response whose run authority does not match the request', async () => {
|
||||
const shared = new WorkerIngressHttpsClient({
|
||||
origin: 'https://cluster.example',
|
||||
credentials: { async load() { return credentials(); } },
|
||||
requestFactory: requestFactory([], () => responseBody({
|
||||
snapshot: { ...responseBody().snapshot, runId: 'run-other' },
|
||||
})),
|
||||
});
|
||||
const activation = new WorkerRemoteExecutionHttpsActivationClient({
|
||||
client: shared,
|
||||
});
|
||||
try {
|
||||
await assert.rejects(
|
||||
activation.acknowledgeStarting({
|
||||
...command(),
|
||||
eventId: '018f0000-0000-7000-8000-000000000002',
|
||||
}),
|
||||
/response_invalid/,
|
||||
);
|
||||
} finally {
|
||||
shared.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('keeps 4 KiB as the default body cap and permits bounded Secret batches explicitly', async () => {
|
||||
const observations = [];
|
||||
const shared = new WorkerIngressHttpsClient({
|
||||
origin: 'https://cluster.example',
|
||||
credentials: { async load() { return credentials(); } },
|
||||
requestFactory: requestFactory(observations, () => ({ ok: true })),
|
||||
});
|
||||
const path = `/api/v3/worker-ingress/workers/edge-1/sessions/${SESSION_ID}/secrets`;
|
||||
const body = { value: 'x'.repeat(5 * 1024) };
|
||||
try {
|
||||
await assert.rejects(
|
||||
shared.postJson({ path, body, maximumResponseBytes: 1024 }),
|
||||
/request_rejected/,
|
||||
);
|
||||
await shared.postJson({
|
||||
path, body, maximumRequestBytes: 64 * 1024, maximumResponseBytes: 1024,
|
||||
});
|
||||
assert.equal(observations.length, 1);
|
||||
} finally {
|
||||
shared.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { readFile } = require('node:fs/promises');
|
||||
const https = require('node:https');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WorkerIngressHttpsClient,
|
||||
WorkerRemoteExecutionHttpsActivationClient,
|
||||
} = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
|
||||
const SESSION_ID = '018f0000-0000-7000-8000-000000000001';
|
||||
const AUTHORIZATION =
|
||||
`Worker ql3w_worker_primary_${Buffer.alloc(32, 7).toString('base64url')}`;
|
||||
const fixtures = path.resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls',
|
||||
);
|
||||
|
||||
async function material(name) {
|
||||
return readFile(path.join(fixtures, name));
|
||||
}
|
||||
|
||||
test('completes a real TLS 1.3 mutual-auth activation exchange', async () => {
|
||||
const [ca, serverCertificate, serverKey, clientCertificate, clientKey] =
|
||||
await Promise.all([
|
||||
material('ca-cert.pem'),
|
||||
material('server-cert.pem'),
|
||||
material('server-key.pem'),
|
||||
material('client-cert.pem'),
|
||||
material('client-key.pem'),
|
||||
]);
|
||||
const observations = [];
|
||||
const server = https.createServer({
|
||||
ca,
|
||||
cert: serverCertificate,
|
||||
key: serverKey,
|
||||
minVersion: 'TLSv1.3',
|
||||
maxVersion: 'TLSv1.3',
|
||||
requestCert: true,
|
||||
rejectUnauthorized: true,
|
||||
}, (request, response) => {
|
||||
const chunks = [];
|
||||
request.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
request.on('end', () => {
|
||||
observations.push({
|
||||
authorized: request.socket.authorized,
|
||||
protocol: request.socket.getProtocol(),
|
||||
authorization: request.headers.authorization,
|
||||
path: request.url,
|
||||
body: JSON.parse(Buffer.concat(chunks).toString('utf8')),
|
||||
});
|
||||
const body = JSON.stringify({
|
||||
schema: 'qinglong/remote-run-activation@v1',
|
||||
status: 'applied',
|
||||
snapshot: {
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
runStatus: 'dispatching',
|
||||
attemptStatus: 'starting',
|
||||
leaseVersion: 4,
|
||||
leaseGeneration: 3,
|
||||
callbackSequence: 0,
|
||||
},
|
||||
});
|
||||
response.writeHead(200, {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(body)),
|
||||
});
|
||||
response.end(body);
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
assert.ok(address && typeof address === 'object');
|
||||
const shared = new WorkerIngressHttpsClient({
|
||||
origin: `https://127.0.0.1:${address.port}`,
|
||||
credentials: {
|
||||
async load() {
|
||||
return {
|
||||
authorization: AUTHORIZATION,
|
||||
certificateChainPem: clientCertificate,
|
||||
privateKeyPem: clientKey,
|
||||
trustAnchors: [ca],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const activation = new WorkerRemoteExecutionHttpsActivationClient({
|
||||
client: shared,
|
||||
});
|
||||
const result = await activation.acknowledgeStarting({
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
workerId: 'edge-1',
|
||||
workerSessionId: SESSION_ID,
|
||||
workerGeneration: 2,
|
||||
offerId: 'offer-1',
|
||||
leaseGeneration: 3,
|
||||
leaseToken: 'worker_generated_lease_capability_0000000000000001',
|
||||
expectedLeaseVersion: 4,
|
||||
eventId: '018f0000-0000-7000-8000-000000000002',
|
||||
});
|
||||
assert.equal(result.status, 'applied');
|
||||
assert.deepEqual(observations, [{
|
||||
authorized: true,
|
||||
protocol: 'TLSv1.3',
|
||||
authorization: AUTHORIZATION,
|
||||
path: `/api/v3/worker-ingress/workers/edge-1/sessions/${SESSION_ID}/starting`,
|
||||
body: {
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
workerGeneration: 2,
|
||||
offerId: 'offer-1',
|
||||
leaseGeneration: 3,
|
||||
leaseToken: 'worker_generated_lease_capability_0000000000000001',
|
||||
expectedLeaseVersion: 4,
|
||||
},
|
||||
}]);
|
||||
} finally {
|
||||
shared.close();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,371 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { mkdtemp, lstat, rm } = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
createClusterTaskExecutionRevision,
|
||||
} = require('@qinglong/runtime-core/cluster-execution-revision');
|
||||
const {
|
||||
createClusterRemoteExecutionOffer,
|
||||
} = require('@qinglong/runtime-core/remote-dispatch');
|
||||
const {
|
||||
createRemoteExecutionOfferPullBody,
|
||||
} = require('@qinglong/runtime-core/remote-offer-delivery');
|
||||
const {
|
||||
digestRunDispatchLeaseToken,
|
||||
} = require('@qinglong/runtime-core');
|
||||
const {
|
||||
WorkerRemoteOfferFileJournal,
|
||||
WorkerRemoteOfferPullCoordinator,
|
||||
createWorkerRemoteOfferClaimRecord,
|
||||
normalizeWorkerRemoteExecutionInboxRecord,
|
||||
} = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
|
||||
const SESSION = '018f0000-0000-7000-8000-000000000001';
|
||||
const SOURCE_DIGEST = 'a'.repeat(64);
|
||||
const TASK_REVISION = `qltd:v1:1:${SOURCE_DIGEST}`;
|
||||
const STATS = Object.freeze({
|
||||
pages: 1,
|
||||
candidates: 1,
|
||||
plansUnavailable: 0,
|
||||
placementMismatches: 0,
|
||||
claimAttempts: 1,
|
||||
claimRaces: 0,
|
||||
});
|
||||
|
||||
const session = Object.freeze({
|
||||
workerId: 'edge-1',
|
||||
sessionId: SESSION,
|
||||
generation: 2,
|
||||
});
|
||||
|
||||
function executionRevision() {
|
||||
return createClusterTaskExecutionRevision({
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
taskRevision: TASK_REVISION,
|
||||
sourceRevision: 1,
|
||||
sourceContentDigest: SOURCE_DIGEST,
|
||||
executorType: 'remote_worker',
|
||||
planSchema: 'qinglong/command-execution@v1',
|
||||
command: { kind: 'argv', file: '/bin/true', args: [] },
|
||||
environment: [],
|
||||
createdAtMs: 1,
|
||||
});
|
||||
}
|
||||
|
||||
function offerFromRequest(request, version = 0) {
|
||||
const revision = executionRevision();
|
||||
return createClusterRemoteExecutionOffer({
|
||||
offerId: request.body.offerId,
|
||||
deliveryKind: version === 0 ? 'new_claim' : 'lease_recovery',
|
||||
executionDigest: revision.contentDigest,
|
||||
candidate: {
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
taskRevision: TASK_REVISION,
|
||||
priority: 1,
|
||||
queuedAtMs: 10,
|
||||
attemptCreatedAtMs: 11,
|
||||
attemptNumber: 1,
|
||||
executorType: 'remote_worker',
|
||||
},
|
||||
worker: {
|
||||
workerId: session.workerId,
|
||||
sessionId: session.sessionId,
|
||||
generation: session.generation,
|
||||
},
|
||||
lease: {
|
||||
attemptId: 'attempt-1',
|
||||
runId: 'run-1',
|
||||
status: 'leased',
|
||||
version,
|
||||
leaseGeneration: 1,
|
||||
workerId: session.workerId,
|
||||
workerSessionId: session.sessionId,
|
||||
workerGeneration: session.generation,
|
||||
leaseTokenDigest: digestRunDispatchLeaseToken(request.body.leaseToken),
|
||||
acquiredAtMs: 20,
|
||||
renewedAtMs: 20 + version,
|
||||
expiresAtMs: 30_020 + version,
|
||||
updatedAtMs: 20 + version,
|
||||
},
|
||||
leaseToken: request.body.leaseToken,
|
||||
executionRevision: revision,
|
||||
placementScore: 0,
|
||||
});
|
||||
}
|
||||
|
||||
async function journalFixture(t, maximumEntries = 64) {
|
||||
const parent = await mkdtemp(path.join(os.tmpdir(), 'ql3-worker-offer-'));
|
||||
t.after(() => rm(parent, { recursive: true, force: true }));
|
||||
const rootDirectory = path.join(parent, 'inbox');
|
||||
const journal = new WorkerRemoteOfferFileJournal({
|
||||
rootDirectory,
|
||||
maximumEntries,
|
||||
ownershipStaleMs: 5_000,
|
||||
});
|
||||
await journal.acquireOwnership();
|
||||
t.after(() => journal.releaseOwnership().catch(() => undefined));
|
||||
return { journal, rootDirectory };
|
||||
}
|
||||
|
||||
test('persists the claim intent and accepted capability in private atomic files', async (t) => {
|
||||
const { journal, rootDirectory } = await journalFixture(t);
|
||||
const claim = createWorkerRemoteOfferClaimRecord({
|
||||
workerId: session.workerId,
|
||||
workerSessionId: session.sessionId,
|
||||
workerGeneration: session.generation,
|
||||
offerId: 'offer-1',
|
||||
leaseToken: 'worker_generated_lease_capability_0000000000000001',
|
||||
}, 1_000);
|
||||
await journal.createPendingClaim(claim);
|
||||
assert.equal((await journal.readPendingClaim()).offerId, 'offer-1');
|
||||
|
||||
const request = {
|
||||
body: {
|
||||
offerId: claim.offerId,
|
||||
leaseToken: claim.leaseToken,
|
||||
},
|
||||
};
|
||||
const accepted = await journal.acceptOffer(offerFromRequest(request), 1_001);
|
||||
assert.equal(accepted.status, 'accepted');
|
||||
const replayed = await journal.acceptOffer(offerFromRequest(request), 1_002);
|
||||
assert.equal(replayed.status, 'replayed');
|
||||
assert.equal(replayed.record.revision, 0);
|
||||
assert.equal(
|
||||
(await lstat(path.join(rootDirectory, 'offers', 'offer-1.json'))).mode & 0o777,
|
||||
0o600,
|
||||
);
|
||||
assert.equal((await lstat(rootDirectory)).mode & 0o777, 0o700);
|
||||
});
|
||||
|
||||
test('uses one revision-fenced inbox record through ACK and spawn barriers', async (t) => {
|
||||
const { journal } = await journalFixture(t);
|
||||
const request = {
|
||||
body: {
|
||||
offerId: 'offer-state-machine-1',
|
||||
leaseToken: 'worker_generated_lease_capability_0000000000000002',
|
||||
},
|
||||
};
|
||||
let record = (await journal.acceptOffer(offerFromRequest(request), 1_000)).record;
|
||||
assert.equal(record.state, 'accepted');
|
||||
|
||||
const advance = async (patch) => {
|
||||
const next = normalizeWorkerRemoteExecutionInboxRecord({
|
||||
...record,
|
||||
...patch,
|
||||
revision: record.revision + 1,
|
||||
updatedAtMs: record.updatedAtMs + 1,
|
||||
});
|
||||
await journal.replaceOffer(next, record.revision);
|
||||
record = await journal.readOffer(record.offer.offerId);
|
||||
};
|
||||
|
||||
await advance({ state: 'starting_acknowledged' });
|
||||
await advance({
|
||||
state: 'launching',
|
||||
executorStartedAtMs: 1_002,
|
||||
logArtifactId: 'log-artifact-1',
|
||||
completionReceiptCallbackSequence: 1,
|
||||
completionReceiptTokenDigest: 'b'.repeat(64),
|
||||
});
|
||||
await advance({
|
||||
state: 'started',
|
||||
executorHandle: 'pid:123:boot:abc',
|
||||
executorStartedAtMs: 1_002,
|
||||
logArtifactId: 'log-artifact-1',
|
||||
});
|
||||
await advance({ state: 'running_acknowledged' });
|
||||
|
||||
assert.equal(record.state, 'running_acknowledged');
|
||||
assert.equal(record.revision, 4);
|
||||
assert.equal(record.offer.leaseToken, request.body.leaseToken);
|
||||
assert.equal(record.executorHandle, 'pid:123:boot:abc');
|
||||
assert.equal(record.completionReceiptTokenDigest, 'b'.repeat(64));
|
||||
|
||||
const regressed = normalizeWorkerRemoteExecutionInboxRecord({
|
||||
schemaVersion: 1,
|
||||
revision: record.revision + 1,
|
||||
state: 'starting_acknowledged',
|
||||
offer: record.offer,
|
||||
acceptedAtMs: record.acceptedAtMs,
|
||||
updatedAtMs: record.updatedAtMs + 1,
|
||||
});
|
||||
await assert.rejects(
|
||||
journal.replaceOffer(regressed, record.revision),
|
||||
/invalid_transition/,
|
||||
);
|
||||
await assert.rejects(
|
||||
journal.replaceOffer({ ...record, revision: record.revision + 2 }, record.revision),
|
||||
/offer_revision_conflict/,
|
||||
);
|
||||
assert.equal((await journal.readOffer(record.offer.offerId)).revision, 4);
|
||||
});
|
||||
|
||||
test('lists the single execution inbox authority with a stable bounded cursor', async (t) => {
|
||||
const { journal } = await journalFixture(t);
|
||||
for (const offerId of ['offer-page-a', 'offer-page-b', 'offer-page-c']) {
|
||||
await journal.acceptOffer(offerFromRequest({
|
||||
body: {
|
||||
offerId,
|
||||
leaseToken: `worker_generated_lease_capability_${offerId}`,
|
||||
},
|
||||
}), 1_000);
|
||||
}
|
||||
const first = await journal.listOffers({ limit: 2 });
|
||||
assert.deepEqual(
|
||||
first.records.map((record) => record.offer.offerId),
|
||||
['offer-page-a', 'offer-page-b'],
|
||||
);
|
||||
assert.equal(first.nextAfterOfferId, 'offer-page-b');
|
||||
const second = await journal.listOffers({
|
||||
afterOfferId: first.nextAfterOfferId,
|
||||
limit: 2,
|
||||
});
|
||||
assert.deepEqual(
|
||||
second.records.map((record) => record.offer.offerId),
|
||||
['offer-page-c'],
|
||||
);
|
||||
assert.equal(second.nextAfterOfferId, undefined);
|
||||
await assert.rejects(journal.listOffers({ limit: 65 }), /invalid_configuration/);
|
||||
});
|
||||
|
||||
test('keeps one stable claim through transport loss, bounded backoff and restart', async (t) => {
|
||||
const { journal, rootDirectory } = await journalFixture(t);
|
||||
let now = 1_000;
|
||||
const firstRequests = [];
|
||||
const first = new WorkerRemoteOfferPullCoordinator({
|
||||
journal,
|
||||
currentSession: () => session,
|
||||
now: () => now,
|
||||
random: () => 0.5,
|
||||
backoffBaseMs: 1_000,
|
||||
transport: {
|
||||
async exchange(request) {
|
||||
firstRequests.push(request);
|
||||
throw new Error('response lost');
|
||||
},
|
||||
},
|
||||
});
|
||||
const unavailable = await first.pull(session);
|
||||
assert.equal(unavailable.status, 'unavailable');
|
||||
assert.equal(unavailable.nextAttemptAtMs, 1_500);
|
||||
assert.equal(firstRequests.length, 1);
|
||||
|
||||
now = 1_400;
|
||||
const suppressed = await first.pull(session);
|
||||
assert.equal(suppressed.status, 'backoff');
|
||||
assert.equal(firstRequests.length, 1);
|
||||
|
||||
await journal.releaseOwnership();
|
||||
const resumedJournal = new WorkerRemoteOfferFileJournal({
|
||||
rootDirectory,
|
||||
ownershipStaleMs: 5_000,
|
||||
});
|
||||
await resumedJournal.acquireOwnership();
|
||||
t.after(() => resumedJournal.releaseOwnership().catch(() => undefined));
|
||||
now = 1_500;
|
||||
let resumedRequest;
|
||||
const resumed = new WorkerRemoteOfferPullCoordinator({
|
||||
journal: resumedJournal,
|
||||
currentSession: () => session,
|
||||
now: () => now,
|
||||
random: () => 0,
|
||||
transport: {
|
||||
async exchange(request) {
|
||||
resumedRequest = request;
|
||||
return JSON.stringify(createRemoteExecutionOfferPullBody({
|
||||
status: 'offered',
|
||||
offer: offerFromRequest(request),
|
||||
stats: STATS,
|
||||
truncated: false,
|
||||
}));
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await resumed.pull(session);
|
||||
assert.equal(result.status, 'accepted');
|
||||
assert.equal(resumedRequest.body.offerId, firstRequests[0].body.offerId);
|
||||
assert.equal(resumedRequest.body.leaseToken, firstRequests[0].body.leaseToken);
|
||||
assert.equal(await resumedJournal.readPendingClaim(), undefined);
|
||||
assert.equal(
|
||||
(await resumedJournal.readOffer(resumedRequest.body.offerId)).offer.leaseToken,
|
||||
resumedRequest.body.leaseToken,
|
||||
);
|
||||
});
|
||||
|
||||
test('writes the inbox before clearing the pending claim and rejects target drift', async (t) => {
|
||||
const events = [];
|
||||
let stored;
|
||||
let pending;
|
||||
const journal = {
|
||||
async readPendingClaim() { return pending; },
|
||||
async createPendingClaim(record) { pending = record; return record; },
|
||||
async replacePendingClaim(record) { pending = record; return record; },
|
||||
async clearPendingClaim() { events.push('clear'); pending = undefined; },
|
||||
async acceptOffer(offer, acceptedAtMs) {
|
||||
events.push('accept');
|
||||
stored = { schemaVersion: 1, revision: 0, state: 'accepted', offer, acceptedAtMs, updatedAtMs: acceptedAtMs };
|
||||
return { status: 'accepted', record: stored };
|
||||
},
|
||||
async readOffer() { return stored; },
|
||||
};
|
||||
const coordinator = new WorkerRemoteOfferPullCoordinator({
|
||||
journal,
|
||||
currentSession: () => session,
|
||||
now: () => 1_000,
|
||||
transport: {
|
||||
async exchange(request) {
|
||||
return JSON.stringify(createRemoteExecutionOfferPullBody({
|
||||
status: 'offered',
|
||||
offer: offerFromRequest(request),
|
||||
stats: STATS,
|
||||
truncated: false,
|
||||
}));
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.equal((await coordinator.pull(session)).status, 'accepted');
|
||||
assert.deepEqual(events, ['accept', 'clear']);
|
||||
});
|
||||
|
||||
test('retains the old claim without accepting when the current Session changes', async () => {
|
||||
let current = session;
|
||||
let pending;
|
||||
let accepted = false;
|
||||
const journal = {
|
||||
async readPendingClaim() { return pending; },
|
||||
async createPendingClaim(record) { pending = record; return record; },
|
||||
async replacePendingClaim(record) { pending = record; return record; },
|
||||
async clearPendingClaim() { pending = undefined; },
|
||||
async acceptOffer() { accepted = true; throw new Error('must not accept'); },
|
||||
async readOffer() { return undefined; },
|
||||
};
|
||||
const coordinator = new WorkerRemoteOfferPullCoordinator({
|
||||
journal,
|
||||
currentSession: () => current,
|
||||
now: () => 1_000,
|
||||
random: () => 0,
|
||||
transport: {
|
||||
async exchange(request) {
|
||||
current = { ...session, generation: 3 };
|
||||
return JSON.stringify(createRemoteExecutionOfferPullBody({
|
||||
status: 'offered',
|
||||
offer: offerFromRequest(request),
|
||||
stats: STATS,
|
||||
truncated: false,
|
||||
}));
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await coordinator.pull(session);
|
||||
assert.equal(result.status, 'unavailable');
|
||||
assert.equal(accepted, false);
|
||||
assert.equal(pending.workerGeneration, 2);
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { EventEmitter } = require('node:events');
|
||||
const { PassThrough } = require('node:stream');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WorkerRemoteOfferHttpsTransport,
|
||||
} = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
|
||||
const AUTHORIZATION =
|
||||
`Worker ql3w_worker_primary_${Buffer.alloc(32, 7).toString('base64url')}`;
|
||||
const PATH =
|
||||
'/api/v3/worker-ingress/workers/edge-1/sessions/018f0000-0000-7000-8000-000000000001/offers';
|
||||
|
||||
function requestFactory(responseFactory, observations) {
|
||||
return (options, callback) => {
|
||||
observations.options = options;
|
||||
const request = new EventEmitter();
|
||||
request.setTimeout = (timeout, handler) => {
|
||||
observations.timeout = timeout;
|
||||
observations.timeoutHandler = handler;
|
||||
return request;
|
||||
};
|
||||
request.destroy = (error) => {
|
||||
if (error) queueMicrotask(() => request.emit('error', error));
|
||||
};
|
||||
request.end = (body) => {
|
||||
observations.body = Buffer.from(body);
|
||||
const response = responseFactory();
|
||||
queueMicrotask(() => callback(response));
|
||||
};
|
||||
return request;
|
||||
};
|
||||
}
|
||||
|
||||
function response(body, headers = {}) {
|
||||
const stream = new PassThrough();
|
||||
stream.statusCode = 200;
|
||||
stream.headers = {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(body)),
|
||||
...headers,
|
||||
};
|
||||
queueMicrotask(() => stream.end(body));
|
||||
return stream;
|
||||
}
|
||||
|
||||
function credentials() {
|
||||
return {
|
||||
authorization: AUTHORIZATION,
|
||||
certificateChainPem: 'client certificate',
|
||||
privateKeyPem: 'client private key',
|
||||
trustAnchors: ['trusted ca'],
|
||||
};
|
||||
}
|
||||
|
||||
function request() {
|
||||
return {
|
||||
path: PATH,
|
||||
body: {
|
||||
workerGeneration: 2,
|
||||
offerId: 'offer-1',
|
||||
leaseToken: 'worker_generated_lease_capability_0000000000000001',
|
||||
},
|
||||
maximumResponseBytes: 1024,
|
||||
};
|
||||
}
|
||||
|
||||
test('uses one bounded TLS 1.3 mTLS POST with the Worker credential', async () => {
|
||||
const observations = {};
|
||||
const transport = new WorkerRemoteOfferHttpsTransport({
|
||||
origin: 'https://cluster.example:7443',
|
||||
credentials: { async load() { return credentials(); } },
|
||||
requestTimeoutMs: 5_000,
|
||||
requestFactory: requestFactory(
|
||||
() => response('{"status":"idle"}'),
|
||||
observations,
|
||||
),
|
||||
});
|
||||
try {
|
||||
const result = await transport.exchange(request());
|
||||
assert.equal(Buffer.from(result).toString('utf8'), '{"status":"idle"}');
|
||||
assert.equal(observations.options.protocol, 'https:');
|
||||
assert.equal(observations.options.hostname, 'cluster.example');
|
||||
assert.equal(observations.options.port, '7443');
|
||||
assert.equal(observations.options.minVersion, 'TLSv1.3');
|
||||
assert.equal(observations.options.rejectUnauthorized, true);
|
||||
assert.equal(observations.options.headers.authorization, AUTHORIZATION);
|
||||
assert.equal(observations.options.path, PATH);
|
||||
assert.equal(observations.timeout, 5_000);
|
||||
assert.deepEqual(JSON.parse(observations.body.toString('utf8')), request().body);
|
||||
} finally {
|
||||
transport.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects plaintext origins, malformed credentials and oversized responses', async () => {
|
||||
assert.throws(
|
||||
() => new WorkerRemoteOfferHttpsTransport({
|
||||
origin: 'http://cluster.example',
|
||||
credentials: { async load() { return credentials(); } },
|
||||
}),
|
||||
/invalid_configuration/,
|
||||
);
|
||||
|
||||
const malformed = new WorkerRemoteOfferHttpsTransport({
|
||||
origin: 'https://cluster.example',
|
||||
credentials: {
|
||||
async load() { return { ...credentials(), authorization: 'Bearer token' }; },
|
||||
},
|
||||
requestFactory: requestFactory(
|
||||
() => response('{}'),
|
||||
{},
|
||||
),
|
||||
});
|
||||
await assert.rejects(malformed.exchange(request()), /credentials_unavailable/);
|
||||
malformed.close();
|
||||
|
||||
const observations = {};
|
||||
const oversized = new WorkerRemoteOfferHttpsTransport({
|
||||
origin: 'https://cluster.example',
|
||||
credentials: { async load() { return credentials(); } },
|
||||
requestFactory: requestFactory(() => {
|
||||
const stream = new PassThrough();
|
||||
stream.statusCode = 200;
|
||||
stream.headers = { 'content-type': 'application/json' };
|
||||
queueMicrotask(() => stream.end(Buffer.alloc(1025, 1)));
|
||||
return stream;
|
||||
}, observations),
|
||||
});
|
||||
await assert.rejects(oversized.exchange(request()), /response_too_large/);
|
||||
oversized.close();
|
||||
});
|
||||
|
||||
test('propagates caller cancellation and refuses work after close', async () => {
|
||||
const transport = new WorkerRemoteOfferHttpsTransport({
|
||||
origin: 'https://cluster.example',
|
||||
credentials: { async load() { return credentials(); } },
|
||||
requestFactory: requestFactory(() => response('{}'), {}),
|
||||
});
|
||||
const controller = new AbortController();
|
||||
controller.abort(new Error('shutdown'));
|
||||
await assert.rejects(
|
||||
transport.exchange({ ...request(), signal: controller.signal }),
|
||||
/shutdown/,
|
||||
);
|
||||
transport.close();
|
||||
await assert.rejects(transport.exchange(request()), /closed/);
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
createClusterTaskExecutionRevision,
|
||||
} = require('@qinglong/runtime-core/cluster-execution-revision');
|
||||
const {
|
||||
createClusterRemoteExecutionOffer,
|
||||
} = require('@qinglong/runtime-core/remote-dispatch');
|
||||
const {
|
||||
digestRunDispatchLeaseToken,
|
||||
} = require('@qinglong/runtime-core/run-dispatch-lease');
|
||||
const { createSecretRef } = require('@qinglong/runtime-core/secret-reference');
|
||||
const {
|
||||
WorkerRemoteSecretHttpsProvider,
|
||||
} = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
|
||||
const SESSION_ID = '018f0000-0000-7000-8000-000000000001';
|
||||
const SOURCE_DIGEST = 'a'.repeat(64);
|
||||
const TASK_REVISION = `qltd:v1:1:${SOURCE_DIGEST}`;
|
||||
const LEASE_TOKEN = 'worker_generated_lease_capability_0000000000000001';
|
||||
const SECRET_REF = createSecretRef({ projectId: 'project-1', name: 'token' });
|
||||
|
||||
function acceptedOffer() {
|
||||
const executionRevision = createClusterTaskExecutionRevision({
|
||||
projectId: 'project-1', taskId: 'task-1', taskRevision: TASK_REVISION,
|
||||
sourceRevision: 1, sourceContentDigest: SOURCE_DIGEST,
|
||||
executorType: 'remote_worker', planSchema: 'qinglong/command-execution@v1',
|
||||
command: { kind: 'argv', file: '/bin/true', args: [] },
|
||||
environment: [{ name: 'TOKEN', kind: 'secret', secretRef: SECRET_REF }],
|
||||
createdAtMs: 1,
|
||||
});
|
||||
return createClusterRemoteExecutionOffer({
|
||||
offerId: 'offer-1', deliveryKind: 'new_claim',
|
||||
executionDigest: executionRevision.contentDigest,
|
||||
candidate: {
|
||||
runId: 'run-1', attemptId: 'attempt-1', projectId: 'project-1',
|
||||
taskId: 'task-1', taskRevision: TASK_REVISION, priority: 1,
|
||||
queuedAtMs: 10, attemptCreatedAtMs: 11, attemptNumber: 1,
|
||||
executorType: 'remote_worker',
|
||||
},
|
||||
worker: { workerId: 'edge-1', sessionId: SESSION_ID, generation: 2 },
|
||||
lease: {
|
||||
attemptId: 'attempt-1', runId: 'run-1', status: 'leased', version: 4,
|
||||
leaseGeneration: 3, workerId: 'edge-1', workerSessionId: SESSION_ID,
|
||||
workerGeneration: 2, leaseTokenDigest: digestRunDispatchLeaseToken(LEASE_TOKEN),
|
||||
acquiredAtMs: 20, renewedAtMs: 20, expiresAtMs: 30_020,
|
||||
updatedAtMs: 20,
|
||||
},
|
||||
leaseToken: LEASE_TOKEN, executionRevision, placementScore: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function requestFor(offer) {
|
||||
return {
|
||||
projectId: offer.candidate.projectId,
|
||||
taskId: offer.candidate.taskId,
|
||||
taskRevision: offer.candidate.taskRevision,
|
||||
runId: offer.candidate.runId,
|
||||
attemptId: offer.candidate.attemptId,
|
||||
offerId: offer.offerId,
|
||||
executionDigest: offer.executionDigest,
|
||||
secretRefs: [SECRET_REF],
|
||||
};
|
||||
}
|
||||
|
||||
test('rehydrates lease authority from inbox and delivers one exact Secret batch', async () => {
|
||||
const offer = acceptedOffer();
|
||||
let transport;
|
||||
const provider = new WorkerRemoteSecretHttpsProvider({
|
||||
inbox: {
|
||||
async readOffer(offerId) {
|
||||
assert.equal(offerId, offer.offerId);
|
||||
return { state: 'starting_acknowledged', offer };
|
||||
},
|
||||
},
|
||||
client: {
|
||||
async postJson(request) {
|
||||
transport = request;
|
||||
return Buffer.from(JSON.stringify({
|
||||
schema: 'qinglong/remote-secret-delivery@v1',
|
||||
runId: 'run-1', attemptId: 'attempt-1', offerId: 'offer-1',
|
||||
executionDigest: offer.executionDigest,
|
||||
values: [{ secretRef: SECRET_REF, value: 'resolved-value' }],
|
||||
}));
|
||||
},
|
||||
},
|
||||
});
|
||||
const resolution = await provider.resolve(requestFor(offer));
|
||||
assert.deepEqual(resolution.values, [
|
||||
{ secretRef: SECRET_REF, value: 'resolved-value' },
|
||||
]);
|
||||
assert.equal(transport.path.endsWith(`/sessions/${SESSION_ID}/secrets`), true);
|
||||
assert.equal(transport.body.leaseToken, LEASE_TOKEN);
|
||||
assert.equal(transport.maximumRequestBytes, 64 * 1024);
|
||||
assert.equal(JSON.stringify(requestFor(offer)).includes(LEASE_TOKEN), false);
|
||||
});
|
||||
|
||||
test('rejects a stale inbox identity before sending the capability', async () => {
|
||||
const offer = acceptedOffer();
|
||||
let calls = 0;
|
||||
const provider = new WorkerRemoteSecretHttpsProvider({
|
||||
inbox: {
|
||||
async readOffer() { return { state: 'starting_acknowledged', offer }; },
|
||||
},
|
||||
client: { async postJson() { calls += 1; } },
|
||||
});
|
||||
await assert.rejects(
|
||||
provider.resolve({ ...requestFor(offer), executionDigest: 'b'.repeat(64) }),
|
||||
/authority_mismatch/,
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
});
|
||||
|
||||
test('rejects response authority drift and does not return plaintext', async () => {
|
||||
const offer = acceptedOffer();
|
||||
const provider = new WorkerRemoteSecretHttpsProvider({
|
||||
inbox: {
|
||||
async readOffer() { return { state: 'starting_acknowledged', offer }; },
|
||||
},
|
||||
client: {
|
||||
async postJson() {
|
||||
return Buffer.from(JSON.stringify({
|
||||
schema: 'qinglong/remote-secret-delivery@v1',
|
||||
runId: 'run-other', attemptId: 'attempt-1', offerId: 'offer-1',
|
||||
executionDigest: offer.executionDigest,
|
||||
values: [{ secretRef: SECRET_REF, value: 'must-not-escape' }],
|
||||
}));
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(provider.resolve(requestFor(offer)), /response_invalid/);
|
||||
});
|
||||
|
||||
test('does not fetch Secrets before starting ACK or after the launch barrier', async () => {
|
||||
const offer = acceptedOffer();
|
||||
for (const state of ['accepted', 'launching']) {
|
||||
let calls = 0;
|
||||
const provider = new WorkerRemoteSecretHttpsProvider({
|
||||
inbox: { async readOffer() { return { state, offer }; } },
|
||||
client: { async postJson() { calls += 1; } },
|
||||
});
|
||||
await assert.rejects(provider.resolve(requestFor(offer)), /offer_unavailable/);
|
||||
assert.equal(calls, 0);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
createRemoteWorkerArtifactUploadResponseBody,
|
||||
createRemoteWorkerCompletionResponseBody,
|
||||
parseRemoteWorkerArtifactUploadHeader,
|
||||
} = require('@qinglong/runtime-core/remote-worker-completion');
|
||||
const {
|
||||
WorkerRemoteArtifactHttpsUploader,
|
||||
WorkerRemoteCompletionHttpsError,
|
||||
WorkerRemoteExecutionHttpsCompletionClient,
|
||||
} = require('@qinglong/worker-runtime/completion-transport');
|
||||
|
||||
const SESSION_ID = '018f0000-0000-7000-8000-000000000001';
|
||||
const LEASE_TOKEN = 'worker_generated_lease_capability_0000000000000001';
|
||||
const LOG_ARTIFACT_ID = `wlog-${'a'.repeat(30)}`;
|
||||
const CALLBACK_DIGEST = 'b'.repeat(64);
|
||||
|
||||
function fence() {
|
||||
return {
|
||||
workerId: 'worker-1',
|
||||
workerSessionId: SESSION_ID,
|
||||
workerGeneration: 2,
|
||||
projectId: 'project-1',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
offerId: 'offer-1',
|
||||
leaseGeneration: 3,
|
||||
leaseToken: LEASE_TOKEN,
|
||||
expectedLeaseVersion: 4,
|
||||
};
|
||||
}
|
||||
|
||||
function uploadCommand(content) {
|
||||
return {
|
||||
...fence(),
|
||||
logArtifactId: LOG_ARTIFACT_ID,
|
||||
byteLength: content.byteLength,
|
||||
truncated: false,
|
||||
content: (async function* () { yield content; })(),
|
||||
};
|
||||
}
|
||||
|
||||
function completionCommand(content, overrides = {}) {
|
||||
return {
|
||||
...fence(),
|
||||
callbackSequence: 1,
|
||||
callbackTokenDigest: CALLBACK_DIGEST,
|
||||
result: {
|
||||
outcome: 'succeeded',
|
||||
startedAtMs: 100,
|
||||
finishedAtMs: 200,
|
||||
exitCode: 0,
|
||||
},
|
||||
artifact: {
|
||||
logArtifactId: LOG_ARTIFACT_ID,
|
||||
byteLength: content.byteLength,
|
||||
sha256: createHash('sha256').update(content).digest('hex'),
|
||||
truncated: false,
|
||||
},
|
||||
executorType: 'remote_worker',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('uploads one framed Artifact and verifies exact response authority', async () => {
|
||||
const content = Buffer.from('worker-log');
|
||||
let observed;
|
||||
const uploader = new WorkerRemoteArtifactHttpsUploader({
|
||||
client: {
|
||||
async postStream(request) {
|
||||
const chunks = [];
|
||||
for await (const chunk of request.body) chunks.push(Buffer.from(chunk));
|
||||
const envelope = Buffer.concat(chunks);
|
||||
const headerLength = envelope.readUInt32BE(0);
|
||||
const header = parseRemoteWorkerArtifactUploadHeader(
|
||||
envelope.subarray(4, 4 + headerLength),
|
||||
{ workerId: 'worker-1', workerSessionId: SESSION_ID },
|
||||
);
|
||||
observed = { request, envelope, header, headerLength };
|
||||
return Buffer.from(JSON.stringify(
|
||||
createRemoteWorkerArtifactUploadResponseBody({
|
||||
status: 'stored',
|
||||
projectId: 'project-1',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
logArtifactId: LOG_ARTIFACT_ID,
|
||||
byteLength: content.byteLength,
|
||||
sha256: createHash('sha256').update(content).digest('hex'),
|
||||
truncated: false,
|
||||
}),
|
||||
));
|
||||
},
|
||||
},
|
||||
});
|
||||
const result = await uploader.upload(uploadCommand(content));
|
||||
assert.equal(result.status, 'stored');
|
||||
assert.equal(result.sha256, createHash('sha256').update(content).digest('hex'));
|
||||
assert.equal(
|
||||
observed.request.path,
|
||||
`/api/v3/worker-ingress/workers/worker-1/sessions/${SESSION_ID}/artifacts`,
|
||||
);
|
||||
assert.equal(observed.request.byteLength, observed.envelope.byteLength);
|
||||
assert.deepEqual(
|
||||
observed.envelope.subarray(4 + observed.headerLength),
|
||||
content,
|
||||
);
|
||||
assert.equal(observed.header.leaseToken, LEASE_TOKEN);
|
||||
assert.equal(observed.header.logArtifactId, LOG_ARTIFACT_ID);
|
||||
});
|
||||
|
||||
test('rejects Artifact receipt authority drift', async () => {
|
||||
const content = Buffer.from('log');
|
||||
const uploader = new WorkerRemoteArtifactHttpsUploader({
|
||||
client: {
|
||||
async postStream(request) {
|
||||
for await (const _chunk of request.body) { /* consume */ }
|
||||
return Buffer.from(JSON.stringify(
|
||||
createRemoteWorkerArtifactUploadResponseBody({
|
||||
status: 'stored',
|
||||
projectId: 'project-other',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
logArtifactId: LOG_ARTIFACT_ID,
|
||||
byteLength: content.byteLength,
|
||||
sha256: 'c'.repeat(64),
|
||||
truncated: false,
|
||||
}),
|
||||
));
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
uploader.upload(uploadCommand(content)),
|
||||
(error) =>
|
||||
error instanceof WorkerRemoteCompletionHttpsError &&
|
||||
error.reason === 'response_invalid',
|
||||
);
|
||||
});
|
||||
|
||||
test('posts exact completion JSON and binds the response to the receipt', async () => {
|
||||
const content = Buffer.from('worker-log');
|
||||
let observed;
|
||||
const client = new WorkerRemoteExecutionHttpsCompletionClient({
|
||||
client: {
|
||||
async postJson(request) {
|
||||
observed = request;
|
||||
return Buffer.from(JSON.stringify(
|
||||
createRemoteWorkerCompletionResponseBody({
|
||||
status: 'applied',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
callbackSequence: 1,
|
||||
}),
|
||||
));
|
||||
},
|
||||
},
|
||||
});
|
||||
assert.deepEqual(await client.complete(completionCommand(content)), {
|
||||
status: 'applied',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
callbackSequence: 1,
|
||||
});
|
||||
assert.equal(
|
||||
observed.path,
|
||||
`/api/v3/worker-ingress/workers/worker-1/sessions/${SESSION_ID}/completion`,
|
||||
);
|
||||
assert.equal(observed.body.schema, 'qinglong/remote-worker-completion@v1');
|
||||
assert.equal('workerId' in observed.body, false);
|
||||
assert.equal('workerSessionId' in observed.body, false);
|
||||
assert.equal(observed.body.callbackTokenDigest, CALLBACK_DIGEST);
|
||||
assert.equal(observed.body.leaseToken, LEASE_TOKEN);
|
||||
});
|
||||
|
||||
test('rejects non-Worker execution and response authority drift', async () => {
|
||||
const content = Buffer.from('log');
|
||||
let calls = 0;
|
||||
const client = new WorkerRemoteExecutionHttpsCompletionClient({
|
||||
client: {
|
||||
async postJson() {
|
||||
calls += 1;
|
||||
return Buffer.from(JSON.stringify(
|
||||
createRemoteWorkerCompletionResponseBody({
|
||||
status: 'applied',
|
||||
runId: 'run-other',
|
||||
attemptId: 'attempt-1',
|
||||
callbackSequence: 1,
|
||||
}),
|
||||
));
|
||||
},
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
client.complete(completionCommand(content, { executorType: 'local_process' })),
|
||||
(error) =>
|
||||
error instanceof WorkerRemoteCompletionHttpsError &&
|
||||
error.reason === 'request_invalid',
|
||||
);
|
||||
assert.equal(calls, 0);
|
||||
await assert.rejects(
|
||||
client.complete(completionCommand(content)),
|
||||
(error) =>
|
||||
error instanceof WorkerRemoteCompletionHttpsError &&
|
||||
error.reason === 'response_invalid',
|
||||
);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
@@ -0,0 +1,268 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { readFile } = require('node:fs/promises');
|
||||
const https = require('node:https');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
REMOTE_WORKER_ARTIFACT_CONTENT_TYPE,
|
||||
createRemoteWorkerArtifactUploadResponseBody,
|
||||
createRemoteWorkerCompletionResponseBody,
|
||||
parseRemoteWorkerArtifactUploadHeader,
|
||||
parseRemoteWorkerCompletionRequestBody,
|
||||
} = require('@qinglong/runtime-core/remote-worker-completion');
|
||||
const {
|
||||
createRemoteWorkerLeaseControlResponseBody,
|
||||
parseRemoteWorkerLeaseControlRequestBody,
|
||||
} = require('@qinglong/runtime-core/remote-worker-lease-control');
|
||||
const {
|
||||
WorkerIngressHttpsClient,
|
||||
} = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
const {
|
||||
WorkerRemoteArtifactHttpsUploader,
|
||||
WorkerRemoteExecutionHttpsCompletionClient,
|
||||
} = require('../dist/remote-execution/transport/remoteWorkerCompletionHttpsClient');
|
||||
const {
|
||||
WorkerRemoteLeaseControlHttpsClient,
|
||||
} = require('../dist/remote-execution/transport/remoteWorkerLeaseControlHttpsClient');
|
||||
|
||||
const SESSION_ID = '018f0000-0000-7000-8000-000000000001';
|
||||
const AUTHORIZATION =
|
||||
`Worker ql3w_worker_primary_${Buffer.alloc(32, 7).toString('base64url')}`;
|
||||
const LEASE_TOKEN = 'worker_generated_lease_capability_0000000000000001';
|
||||
const LOG_ARTIFACT_ID = `wlog-${'a'.repeat(30)}`;
|
||||
const CALLBACK_DIGEST = 'b'.repeat(64);
|
||||
const fixtures = path.resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls',
|
||||
);
|
||||
|
||||
async function material(name) {
|
||||
return readFile(path.join(fixtures, name));
|
||||
}
|
||||
|
||||
function fence() {
|
||||
return {
|
||||
workerId: 'worker-1',
|
||||
workerSessionId: SESSION_ID,
|
||||
workerGeneration: 2,
|
||||
projectId: 'project-1',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
offerId: 'offer-1',
|
||||
leaseGeneration: 3,
|
||||
leaseToken: LEASE_TOKEN,
|
||||
expectedLeaseVersion: 4,
|
||||
};
|
||||
}
|
||||
|
||||
function json(response, body) {
|
||||
const serialized = Buffer.from(JSON.stringify(body));
|
||||
response.writeHead(200, {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(serialized.byteLength),
|
||||
});
|
||||
response.end(serialized);
|
||||
}
|
||||
|
||||
test('streams Artifact, completion and lease control over one TLS 1.3 mTLS client', async () => {
|
||||
const [ca, serverCertificate, serverKey, clientCertificate, clientKey] =
|
||||
await Promise.all([
|
||||
material('ca-cert.pem'),
|
||||
material('server-cert.pem'),
|
||||
material('server-key.pem'),
|
||||
material('client-cert.pem'),
|
||||
material('client-key.pem'),
|
||||
]);
|
||||
const content = Buffer.from('first log frame\nsecond log frame\n');
|
||||
const digest = createHash('sha256').update(content).digest('hex');
|
||||
const observations = [];
|
||||
const server = https.createServer({
|
||||
ca,
|
||||
cert: serverCertificate,
|
||||
key: serverKey,
|
||||
minVersion: 'TLSv1.3',
|
||||
maxVersion: 'TLSv1.3',
|
||||
requestCert: true,
|
||||
rejectUnauthorized: true,
|
||||
}, (request, response) => {
|
||||
const chunks = [];
|
||||
request.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
request.on('end', () => {
|
||||
const body = Buffer.concat(chunks);
|
||||
const common = {
|
||||
authorized: request.socket.authorized,
|
||||
protocol: request.socket.getProtocol(),
|
||||
authorization: request.headers.authorization,
|
||||
contentLength: request.headers['content-length'],
|
||||
contentType: request.headers['content-type'],
|
||||
path: request.url,
|
||||
};
|
||||
if (request.url.endsWith('/artifacts')) {
|
||||
const headerLength = body.readUInt32BE(0);
|
||||
const header = parseRemoteWorkerArtifactUploadHeader(
|
||||
body.subarray(4, 4 + headerLength),
|
||||
{ workerId: 'worker-1', workerSessionId: SESSION_ID },
|
||||
);
|
||||
const artifact = body.subarray(4 + headerLength);
|
||||
observations.push({ ...common, header, artifact: artifact.toString() });
|
||||
json(response, createRemoteWorkerArtifactUploadResponseBody({
|
||||
status: 'stored',
|
||||
projectId: header.projectId,
|
||||
runId: header.runId,
|
||||
attemptId: header.attemptId,
|
||||
logArtifactId: header.logArtifactId,
|
||||
byteLength: artifact.byteLength,
|
||||
sha256: createHash('sha256').update(artifact).digest('hex'),
|
||||
truncated: header.truncated,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
if (request.url.endsWith('/lease-control')) {
|
||||
const control = parseRemoteWorkerLeaseControlRequestBody(
|
||||
JSON.parse(body.toString('utf8')),
|
||||
{ workerId: 'worker-1', workerSessionId: SESSION_ID },
|
||||
);
|
||||
observations.push({ ...common, control });
|
||||
json(response, createRemoteWorkerLeaseControlResponseBody({
|
||||
status: 'renewed',
|
||||
projectId: control.projectId,
|
||||
runId: control.runId,
|
||||
attemptId: control.attemptId,
|
||||
offerId: control.offerId,
|
||||
leaseGeneration: control.leaseGeneration,
|
||||
leaseVersion: control.expectedLeaseVersion + 1,
|
||||
renewedAtMs: 1_000,
|
||||
expiresAtMs: 31_000,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
const completion = parseRemoteWorkerCompletionRequestBody(
|
||||
JSON.parse(body.toString('utf8')),
|
||||
{ workerId: 'worker-1', workerSessionId: SESSION_ID },
|
||||
);
|
||||
observations.push({ ...common, completion });
|
||||
json(response, createRemoteWorkerCompletionResponseBody({
|
||||
status: 'applied',
|
||||
runId: completion.runId,
|
||||
attemptId: completion.attemptId,
|
||||
callbackSequence: completion.callbackSequence,
|
||||
}));
|
||||
});
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
server.once('error', reject);
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
const address = server.address();
|
||||
assert.ok(address && typeof address === 'object');
|
||||
const shared = new WorkerIngressHttpsClient({
|
||||
origin: `https://127.0.0.1:${address.port}`,
|
||||
credentials: {
|
||||
async load() {
|
||||
return {
|
||||
authorization: AUTHORIZATION,
|
||||
certificateChainPem: clientCertificate,
|
||||
privateKeyPem: clientKey,
|
||||
trustAnchors: [ca],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
try {
|
||||
const uploader = new WorkerRemoteArtifactHttpsUploader({ client: shared });
|
||||
const completion = new WorkerRemoteExecutionHttpsCompletionClient({
|
||||
client: shared,
|
||||
});
|
||||
const leaseControl = new WorkerRemoteLeaseControlHttpsClient({
|
||||
client: shared,
|
||||
});
|
||||
const artifact = await uploader.upload({
|
||||
...fence(),
|
||||
logArtifactId: LOG_ARTIFACT_ID,
|
||||
byteLength: content.byteLength,
|
||||
truncated: false,
|
||||
content: (async function* () {
|
||||
yield content.subarray(0, 7);
|
||||
yield content.subarray(7);
|
||||
})(),
|
||||
});
|
||||
assert.deepEqual(artifact, {
|
||||
status: 'stored',
|
||||
logArtifactId: LOG_ARTIFACT_ID,
|
||||
byteLength: content.byteLength,
|
||||
sha256: digest,
|
||||
});
|
||||
assert.deepEqual(await completion.complete({
|
||||
...fence(),
|
||||
callbackSequence: 1,
|
||||
callbackTokenDigest: CALLBACK_DIGEST,
|
||||
result: {
|
||||
outcome: 'succeeded',
|
||||
startedAtMs: 100,
|
||||
finishedAtMs: 200,
|
||||
exitCode: 0,
|
||||
},
|
||||
artifact: {
|
||||
logArtifactId: LOG_ARTIFACT_ID,
|
||||
byteLength: content.byteLength,
|
||||
sha256: digest,
|
||||
truncated: false,
|
||||
},
|
||||
executorType: 'remote_worker',
|
||||
}), {
|
||||
status: 'applied',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
callbackSequence: 1,
|
||||
});
|
||||
assert.deepEqual(await leaseControl.control(fence()), {
|
||||
status: 'renewed',
|
||||
projectId: 'project-1',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
offerId: 'offer-1',
|
||||
leaseGeneration: 3,
|
||||
leaseVersion: 5,
|
||||
renewedAtMs: 1_000,
|
||||
expiresAtMs: 31_000,
|
||||
});
|
||||
assert.equal(observations.length, 3);
|
||||
assert.deepEqual(observations.map((value) => ({
|
||||
authorized: value.authorized,
|
||||
protocol: value.protocol,
|
||||
authorization: value.authorization,
|
||||
})), [
|
||||
{ authorized: true, protocol: 'TLSv1.3', authorization: AUTHORIZATION },
|
||||
{ authorized: true, protocol: 'TLSv1.3', authorization: AUTHORIZATION },
|
||||
{ authorized: true, protocol: 'TLSv1.3', authorization: AUTHORIZATION },
|
||||
]);
|
||||
assert.equal(
|
||||
observations[0].path,
|
||||
`/api/v3/worker-ingress/workers/worker-1/sessions/${SESSION_ID}/artifacts`,
|
||||
);
|
||||
assert.equal(
|
||||
observations[0].contentType,
|
||||
REMOTE_WORKER_ARTIFACT_CONTENT_TYPE,
|
||||
);
|
||||
assert.equal(observations[0].artifact, content.toString());
|
||||
assert.equal(observations[0].header.leaseToken, LEASE_TOKEN);
|
||||
assert.equal(
|
||||
observations[1].path,
|
||||
`/api/v3/worker-ingress/workers/worker-1/sessions/${SESSION_ID}/completion`,
|
||||
);
|
||||
assert.equal(observations[1].contentType, 'application/json');
|
||||
assert.equal(observations[1].completion.callbackTokenDigest, CALLBACK_DIGEST);
|
||||
assert.equal(observations[1].completion.artifact.sha256, digest);
|
||||
assert.equal(
|
||||
observations[2].path,
|
||||
`/api/v3/worker-ingress/workers/worker-1/sessions/${SESSION_ID}/lease-control`,
|
||||
);
|
||||
assert.equal(observations[2].control.leaseToken, LEASE_TOKEN);
|
||||
} finally {
|
||||
shared.close();
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { EventEmitter } = require('node:events');
|
||||
const { PassThrough } = require('node:stream');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WorkerIngressHttpsClient,
|
||||
WorkerRemoteLeaseControlHttpsClient,
|
||||
} = require('../dist/remote-execution/remoteOfferDeliveryEntrypoint');
|
||||
|
||||
const SESSION_ID = '018f0000-0000-7000-8000-000000000001';
|
||||
const LEASE_TOKEN = 'worker_generated_lease_capability_0000000000000001';
|
||||
const AUTHORIZATION =
|
||||
`Worker ql3w_worker_primary_${Buffer.alloc(32, 7).toString('base64url')}`;
|
||||
|
||||
function command(overrides = {}) {
|
||||
return {
|
||||
workerId: 'edge-1', workerSessionId: SESSION_ID, workerGeneration: 2,
|
||||
projectId: 'project-1', runId: 'run-1', attemptId: 'attempt-1',
|
||||
offerId: 'offer-1', leaseGeneration: 3, leaseToken: LEASE_TOKEN,
|
||||
expectedLeaseVersion: 4, ...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function responseBody(overrides = {}) {
|
||||
return {
|
||||
schema: 'qinglong/remote-worker-lease-control@v1',
|
||||
status: 'renewed', projectId: 'project-1', runId: 'run-1',
|
||||
attemptId: 'attempt-1', offerId: 'offer-1', leaseGeneration: 3,
|
||||
leaseVersion: 5, renewedAtMs: 10_000, expiresAtMs: 40_000,
|
||||
stop: null, terminalStatus: null, ...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function response(value) {
|
||||
const serialized = JSON.stringify(value);
|
||||
const stream = new PassThrough();
|
||||
stream.statusCode = 200;
|
||||
stream.headers = {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(serialized)),
|
||||
};
|
||||
queueMicrotask(() => stream.end(serialized));
|
||||
return stream;
|
||||
}
|
||||
|
||||
function fixture(responseFactory = () => responseBody()) {
|
||||
const observations = [];
|
||||
const shared = new WorkerIngressHttpsClient({
|
||||
origin: 'https://cluster.example:7443',
|
||||
credentials: { async load() {
|
||||
return {
|
||||
authorization: AUTHORIZATION,
|
||||
certificateChainPem: 'client certificate',
|
||||
privateKeyPem: 'client private key',
|
||||
trustAnchors: ['trusted ca'],
|
||||
};
|
||||
} },
|
||||
requestFactory(options, callback) {
|
||||
const request = new EventEmitter();
|
||||
request.setTimeout = () => request;
|
||||
request.destroy = (error) => {
|
||||
if (error) queueMicrotask(() => request.emit('error', error));
|
||||
};
|
||||
request.end = (body) => {
|
||||
observations.push({
|
||||
path: options.path,
|
||||
body: JSON.parse(Buffer.from(body).toString('utf8')),
|
||||
});
|
||||
queueMicrotask(() => callback(response(responseFactory())));
|
||||
};
|
||||
return request;
|
||||
},
|
||||
});
|
||||
return {
|
||||
observations,
|
||||
shared,
|
||||
client: new WorkerRemoteLeaseControlHttpsClient({ client: shared }),
|
||||
};
|
||||
}
|
||||
|
||||
test('posts a path-bound fence and accepts only the next lease version', async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
assert.deepEqual(await f.client.control(command()), {
|
||||
status: 'renewed', projectId: 'project-1', runId: 'run-1',
|
||||
attemptId: 'attempt-1', offerId: 'offer-1', leaseGeneration: 3,
|
||||
leaseVersion: 5, renewedAtMs: 10_000, expiresAtMs: 40_000,
|
||||
});
|
||||
assert.equal(f.observations[0].path,
|
||||
`/api/v3/worker-ingress/workers/edge-1/sessions/${SESSION_ID}/lease-control`);
|
||||
assert.equal('workerId' in f.observations[0].body, false);
|
||||
assert.equal('workerSessionId' in f.observations[0].body, false);
|
||||
assert.equal(f.observations[0].body.leaseToken, LEASE_TOKEN);
|
||||
} finally { f.shared.close(); }
|
||||
});
|
||||
|
||||
test('accepts a durable stop request after the lease is renewed', async () => {
|
||||
const f = fixture(() => responseBody({
|
||||
status: 'stop_requested',
|
||||
stop: { reason: 'user', requestedAtMs: 9_000 },
|
||||
}));
|
||||
try {
|
||||
const result = await f.client.control(command());
|
||||
assert.equal(result.status, 'stop_requested');
|
||||
assert.deepEqual(result.stop, { reason: 'user', requestedAtMs: 9_000 });
|
||||
} finally { f.shared.close(); }
|
||||
});
|
||||
|
||||
test('rejects response identity or lease-version drift', async () => {
|
||||
for (const drift of [
|
||||
{ runId: 'run-other' },
|
||||
{ leaseVersion: 6 },
|
||||
]) {
|
||||
const f = fixture(() => responseBody(drift));
|
||||
try {
|
||||
await assert.rejects(f.client.control(command()), /response_invalid/);
|
||||
} finally { f.shared.close(); }
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects invalid requests before any transport access', async () => {
|
||||
const f = fixture();
|
||||
try {
|
||||
await assert.rejects(
|
||||
f.client.control(command({ leaseToken: 'short' })),
|
||||
/request_invalid/,
|
||||
);
|
||||
assert.equal(f.observations.length, 0);
|
||||
} finally { f.shared.close(); }
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { mkdtemp, rm } = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
|
||||
const {
|
||||
WorkerCertificateFileStore,
|
||||
} = require('../dist/credential/workerCertificateStore');
|
||||
const {
|
||||
WorkerCertificateRenewalCoordinator,
|
||||
} = require('../dist/credential/workerCertificateRenewal');
|
||||
const {
|
||||
createCertificateAuthority,
|
||||
} = require('./helpers/certificateAuthority.cjs');
|
||||
|
||||
const HOUR_MS = 60 * 60_000;
|
||||
const DAY_MS = 24 * HOUR_MS;
|
||||
|
||||
async function fixture(t) {
|
||||
const now = Date.now();
|
||||
const parent = await mkdtemp(path.join(os.tmpdir(), 'ql3-worker-renewal-'));
|
||||
t.after(() => rm(parent, { recursive: true, force: true }));
|
||||
return {
|
||||
now,
|
||||
ca: await createCertificateAuthority({ now }),
|
||||
store: new WorkerCertificateFileStore({
|
||||
rootDirectory: path.join(parent, 'identity'),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function coordinator(options) {
|
||||
return new WorkerCertificateRenewalCoordinator({
|
||||
workerId: 'worker-renewal-01',
|
||||
store: options.store,
|
||||
issuer: options.issuer,
|
||||
trustAnchors: { load: async () => [options.ca.certificatePem] },
|
||||
now: () => options.now,
|
||||
random: () => 0,
|
||||
policy: {
|
||||
renewBeforeMs: HOUR_MS,
|
||||
minimumIssuedValidityMs: 2 * HOUR_MS,
|
||||
backoffBaseMs: 10_000,
|
||||
backoffMaximumMs: 60_000,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test('coalesces enrollment and leaves a fresh identity timer-free', async (t) => {
|
||||
const context = await fixture(t);
|
||||
let issueCalls = 0;
|
||||
const renewal = coordinator({
|
||||
...context,
|
||||
issuer: {
|
||||
async issue({ certificateSigningRequestPem }) {
|
||||
issueCalls += 1;
|
||||
return {
|
||||
certificateChainPem: await context.ca.issue(
|
||||
certificateSigningRequestPem,
|
||||
{ notAfterMs: context.now + 30 * DAY_MS },
|
||||
),
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const firstRun = renewal.run();
|
||||
const coalescedRun = renewal.run();
|
||||
assert.equal(firstRun, coalescedRun);
|
||||
const result = await firstRun;
|
||||
|
||||
assert.equal(result.status, 'renewed');
|
||||
assert.equal(issueCalls, 1);
|
||||
const next = await renewal.run();
|
||||
assert.equal(next.status, 'not_due');
|
||||
assert.equal(issueCalls, 1);
|
||||
});
|
||||
|
||||
test('persists bounded backoff and suppresses repeated CA attempts', async (t) => {
|
||||
const context = await fixture(t);
|
||||
let issueCalls = 0;
|
||||
const renewal = coordinator({
|
||||
...context,
|
||||
issuer: {
|
||||
async issue() {
|
||||
issueCalls += 1;
|
||||
throw new Error('CA unavailable');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const failed = await renewal.run();
|
||||
assert.equal(failed.status, 'unavailable');
|
||||
assert.equal(failed.nextAttemptAtMs, context.now + 5_000);
|
||||
assert.equal(issueCalls, 1);
|
||||
|
||||
const suppressed = await renewal.run();
|
||||
assert.equal(suppressed.status, 'unavailable');
|
||||
assert.equal(suppressed.nextAttemptAtMs, context.now + 5_000);
|
||||
assert.equal(issueCalls, 1);
|
||||
assert.deepEqual(await context.store.readRenewalState(), {
|
||||
consecutiveFailures: 1,
|
||||
nextAttemptAtMs: context.now + 5_000,
|
||||
lastAttemptAtMs: context.now,
|
||||
lastSuccessAtMs: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('does not convert caller cancellation into a renewal failure', async (t) => {
|
||||
const context = await fixture(t);
|
||||
const renewal = coordinator({
|
||||
...context,
|
||||
issuer: {
|
||||
async issue() {
|
||||
throw new Error('must not run');
|
||||
},
|
||||
},
|
||||
});
|
||||
const controller = new AbortController();
|
||||
controller.abort(new Error('shutdown'));
|
||||
|
||||
await assert.rejects(renewal.run(controller.signal), /shutdown/);
|
||||
assert.deepEqual(await context.store.readRenewalState(), {
|
||||
consecutiveFailures: 0,
|
||||
nextAttemptAtMs: null,
|
||||
lastAttemptAtMs: null,
|
||||
lastSuccessAtMs: null,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,280 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
createClusterTaskExecutionRevision,
|
||||
} = require('@qinglong/runtime-core/cluster-execution-revision');
|
||||
const {
|
||||
createClusterRemoteExecutionOffer,
|
||||
} = require('@qinglong/runtime-core/remote-dispatch');
|
||||
const {
|
||||
digestRunDispatchLeaseToken,
|
||||
} = require('@qinglong/runtime-core/run-dispatch-lease');
|
||||
const {
|
||||
assertWorkerRemoteExecutionInboxTransition,
|
||||
createWorkerRemoteExecutionInboxRecord,
|
||||
} = require('../dist/remote-execution/executionInbox');
|
||||
const {
|
||||
WorkerRemoteCompletionCoordinator,
|
||||
} = require('../dist/execution/workerCompletionCoordinator');
|
||||
|
||||
const RUN_ID = '019f70e0-0000-7000-8000-000000000201';
|
||||
const ATTEMPT_ID = '019f70e0-0000-7000-8000-000000000202';
|
||||
const SESSION_ID = '019f70e0-0000-7000-8000-000000000203';
|
||||
const TOKEN = Buffer.alloc(32, 0x41);
|
||||
const TOKEN_DIGEST = createHash('sha256').update(TOKEN).digest('hex');
|
||||
const LEASE_TOKEN = 'worker_generated_lease_capability_0000000000000009';
|
||||
const SOURCE_DIGEST = 'a'.repeat(64);
|
||||
const TASK_REVISION = `qltd:v1:1:${SOURCE_DIGEST}`;
|
||||
const LOG_ID = `wlog-${'b'.repeat(30)}`;
|
||||
|
||||
function offer() {
|
||||
const executionRevision = createClusterTaskExecutionRevision({
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
taskRevision: TASK_REVISION,
|
||||
sourceRevision: 1,
|
||||
sourceContentDigest: SOURCE_DIGEST,
|
||||
executorType: 'remote_worker',
|
||||
planSchema: 'qinglong/command-execution@v1',
|
||||
command: { kind: 'argv', file: '/bin/true', args: [] },
|
||||
environment: [],
|
||||
createdAtMs: 1,
|
||||
});
|
||||
return createClusterRemoteExecutionOffer({
|
||||
offerId: 'offer-completion-1',
|
||||
deliveryKind: 'new_claim',
|
||||
executionDigest: executionRevision.contentDigest,
|
||||
candidate: {
|
||||
runId: RUN_ID,
|
||||
attemptId: ATTEMPT_ID,
|
||||
projectId: 'project-1',
|
||||
taskId: 'task-1',
|
||||
taskRevision: TASK_REVISION,
|
||||
priority: 1,
|
||||
queuedAtMs: 10,
|
||||
attemptCreatedAtMs: 11,
|
||||
attemptNumber: 1,
|
||||
executorType: 'remote_worker',
|
||||
},
|
||||
worker: { workerId: 'edge-1', sessionId: SESSION_ID, generation: 2 },
|
||||
lease: {
|
||||
attemptId: ATTEMPT_ID,
|
||||
runId: RUN_ID,
|
||||
status: 'leased',
|
||||
version: 0,
|
||||
leaseGeneration: 1,
|
||||
workerId: 'edge-1',
|
||||
workerSessionId: SESSION_ID,
|
||||
workerGeneration: 2,
|
||||
leaseTokenDigest: digestRunDispatchLeaseToken(LEASE_TOKEN),
|
||||
acquiredAtMs: 20,
|
||||
renewedAtMs: 20,
|
||||
expiresAtMs: 30_020,
|
||||
updatedAtMs: 20,
|
||||
},
|
||||
leaseToken: LEASE_TOKEN,
|
||||
executionRevision,
|
||||
placementScore: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function launchingRecord() {
|
||||
const accepted = createWorkerRemoteExecutionInboxRecord(offer(), 100);
|
||||
const starting = {
|
||||
...accepted,
|
||||
revision: 1,
|
||||
state: 'starting_acknowledged',
|
||||
updatedAtMs: 101,
|
||||
};
|
||||
const launching = {
|
||||
...starting,
|
||||
revision: 2,
|
||||
state: 'launching',
|
||||
updatedAtMs: 102,
|
||||
executorStartedAtMs: 100,
|
||||
logArtifactId: LOG_ID,
|
||||
completionReceiptCallbackSequence: 1,
|
||||
completionReceiptTokenDigest: TOKEN_DIGEST,
|
||||
};
|
||||
assertWorkerRemoteExecutionInboxTransition(accepted, starting);
|
||||
assertWorkerRemoteExecutionInboxTransition(starting, launching);
|
||||
return launching;
|
||||
}
|
||||
|
||||
function harness(overrides = {}) {
|
||||
let record = launchingRecord();
|
||||
let removed = 0;
|
||||
let uploaded = false;
|
||||
let completed = false;
|
||||
let artifactClosed = false;
|
||||
const receipt = {
|
||||
schemaVersion: 1,
|
||||
runId: RUN_ID,
|
||||
attemptId: ATTEMPT_ID,
|
||||
callbackSequence: 1,
|
||||
token: TOKEN.toString('base64url'),
|
||||
startedAtMs: 100,
|
||||
finishedAtMs: 200,
|
||||
exitCode: 0,
|
||||
...overrides.receipt,
|
||||
};
|
||||
const inbox = {
|
||||
async readOffer(id) { return id === record.offer.offerId ? record : undefined; },
|
||||
async replaceOffer(next, expectedRevision) {
|
||||
assert.equal(expectedRevision, record.revision);
|
||||
assertWorkerRemoteExecutionInboxTransition(record, next);
|
||||
record = next;
|
||||
},
|
||||
};
|
||||
const receipts = {
|
||||
async read() {
|
||||
if (overrides.readReceipt) return overrides.readReceipt();
|
||||
return receipt;
|
||||
},
|
||||
async remove() {
|
||||
assert.equal(record.state, 'completion_acknowledged');
|
||||
removed += 1;
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const artifacts = {
|
||||
async open() {
|
||||
return {
|
||||
logArtifactId: LOG_ID,
|
||||
byteLength: 3,
|
||||
truncated: false,
|
||||
async *chunks() { yield Buffer.from('log'); },
|
||||
async close() { artifactClosed = true; },
|
||||
};
|
||||
},
|
||||
};
|
||||
const uploader = {
|
||||
async upload(command) {
|
||||
assert.equal(command.workerId, 'edge-1');
|
||||
assert.equal(command.workerSessionId, SESSION_ID);
|
||||
assert.equal(command.workerGeneration, 2);
|
||||
assert.equal(command.offerId, 'offer-completion-1');
|
||||
assert.equal(command.leaseGeneration, 1);
|
||||
assert.equal(command.leaseToken, LEASE_TOKEN);
|
||||
assert.equal(command.expectedLeaseVersion, 0);
|
||||
const chunks = [];
|
||||
for await (const chunk of command.content) chunks.push(chunk);
|
||||
const body = Buffer.concat(chunks);
|
||||
assert.equal(body.toString(), 'log');
|
||||
uploaded = true;
|
||||
if (overrides.upload) return overrides.upload(command, body);
|
||||
return {
|
||||
status: 'stored',
|
||||
logArtifactId: command.logArtifactId,
|
||||
byteLength: body.length,
|
||||
sha256: createHash('sha256').update(body).digest('hex'),
|
||||
};
|
||||
},
|
||||
};
|
||||
const completion = {
|
||||
async complete(command) {
|
||||
assert.equal(uploaded, true);
|
||||
assert.equal(record.state, 'launching');
|
||||
assert.equal(removed, 0);
|
||||
assert.equal(command.callbackTokenDigest, TOKEN_DIGEST);
|
||||
assert.equal(command.artifact.logArtifactId, LOG_ID);
|
||||
completed = true;
|
||||
return overrides.complete?.(command) ?? {
|
||||
status: 'applied',
|
||||
runId: RUN_ID,
|
||||
attemptId: ATTEMPT_ID,
|
||||
callbackSequence: 1,
|
||||
};
|
||||
},
|
||||
};
|
||||
const coordinator = new WorkerRemoteCompletionCoordinator(
|
||||
inbox,
|
||||
receipts,
|
||||
artifacts,
|
||||
uploader,
|
||||
completion,
|
||||
{
|
||||
currentSession: () => ({
|
||||
workerId: 'edge-1',
|
||||
sessionId: SESSION_ID,
|
||||
generation: 2,
|
||||
status: 'available',
|
||||
leaseExpiresAtMs: 30_000,
|
||||
}),
|
||||
now: () => 1_000,
|
||||
},
|
||||
);
|
||||
return {
|
||||
coordinator,
|
||||
record: () => record,
|
||||
removed: () => removed,
|
||||
uploaded: () => uploaded,
|
||||
completed: () => completed,
|
||||
artifactClosed: () => artifactClosed,
|
||||
};
|
||||
}
|
||||
|
||||
test('uploads before completion and deletes the receipt only after durable ACK', async () => {
|
||||
const fixture = harness();
|
||||
const result = await fixture.coordinator.recover('offer-completion-1');
|
||||
assert.deepEqual(result, {
|
||||
offerId: 'offer-completion-1',
|
||||
status: 'completion_acknowledged',
|
||||
receiptCleanup: 'removed',
|
||||
});
|
||||
assert.equal(fixture.record().state, 'completion_acknowledged');
|
||||
assert.equal(fixture.uploaded(), true);
|
||||
assert.equal(fixture.completed(), true);
|
||||
assert.equal(fixture.removed(), 1);
|
||||
assert.equal(fixture.artifactClosed(), true);
|
||||
});
|
||||
|
||||
test('recovers a receipt from the durable launching crash window', async () => {
|
||||
const fixture = harness();
|
||||
await fixture.coordinator.recover('offer-completion-1');
|
||||
assert.equal(fixture.record().executorHandle, undefined);
|
||||
assert.equal(fixture.record().executorStartedAtMs, 100);
|
||||
assert.equal(fixture.record().state, 'completion_acknowledged');
|
||||
});
|
||||
|
||||
test('rejects a non-matching raw capability before upload', async () => {
|
||||
const fixture = harness({
|
||||
receipt: { token: Buffer.alloc(32, 0x42).toString('base64url') },
|
||||
});
|
||||
assert.deepEqual(await fixture.coordinator.recover('offer-completion-1'), {
|
||||
offerId: 'offer-completion-1',
|
||||
status: 'receipt_invalid',
|
||||
});
|
||||
assert.equal(fixture.uploaded(), false);
|
||||
assert.equal(fixture.completed(), false);
|
||||
assert.equal(fixture.removed(), 0);
|
||||
});
|
||||
|
||||
test('distinguishes unavailable receipt storage from invalid evidence', async () => {
|
||||
const fixture = harness({
|
||||
readReceipt() { throw new Error('storage unavailable'); },
|
||||
});
|
||||
assert.deepEqual(await fixture.coordinator.recover('offer-completion-1'), {
|
||||
offerId: 'offer-completion-1',
|
||||
status: 'receipt_unavailable',
|
||||
});
|
||||
assert.equal(fixture.uploaded(), false);
|
||||
assert.equal(fixture.removed(), 0);
|
||||
});
|
||||
|
||||
test('keeps durable evidence when upload fails', async () => {
|
||||
const fixture = harness({
|
||||
upload() { throw new Error('network unavailable'); },
|
||||
});
|
||||
await assert.rejects(
|
||||
fixture.coordinator.recover('offer-completion-1'),
|
||||
/network unavailable/,
|
||||
);
|
||||
assert.equal(fixture.record().state, 'launching');
|
||||
assert.equal(fixture.completed(), false);
|
||||
assert.equal(fixture.removed(), 0);
|
||||
assert.equal(fixture.artifactClosed(), true);
|
||||
});
|
||||
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WorkerExecutionCapacityOracle,
|
||||
} = require('../dist/session/workerExecutionCapacityOracle');
|
||||
|
||||
function record(offerId, state) {
|
||||
return { state, offer: { offerId } };
|
||||
}
|
||||
|
||||
function fixture(records = [], pending) {
|
||||
const journal = {
|
||||
async listOffers() {
|
||||
return { records, nextAfterOfferId: undefined };
|
||||
},
|
||||
async readPendingClaim() { return pending; },
|
||||
};
|
||||
return new WorkerExecutionCapacityOracle({
|
||||
journal,
|
||||
maxConcurrentRuns: 4,
|
||||
});
|
||||
}
|
||||
|
||||
test('publishes zero until startup reconciliation authorizes registration', async () => {
|
||||
const oracle = fixture();
|
||||
assert.equal(oracle.mode(), 'reconciling');
|
||||
assert.equal(await oracle.availableSlots(), 0);
|
||||
oracle.prepareRegistration();
|
||||
assert.equal(await oracle.availableSlots(), 4);
|
||||
oracle.activate();
|
||||
assert.equal(await oracle.availableSlots(), 4);
|
||||
});
|
||||
|
||||
test('subtracts durable active records and the current pull reservation', async () => {
|
||||
const oracle = fixture([
|
||||
record('offer-1', 'running_acknowledged'),
|
||||
record('offer-2', 'completion_acknowledged'),
|
||||
], { offerId: 'offer-3' });
|
||||
oracle.prepareRegistration();
|
||||
assert.equal(await oracle.availableSlots(), 2);
|
||||
});
|
||||
|
||||
test('does not double count a reservation already admitted to the inbox', async () => {
|
||||
const oracle = fixture([
|
||||
record('offer-1', 'accepted'),
|
||||
], { offerId: 'offer-1' });
|
||||
oracle.prepareRegistration();
|
||||
assert.equal(await oracle.availableSlots(), 3);
|
||||
});
|
||||
|
||||
test('fails closed on recovery and remains zero throughout drain', async () => {
|
||||
const recovery = fixture([record('offer-1', 'recovery_required')]);
|
||||
recovery.prepareRegistration();
|
||||
assert.equal(await recovery.availableSlots(), 0);
|
||||
assert.equal(recovery.mode(), 'recovery_required');
|
||||
|
||||
const draining = fixture();
|
||||
draining.prepareRegistration();
|
||||
draining.activate();
|
||||
draining.beginDrain();
|
||||
assert.equal(await draining.availableSlots(), 0);
|
||||
draining.offline();
|
||||
assert.equal(await draining.availableSlots(), 0);
|
||||
draining.beginDrain();
|
||||
draining.offline();
|
||||
assert.equal(draining.mode(), 'offline');
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { createHash } = require('node:crypto');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
createClusterTaskExecutionRevision,
|
||||
} = require('@qinglong/runtime-core/cluster-execution-revision');
|
||||
const {
|
||||
createClusterRemoteExecutionOffer,
|
||||
} = require('@qinglong/runtime-core/remote-dispatch');
|
||||
const {
|
||||
digestRunDispatchLeaseToken,
|
||||
} = require('@qinglong/runtime-core/run-dispatch-lease');
|
||||
const {
|
||||
assertWorkerRemoteExecutionInboxTransition,
|
||||
createWorkerRemoteExecutionInboxRecord,
|
||||
} = require('../dist/remote-execution/executionInbox');
|
||||
const {
|
||||
WorkerRemoteExecutionControlCoordinator,
|
||||
} = require('../dist/execution/workerExecutionControlCoordinator');
|
||||
|
||||
const RUN_ID = '019f70e0-0000-7000-8000-000000000301';
|
||||
const ATTEMPT_ID = '019f70e0-0000-7000-8000-000000000302';
|
||||
const SESSION_ID = '019f70e0-0000-7000-8000-000000000303';
|
||||
const LEASE_TOKEN = 'worker_generated_lease_capability_0000000000000010';
|
||||
const SOURCE_DIGEST = 'a'.repeat(64);
|
||||
const TASK_REVISION = `qltd:v1:1:${SOURCE_DIGEST}`;
|
||||
const LOG_ID = `wlog-${'b'.repeat(30)}`;
|
||||
const RECEIPT_DIGEST = createHash('sha256').update(Buffer.alloc(32, 1)).digest('hex');
|
||||
|
||||
function offer(expiresAtMs = 30_020) {
|
||||
const executionRevision = createClusterTaskExecutionRevision({
|
||||
projectId: 'project-1', taskId: 'task-1', taskRevision: TASK_REVISION,
|
||||
sourceRevision: 1, sourceContentDigest: SOURCE_DIGEST,
|
||||
executorType: 'remote_worker', planSchema: 'qinglong/command-execution@v1',
|
||||
command: { kind: 'argv', file: '/bin/true', args: [] },
|
||||
environment: [], createdAtMs: 1,
|
||||
});
|
||||
return createClusterRemoteExecutionOffer({
|
||||
offerId: 'offer-control-1', deliveryKind: 'new_claim',
|
||||
executionDigest: executionRevision.contentDigest,
|
||||
candidate: {
|
||||
runId: RUN_ID, attemptId: ATTEMPT_ID, projectId: 'project-1',
|
||||
taskId: 'task-1', taskRevision: TASK_REVISION, priority: 1,
|
||||
queuedAtMs: 10, attemptCreatedAtMs: 11, attemptNumber: 1,
|
||||
executorType: 'remote_worker',
|
||||
},
|
||||
worker: { workerId: 'edge-1', sessionId: SESSION_ID, generation: 2 },
|
||||
lease: {
|
||||
attemptId: ATTEMPT_ID, runId: RUN_ID, status: 'leased', version: 0,
|
||||
leaseGeneration: 1, workerId: 'edge-1', workerSessionId: SESSION_ID,
|
||||
workerGeneration: 2, leaseTokenDigest: digestRunDispatchLeaseToken(LEASE_TOKEN),
|
||||
acquiredAtMs: 20, renewedAtMs: 20, expiresAtMs, updatedAtMs: 20,
|
||||
},
|
||||
leaseToken: LEASE_TOKEN, executionRevision, placementScore: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function runningRecord(expiresAtMs) {
|
||||
const accepted = createWorkerRemoteExecutionInboxRecord(offer(expiresAtMs), 100);
|
||||
const starting = { ...accepted, revision: 1, state: 'starting_acknowledged', updatedAtMs: 101 };
|
||||
const launching = {
|
||||
...starting, revision: 2, state: 'launching', updatedAtMs: 102,
|
||||
executorStartedAtMs: 100, logArtifactId: LOG_ID,
|
||||
completionReceiptCallbackSequence: 1,
|
||||
completionReceiptTokenDigest: RECEIPT_DIGEST,
|
||||
};
|
||||
const started = {
|
||||
...launching, revision: 3, state: 'started', updatedAtMs: 103,
|
||||
executorHandle: 'ql3lp1.durable-handle',
|
||||
};
|
||||
const running = { ...started, revision: 4, state: 'running_acknowledged', updatedAtMs: 104 };
|
||||
assertWorkerRemoteExecutionInboxTransition(accepted, starting);
|
||||
assertWorkerRemoteExecutionInboxTransition(starting, launching);
|
||||
assertWorkerRemoteExecutionInboxTransition(launching, started);
|
||||
assertWorkerRemoteExecutionInboxTransition(started, running);
|
||||
return running;
|
||||
}
|
||||
|
||||
function fixture(overrides = {}) {
|
||||
let record = runningRecord(overrides.expiresAtMs ?? 30_020);
|
||||
const calls = [];
|
||||
const inbox = {
|
||||
async readOffer(id) { return id === record.offer.offerId ? record : undefined; },
|
||||
async replaceOffer(next, expectedRevision) {
|
||||
assert.equal(expectedRevision, record.revision);
|
||||
assertWorkerRemoteExecutionInboxTransition(record, next);
|
||||
record = next;
|
||||
calls.push(`persist:${record.offer.lease.version}:${record.state}`);
|
||||
},
|
||||
};
|
||||
const completion = {
|
||||
async recover() {
|
||||
calls.push('completion');
|
||||
return overrides.completionResult ?? {
|
||||
offerId: record.offer.offerId, status: 'receipt_missing',
|
||||
};
|
||||
},
|
||||
};
|
||||
const leaseControl = {
|
||||
async control(command) {
|
||||
calls.push(`control:${command.expectedLeaseVersion}`);
|
||||
if (overrides.control) return overrides.control(command, () => record);
|
||||
return {
|
||||
status: 'renewed', projectId: command.projectId, runId: command.runId,
|
||||
attemptId: command.attemptId, offerId: command.offerId,
|
||||
leaseGeneration: command.leaseGeneration,
|
||||
leaseVersion: command.expectedLeaseVersion + 1,
|
||||
renewedAtMs: 1_000, expiresAtMs: 31_000,
|
||||
};
|
||||
},
|
||||
};
|
||||
const processes = {
|
||||
async stop(handle) {
|
||||
calls.push(`stop:${handle}:v${record.offer.lease.version}`);
|
||||
return overrides.stopResult ?? { status: 'stopped', signal: 'SIGTERM' };
|
||||
},
|
||||
};
|
||||
const coordinator = new WorkerRemoteExecutionControlCoordinator(
|
||||
inbox, completion, leaseControl, processes,
|
||||
{
|
||||
currentSession: () => overrides.session === null ? undefined : {
|
||||
workerId: 'edge-1', sessionId: SESSION_ID, generation: 2,
|
||||
status: 'available', leaseExpiresAtMs: 60_000,
|
||||
...overrides.session,
|
||||
},
|
||||
now: () => overrides.now ?? 500,
|
||||
},
|
||||
);
|
||||
return { coordinator, calls, record: () => record };
|
||||
}
|
||||
|
||||
test('replays completion first, renews authority, then persists the next lease version', async () => {
|
||||
const f = fixture();
|
||||
assert.deepEqual(await f.coordinator.reconcile('offer-control-1'), {
|
||||
offerId: 'offer-control-1', status: 'renewed', leaseVersion: 1,
|
||||
expiresAtMs: 31_000, completionStatus: 'receipt_missing',
|
||||
});
|
||||
assert.deepEqual(f.calls, ['completion', 'control:0', 'persist:1:running_acknowledged']);
|
||||
assert.equal(f.record().offer.lease.renewedAtMs, 1_000);
|
||||
});
|
||||
|
||||
test('persists stop-request lease authority before stopping the exact process', async () => {
|
||||
const f = fixture({
|
||||
control(command) {
|
||||
return {
|
||||
status: 'stop_requested', projectId: command.projectId,
|
||||
runId: command.runId, attemptId: command.attemptId,
|
||||
offerId: command.offerId, leaseGeneration: command.leaseGeneration,
|
||||
leaseVersion: 1, renewedAtMs: 1_000, expiresAtMs: 31_000,
|
||||
stop: { reason: 'timeout', requestedAtMs: 900 },
|
||||
};
|
||||
},
|
||||
});
|
||||
const result = await f.coordinator.reconcile('offer-control-1');
|
||||
assert.equal(result.status, 'stop_requested');
|
||||
assert.equal(result.reason, 'timeout');
|
||||
assert.deepEqual(f.calls, [
|
||||
'completion', 'control:0', 'persist:1:running_acknowledged',
|
||||
'stop:ql3lp1.durable-handle:v1',
|
||||
]);
|
||||
});
|
||||
|
||||
test('stops locally and records conclusive recovery after lease expiry', async () => {
|
||||
const f = fixture({ expiresAtMs: 400, now: 500 });
|
||||
const result = await f.coordinator.reconcile('offer-control-1');
|
||||
assert.equal(result.status, 'lease_expired');
|
||||
assert.equal(result.recoveryReason, 'lease_lost_local_execution_stopped');
|
||||
assert.equal(f.record().state, 'recovery_required');
|
||||
assert.equal(f.record().recoveryReason, 'lease_lost_local_execution_stopped');
|
||||
assert.equal(f.calls.some((value) => value.startsWith('control:')), false);
|
||||
});
|
||||
|
||||
test('keeps inconclusive stop evidence distinct after lease expiry', async () => {
|
||||
const f = fixture({
|
||||
expiresAtMs: 400, now: 500,
|
||||
stopResult: { status: 'unknown', reason: 'provider_unavailable' },
|
||||
});
|
||||
const result = await f.coordinator.reconcile('offer-control-1');
|
||||
assert.equal(result.recoveryReason, 'lease_lost_local_execution_unverified');
|
||||
assert.equal(f.record().recoveryReason, 'lease_lost_local_execution_unverified');
|
||||
});
|
||||
|
||||
test('does not contact control after completion acknowledgement', async () => {
|
||||
const f = fixture({
|
||||
completionResult: { offerId: 'offer-control-1', status: 'completion_acknowledged' },
|
||||
});
|
||||
assert.equal((await f.coordinator.reconcile('offer-control-1')).status,
|
||||
'completion_acknowledged');
|
||||
assert.deepEqual(f.calls, ['completion']);
|
||||
});
|
||||
|
||||
test('waits for the bound Worker Session while local lease authority remains live', async () => {
|
||||
const f = fixture({ session: null });
|
||||
const result = await f.coordinator.reconcile('offer-control-1');
|
||||
assert.equal(result.status, 'session_unavailable');
|
||||
assert.equal(f.calls.some((value) => value.startsWith('control:')), false);
|
||||
assert.equal(f.calls.some((value) => value.startsWith('stop:')), false);
|
||||
});
|
||||
|
||||
test('stops and quarantines execution when the control plane is terminal', async () => {
|
||||
const f = fixture({
|
||||
control(command) {
|
||||
return {
|
||||
status: 'terminal', projectId: command.projectId, runId: command.runId,
|
||||
attemptId: command.attemptId, offerId: command.offerId,
|
||||
leaseGeneration: command.leaseGeneration, terminalStatus: 'cancelled',
|
||||
};
|
||||
},
|
||||
});
|
||||
const result = await f.coordinator.reconcile('offer-control-1');
|
||||
assert.equal(result.status, 'terminal');
|
||||
assert.equal(result.terminalStatus, 'cancelled');
|
||||
assert.equal(f.record().recoveryReason, 'control_plane_terminal');
|
||||
});
|
||||
|
||||
test('coalesces concurrent supervision for the same offer', async () => {
|
||||
let release;
|
||||
const gate = new Promise((resolve) => { release = resolve; });
|
||||
const f = fixture({
|
||||
async control(command) {
|
||||
await gate;
|
||||
return {
|
||||
status: 'renewed', projectId: command.projectId, runId: command.runId,
|
||||
attemptId: command.attemptId, offerId: command.offerId,
|
||||
leaseGeneration: command.leaseGeneration, leaseVersion: 1,
|
||||
renewedAtMs: 1_000, expiresAtMs: 31_000,
|
||||
};
|
||||
},
|
||||
});
|
||||
const first = f.coordinator.reconcile('offer-control-1');
|
||||
const second = f.coordinator.reconcile('offer-control-1');
|
||||
assert.equal(first, second);
|
||||
release();
|
||||
await Promise.all([first, second]);
|
||||
assert.equal(f.calls.filter((value) => value.startsWith('control:')).length, 1);
|
||||
});
|
||||
@@ -0,0 +1,262 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WorkerFileLogArtifactAllocator,
|
||||
createWorkerRemoteLogArtifactId,
|
||||
workerRemoteLogArtifactPolicy,
|
||||
} = require('../dist/execution/workerFileLogArtifactAllocator');
|
||||
|
||||
const REQUEST = Object.freeze({
|
||||
projectId: 'project-1',
|
||||
runId: 'run-1',
|
||||
attemptId: 'attempt-1',
|
||||
offerId: 'offer-1',
|
||||
});
|
||||
|
||||
async function temporaryRoot(t) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-worker-log-'));
|
||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
return root;
|
||||
}
|
||||
|
||||
function policy(overrides = {}) {
|
||||
return {
|
||||
maximumAttemptBytes: 16,
|
||||
minimumFreeBytes: 32,
|
||||
maximumWriteChunkBytes: 8,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function capacity(availableBytes = 1_000_000n) {
|
||||
return { async availableBytes() { return availableBytes; } };
|
||||
}
|
||||
|
||||
function artifactPath(root, artifactId) {
|
||||
return path.join(root, artifactId.slice(5, 7), `${artifactId}.log`);
|
||||
}
|
||||
|
||||
test('provides explicit edge and node capacity policies', () => {
|
||||
assert.deepEqual(workerRemoteLogArtifactPolicy('edge'), {
|
||||
maximumAttemptBytes: 4 * 1024 * 1024,
|
||||
minimumFreeBytes: 32 * 1024 * 1024,
|
||||
maximumWriteChunkBytes: 1024 * 1024,
|
||||
});
|
||||
assert.deepEqual(workerRemoteLogArtifactPolicy('node'), {
|
||||
maximumAttemptBytes: 64 * 1024 * 1024,
|
||||
minimumFreeBytes: 256 * 1024 * 1024,
|
||||
maximumWriteChunkBytes: 1024 * 1024,
|
||||
});
|
||||
});
|
||||
|
||||
test('derives one opaque log identity per exact offer authority', () => {
|
||||
const first = createWorkerRemoteLogArtifactId(REQUEST);
|
||||
assert.equal(first, createWorkerRemoteLogArtifactId({ ...REQUEST }));
|
||||
assert.notEqual(first, createWorkerRemoteLogArtifactId({
|
||||
...REQUEST,
|
||||
offerId: 'offer-2',
|
||||
}));
|
||||
assert.match(first, /^wlog-[a-f0-9]{30}$/);
|
||||
assert.equal(first.length, 35);
|
||||
assert.equal(first.includes(REQUEST.runId), false);
|
||||
});
|
||||
|
||||
test('hands off once, appends both streams, and keeps private ownership', async (t) => {
|
||||
const root = await temporaryRoot(t);
|
||||
const allocator = new WorkerFileLogArtifactAllocator({
|
||||
root,
|
||||
policy: policy(),
|
||||
capacity: capacity(),
|
||||
});
|
||||
const prepared = await allocator.prepare(REQUEST);
|
||||
const output = prepared.takeOutput();
|
||||
assert.throws(() => prepared.takeOutput(), /closed/);
|
||||
await prepared.release();
|
||||
const mutable = Buffer.from('abc');
|
||||
const firstWrite = output.write({
|
||||
stream: 'stdout',
|
||||
chunk: mutable,
|
||||
observedAtMs: 1,
|
||||
});
|
||||
mutable.fill(0x7a);
|
||||
await firstWrite;
|
||||
await output.write({
|
||||
stream: 'stderr',
|
||||
chunk: Buffer.from('def'),
|
||||
observedAtMs: 2,
|
||||
});
|
||||
await output.close();
|
||||
await output.close();
|
||||
await assert.rejects(
|
||||
output.write({ stream: 'stdout', chunk: Buffer.from('x'), observedAtMs: 3 }),
|
||||
/closed/,
|
||||
);
|
||||
const file = artifactPath(root, prepared.logArtifactId);
|
||||
assert.equal(await fs.readFile(file, 'utf8'), 'abcdef');
|
||||
assert.equal((await fs.stat(root)).mode & 0o777, 0o700);
|
||||
assert.equal((await fs.stat(path.dirname(file))).mode & 0o777, 0o700);
|
||||
assert.equal((await fs.stat(file)).mode & 0o777, 0o600);
|
||||
});
|
||||
|
||||
test('preserves an accepted prefix across reopen without truncation', async (t) => {
|
||||
const root = await temporaryRoot(t);
|
||||
const allocator = new WorkerFileLogArtifactAllocator({
|
||||
root,
|
||||
policy: policy(),
|
||||
capacity: capacity(),
|
||||
});
|
||||
const first = await allocator.prepare(REQUEST);
|
||||
const firstOutput = first.takeOutput();
|
||||
await firstOutput.write({
|
||||
stream: 'stdout',
|
||||
chunk: Buffer.from('before-'),
|
||||
observedAtMs: 1,
|
||||
});
|
||||
const replay = await allocator.prepare(REQUEST);
|
||||
const replayOutput = replay.takeOutput();
|
||||
await replayOutput.write({
|
||||
stream: 'stdout',
|
||||
chunk: Buffer.from('after'),
|
||||
observedAtMs: 2,
|
||||
});
|
||||
await Promise.all([firstOutput.close(), replayOutput.close()]);
|
||||
assert.equal(first.logArtifactId, replay.logArtifactId);
|
||||
assert.equal(
|
||||
await fs.readFile(artifactPath(root, first.logArtifactId), 'utf8'),
|
||||
'before-after',
|
||||
);
|
||||
});
|
||||
|
||||
test('streams a bounded Artifact and authenticates its truncation fact', async (t) => {
|
||||
const root = await temporaryRoot(t);
|
||||
const streamingPolicy = policy({ maximumWriteChunkBytes: 16 });
|
||||
const allocator = new WorkerFileLogArtifactAllocator({
|
||||
root,
|
||||
policy: streamingPolicy,
|
||||
capacity: capacity(),
|
||||
});
|
||||
const prepared = await allocator.prepare(REQUEST);
|
||||
const output = prepared.takeOutput();
|
||||
await output.write({
|
||||
stream: 'stdout',
|
||||
chunk: Buffer.from('streamed-log'),
|
||||
observedAtMs: 1,
|
||||
});
|
||||
await output.close();
|
||||
const file = artifactPath(root, prepared.logArtifactId);
|
||||
await fs.writeFile(
|
||||
path.join(path.dirname(file), `.${prepared.logArtifactId}.log.truncated.json`),
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
runId: REQUEST.runId,
|
||||
attemptId: REQUEST.attemptId,
|
||||
logArtifactId: prepared.logArtifactId,
|
||||
maximumBytes: streamingPolicy.maximumAttemptBytes,
|
||||
quotaReached: false,
|
||||
observedAtMs: 2,
|
||||
}),
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
|
||||
const lease = await allocator.open({
|
||||
runId: REQUEST.runId,
|
||||
attemptId: REQUEST.attemptId,
|
||||
logArtifactId: prepared.logArtifactId,
|
||||
});
|
||||
assert.ok(lease);
|
||||
assert.equal(lease.byteLength, 12);
|
||||
assert.equal(lease.truncated, false);
|
||||
const chunks = [];
|
||||
for await (const chunk of lease.chunks()) chunks.push(chunk);
|
||||
assert.equal(Buffer.concat(chunks).toString(), 'streamed-log');
|
||||
await lease.close();
|
||||
assert.throws(() => lease.chunks(), /closed/);
|
||||
});
|
||||
|
||||
test('writes only the remaining prefix and then enforces the hard quota', async (t) => {
|
||||
const root = await temporaryRoot(t);
|
||||
const allocator = new WorkerFileLogArtifactAllocator({
|
||||
root,
|
||||
policy: policy({ maximumAttemptBytes: 5 }),
|
||||
capacity: capacity(),
|
||||
});
|
||||
const prepared = await allocator.prepare(REQUEST);
|
||||
const output = prepared.takeOutput();
|
||||
await output.write({
|
||||
stream: 'stdout',
|
||||
chunk: Buffer.from('abc'),
|
||||
observedAtMs: 1,
|
||||
});
|
||||
await assert.rejects(
|
||||
output.write({ stream: 'stderr', chunk: Buffer.from('defg'), observedAtMs: 2 }),
|
||||
/quota_exceeded/,
|
||||
);
|
||||
await output.close();
|
||||
assert.equal(
|
||||
await fs.readFile(artifactPath(root, prepared.logArtifactId), 'utf8'),
|
||||
'abcde',
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects oversized write chunks without changing the Artifact', async (t) => {
|
||||
const root = await temporaryRoot(t);
|
||||
const allocator = new WorkerFileLogArtifactAllocator({
|
||||
root,
|
||||
policy: policy({ maximumWriteChunkBytes: 3 }),
|
||||
capacity: capacity(),
|
||||
});
|
||||
const prepared = await allocator.prepare(REQUEST);
|
||||
const output = prepared.takeOutput();
|
||||
await assert.rejects(
|
||||
output.write({ stream: 'stdout', chunk: Buffer.from('four'), observedAtMs: 1 }),
|
||||
/invalid_output/,
|
||||
);
|
||||
await output.close();
|
||||
assert.equal((await fs.stat(artifactPath(root, prepared.logArtifactId))).size, 0);
|
||||
});
|
||||
|
||||
test('fails capacity admission before creating a shard or output file', async (t) => {
|
||||
const root = await temporaryRoot(t);
|
||||
const allocator = new WorkerFileLogArtifactAllocator({
|
||||
root,
|
||||
policy: policy(),
|
||||
capacity: capacity(47n),
|
||||
});
|
||||
await assert.rejects(allocator.prepare(REQUEST), /capacity_unavailable/);
|
||||
assert.deepEqual(await fs.readdir(root), []);
|
||||
});
|
||||
|
||||
test('fails closed when the deterministic output target is a symlink', async (t) => {
|
||||
const root = await temporaryRoot(t);
|
||||
const artifactId = createWorkerRemoteLogArtifactId(REQUEST);
|
||||
const directory = path.dirname(artifactPath(root, artifactId));
|
||||
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
||||
const victim = path.join(root, 'victim');
|
||||
await fs.writeFile(victim, 'unchanged', { mode: 0o600 });
|
||||
await fs.symlink(victim, artifactPath(root, artifactId));
|
||||
const allocator = new WorkerFileLogArtifactAllocator({
|
||||
root,
|
||||
policy: policy(),
|
||||
capacity: capacity(),
|
||||
});
|
||||
await assert.rejects(allocator.prepare(REQUEST), /unsafe_path/);
|
||||
assert.equal(await fs.readFile(victim, 'utf8'), 'unchanged');
|
||||
});
|
||||
|
||||
test('release closes an unclaimed preparation and prevents later handoff', async (t) => {
|
||||
const root = await temporaryRoot(t);
|
||||
const allocator = new WorkerFileLogArtifactAllocator({
|
||||
root,
|
||||
policy: policy(),
|
||||
capacity: capacity(),
|
||||
});
|
||||
const prepared = await allocator.prepare(REQUEST);
|
||||
await prepared.release();
|
||||
await prepared.release();
|
||||
assert.throws(() => prepared.takeOutput(), /closed/);
|
||||
});
|
||||
@@ -0,0 +1,311 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { EventEmitter } = require('node:events');
|
||||
const { PassThrough } = require('node:stream');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WORKER_INGRESS_ARTIFACT_CONTENT_TYPE,
|
||||
WorkerIngressHttpsClient,
|
||||
} = require('../dist/remote-execution/transport/workerIngressHttpsClient');
|
||||
|
||||
const SESSION_ID = '018f0000-0000-7000-8000-000000000001';
|
||||
const ARTIFACT_PATH =
|
||||
`/api/v3/worker-ingress/workers/worker-1/sessions/${SESSION_ID}/artifacts`;
|
||||
const COMPLETION_PATH =
|
||||
`/api/v3/worker-ingress/workers/worker-1/sessions/${SESSION_ID}/completion`;
|
||||
const AUTHORIZATION =
|
||||
`Worker ql3w_worker_primary_${Buffer.alloc(32, 7).toString('base64url')}`;
|
||||
|
||||
function credentials() {
|
||||
return {
|
||||
authorization: AUTHORIZATION,
|
||||
certificateChainPem: 'client certificate',
|
||||
privateKeyPem: 'client private key',
|
||||
trustAnchors: ['trusted ca'],
|
||||
};
|
||||
}
|
||||
|
||||
function response(body = '{"stored":true}') {
|
||||
const stream = new PassThrough();
|
||||
stream.statusCode = 200;
|
||||
stream.headers = {
|
||||
'content-type': 'application/json',
|
||||
'content-length': String(Buffer.byteLength(body)),
|
||||
};
|
||||
queueMicrotask(() => stream.end(body));
|
||||
return stream;
|
||||
}
|
||||
|
||||
function requestFactory(observation, options = {}) {
|
||||
return (requestOptions, callback) => {
|
||||
observation.options = requestOptions;
|
||||
observation.chunks = [];
|
||||
const outgoing = new EventEmitter();
|
||||
let writes = 0;
|
||||
outgoing.setTimeout = (timeout, handler) => {
|
||||
observation.timeout = timeout;
|
||||
observation.timeoutHandler = handler;
|
||||
return outgoing;
|
||||
};
|
||||
outgoing.write = (chunk) => {
|
||||
observation.chunks.push(Buffer.from(chunk));
|
||||
writes += 1;
|
||||
if (options.backpressure && writes === 1) {
|
||||
queueMicrotask(() => outgoing.emit('drain'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
outgoing.end = () => {
|
||||
observation.ended = true;
|
||||
queueMicrotask(() => callback(response()));
|
||||
};
|
||||
outgoing.destroy = (error) => {
|
||||
observation.destroyed = true;
|
||||
if (error) queueMicrotask(() => outgoing.emit('error', error));
|
||||
};
|
||||
return outgoing;
|
||||
};
|
||||
}
|
||||
|
||||
function client(observation, options = {}) {
|
||||
return new WorkerIngressHttpsClient({
|
||||
origin: 'https://cluster.example:7443',
|
||||
credentials: { async load() { return credentials(); } },
|
||||
requestTimeoutMs: 5_000,
|
||||
requestFactory: requestFactory(observation, options),
|
||||
});
|
||||
}
|
||||
|
||||
test('streams exact Artifact bytes with bounded backpressure over shared mTLS', async () => {
|
||||
const observation = {};
|
||||
const transport = client(observation, { backpressure: true });
|
||||
const prefix = Buffer.from('header');
|
||||
const content = Buffer.from('worker-log');
|
||||
try {
|
||||
const result = await transport.postStream({
|
||||
path: ARTIFACT_PATH,
|
||||
body: (async function* () {
|
||||
yield prefix;
|
||||
yield content;
|
||||
})(),
|
||||
byteLength: prefix.byteLength + content.byteLength,
|
||||
maximumResponseBytes: 1024,
|
||||
});
|
||||
assert.equal(Buffer.from(result).toString('utf8'), '{"stored":true}');
|
||||
assert.equal(observation.options.protocol, 'https:');
|
||||
assert.equal(observation.options.hostname, 'cluster.example');
|
||||
assert.equal(observation.options.port, '7443');
|
||||
assert.equal(observation.options.minVersion, 'TLSv1.3');
|
||||
assert.equal(observation.options.rejectUnauthorized, true);
|
||||
assert.equal(observation.options.headers.authorization, AUTHORIZATION);
|
||||
assert.equal(
|
||||
observation.options.headers['content-type'],
|
||||
WORKER_INGRESS_ARTIFACT_CONTENT_TYPE,
|
||||
);
|
||||
assert.equal(
|
||||
observation.options.headers['content-length'],
|
||||
String(prefix.byteLength + content.byteLength),
|
||||
);
|
||||
assert.equal(Buffer.concat(observation.chunks).toString(), 'headerworker-log');
|
||||
assert.equal(observation.ended, true);
|
||||
assert.equal(observation.timeout, 5_000);
|
||||
} finally {
|
||||
transport.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects short, overlong and route-confused stream bodies', async () => {
|
||||
const shortObservation = {};
|
||||
const short = client(shortObservation);
|
||||
try {
|
||||
await assert.rejects(
|
||||
short.postStream({
|
||||
path: ARTIFACT_PATH,
|
||||
body: (async function* () { yield Buffer.from('short'); })(),
|
||||
byteLength: 6,
|
||||
maximumResponseBytes: 1024,
|
||||
}),
|
||||
/request_rejected/,
|
||||
);
|
||||
assert.equal(shortObservation.destroyed, true);
|
||||
} finally {
|
||||
short.close();
|
||||
}
|
||||
|
||||
const longObservation = {};
|
||||
const long = client(longObservation);
|
||||
try {
|
||||
await assert.rejects(
|
||||
long.postStream({
|
||||
path: ARTIFACT_PATH,
|
||||
body: (async function* () { yield Buffer.from('too-long'); })(),
|
||||
byteLength: 3,
|
||||
maximumResponseBytes: 1024,
|
||||
}),
|
||||
/request_rejected/,
|
||||
);
|
||||
await assert.rejects(
|
||||
long.postStream({
|
||||
path: COMPLETION_PATH,
|
||||
body: (async function* () { yield Buffer.from('{}'); })(),
|
||||
byteLength: 2,
|
||||
maximumResponseBytes: 1024,
|
||||
}),
|
||||
/request_rejected/,
|
||||
);
|
||||
await assert.rejects(
|
||||
long.postJson({
|
||||
path: ARTIFACT_PATH,
|
||||
body: {},
|
||||
maximumResponseBytes: 1024,
|
||||
}),
|
||||
/request_rejected/,
|
||||
);
|
||||
} finally {
|
||||
long.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('permits completion JSON while keeping Artifact transport stream-only', async () => {
|
||||
const observation = {};
|
||||
const transport = client(observation);
|
||||
const originalFactory = observation;
|
||||
try {
|
||||
// A separate JSON-capable fake keeps this assertion focused on route policy.
|
||||
const json = new WorkerIngressHttpsClient({
|
||||
origin: 'https://cluster.example',
|
||||
credentials: { async load() { return credentials(); } },
|
||||
requestFactory(options, callback) {
|
||||
originalFactory.options = options;
|
||||
const outgoing = new EventEmitter();
|
||||
outgoing.setTimeout = () => outgoing;
|
||||
outgoing.destroy = (error) => {
|
||||
if (error) queueMicrotask(() => outgoing.emit('error', error));
|
||||
};
|
||||
outgoing.end = (body) => {
|
||||
originalFactory.body = Buffer.from(body);
|
||||
queueMicrotask(() => callback(response('{"status":"applied"}')));
|
||||
};
|
||||
return outgoing;
|
||||
},
|
||||
});
|
||||
try {
|
||||
await json.postJson({
|
||||
path: COMPLETION_PATH,
|
||||
body: { schema: 'qinglong/remote-worker-completion@v1' },
|
||||
maximumResponseBytes: 1024,
|
||||
});
|
||||
assert.equal(originalFactory.options.path, COMPLETION_PATH);
|
||||
} finally {
|
||||
json.close();
|
||||
}
|
||||
} finally {
|
||||
transport.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('disposes provider-owned credential material on success and rejection', async () => {
|
||||
const certificate = Buffer.from('client certificate');
|
||||
const privateKey = Buffer.from('client private key');
|
||||
const trust = Buffer.from('trusted ca');
|
||||
let disposals = 0;
|
||||
const observation = {};
|
||||
const transport = new WorkerIngressHttpsClient({
|
||||
origin: 'https://cluster.example',
|
||||
credentials: {
|
||||
async load() {
|
||||
return {
|
||||
authorization: AUTHORIZATION,
|
||||
certificateChainPem: certificate,
|
||||
privateKeyPem: privateKey,
|
||||
trustAnchors: [trust],
|
||||
dispose() {
|
||||
disposals += 1;
|
||||
certificate.fill(0);
|
||||
privateKey.fill(0);
|
||||
trust.fill(0);
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
requestFactory: requestFactory(observation),
|
||||
});
|
||||
try {
|
||||
await transport.postJson({
|
||||
path: COMPLETION_PATH,
|
||||
body: { schema: 'qinglong/remote-worker-completion@v1' },
|
||||
maximumResponseBytes: 1024,
|
||||
});
|
||||
assert.equal(disposals, 1);
|
||||
assert.equal(certificate.equals(Buffer.alloc(certificate.length)), true);
|
||||
assert.equal(privateKey.equals(Buffer.alloc(privateKey.length)), true);
|
||||
assert.equal(trust.equals(Buffer.alloc(trust.length)), true);
|
||||
} finally {
|
||||
transport.close();
|
||||
}
|
||||
|
||||
let rejectedDisposals = 0;
|
||||
const rejected = new WorkerIngressHttpsClient({
|
||||
origin: 'https://cluster.example',
|
||||
credentials: {
|
||||
async load() {
|
||||
return {
|
||||
authorization: 'invalid',
|
||||
certificateChainPem: 'certificate',
|
||||
privateKeyPem: 'key',
|
||||
trustAnchors: ['trust'],
|
||||
dispose() { rejectedDisposals += 1; },
|
||||
};
|
||||
},
|
||||
},
|
||||
requestFactory() { throw new Error('request must not start'); },
|
||||
});
|
||||
try {
|
||||
await assert.rejects(
|
||||
rejected.postJson({
|
||||
path: COMPLETION_PATH,
|
||||
body: {},
|
||||
maximumResponseBytes: 1024,
|
||||
}),
|
||||
/credentials_unavailable/,
|
||||
);
|
||||
assert.equal(rejectedDisposals, 1);
|
||||
} finally {
|
||||
rejected.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('exposes only the low-sensitive non-success HTTP status class', async () => {
|
||||
const transport = new WorkerIngressHttpsClient({
|
||||
origin: 'https://cluster.example',
|
||||
credentials: { async load() { return credentials(); } },
|
||||
requestFactory(_options, callback) {
|
||||
const outgoing = new EventEmitter();
|
||||
outgoing.setTimeout = () => outgoing;
|
||||
outgoing.destroy = (error) => {
|
||||
if (error) queueMicrotask(() => outgoing.emit('error', error));
|
||||
};
|
||||
outgoing.end = () => {
|
||||
const denied = response('{"code":"must-not-be-read"}');
|
||||
denied.statusCode = 401;
|
||||
queueMicrotask(() => callback(denied));
|
||||
};
|
||||
return outgoing;
|
||||
},
|
||||
});
|
||||
try {
|
||||
await assert.rejects(
|
||||
transport.postJson({
|
||||
path: COMPLETION_PATH,
|
||||
body: {},
|
||||
maximumResponseBytes: 1024,
|
||||
}),
|
||||
(error) =>
|
||||
error.reason === 'response_rejected' && error.httpStatus === 401,
|
||||
);
|
||||
} finally {
|
||||
transport.close();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,192 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const { CompletionReceiptFileStore } = require('@qinglong/local-process');
|
||||
const {
|
||||
WorkerFileLogArtifactAllocator,
|
||||
workerRemoteLogArtifactPolicy,
|
||||
} = require('../dist/execution/workerFileLogArtifactAllocator');
|
||||
const {
|
||||
WorkerPosixExecutionExecutor,
|
||||
} = require('../dist/execution/workerPosixExecutionExecutor');
|
||||
|
||||
const RUN_ID = '019f70e0-0000-7000-8000-000000000101';
|
||||
const ATTEMPT_ID = '019f70e0-0000-7000-8000-000000000102';
|
||||
const TOKEN = Buffer.alloc(32, 0x5a);
|
||||
|
||||
async function fixture(t) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ql3-worker-posix-'));
|
||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
return {
|
||||
root,
|
||||
artifactRoot: path.join(root, 'artifacts'),
|
||||
receiptRoot: path.join(root, 'receipts'),
|
||||
};
|
||||
}
|
||||
|
||||
function identityProvider() {
|
||||
return {
|
||||
async capture(pid) {
|
||||
return {
|
||||
platform: 'linux',
|
||||
bootId: '11111111-2222-3333-4444-555555555555',
|
||||
pid,
|
||||
processGroupId: pid,
|
||||
startTimeTicks: '1',
|
||||
};
|
||||
},
|
||||
async inspect(identity) {
|
||||
return { status: 'running', identityPid: identity.pid };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function preparedOutput(artifactRoot, offerId = 'offer-posix-1') {
|
||||
const allocator = new WorkerFileLogArtifactAllocator({
|
||||
root: artifactRoot,
|
||||
policy: workerRemoteLogArtifactPolicy('edge'),
|
||||
capacity: { async availableBytes() { return 1024n ** 4n; } },
|
||||
});
|
||||
const prepared = await allocator.prepare({
|
||||
projectId: 'project-1',
|
||||
runId: RUN_ID,
|
||||
attemptId: ATTEMPT_ID,
|
||||
offerId,
|
||||
});
|
||||
return { prepared, output: prepared.takeOutput() };
|
||||
}
|
||||
|
||||
function launch(prepared, output, overrides = {}) {
|
||||
return {
|
||||
offerId: 'offer-posix-1',
|
||||
runId: RUN_ID,
|
||||
attemptId: ATTEMPT_ID,
|
||||
executorStartedAtMs: 100,
|
||||
command: {
|
||||
kind: 'argv',
|
||||
file: process.execPath,
|
||||
args: [
|
||||
'-e',
|
||||
"process.stdout.write(process.env.QL3_RECEIPT_CALLBACK_TOKEN ? 'leaked' : 'worker-output')",
|
||||
],
|
||||
},
|
||||
environment: [],
|
||||
logArtifactId: prepared.logArtifactId,
|
||||
output,
|
||||
completionCallback: { sequence: 1, token: Buffer.from(TOKEN) },
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForReceipt(root) {
|
||||
const store = new CompletionReceiptFileStore(root);
|
||||
for (let attempt = 0; attempt < 100; attempt += 1) {
|
||||
const receipt = await store.read(ATTEMPT_ID);
|
||||
if (receipt) return receipt;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
throw new Error('Worker completion receipt was not published');
|
||||
}
|
||||
|
||||
test('verifies the Worker barrier, launches through the reviewed fd and writes a receipt', async (t) => {
|
||||
const roots = await fixture(t);
|
||||
const { prepared, output } = await preparedOutput(roots.artifactRoot);
|
||||
let barrier;
|
||||
const executor = new WorkerPosixExecutionExecutor({
|
||||
barrier: { async verify(input) { barrier = input; } },
|
||||
receiptRoot: roots.receiptRoot,
|
||||
identityProvider: identityProvider(),
|
||||
clock: { now: () => 100 },
|
||||
createHandleId: () => 'worker-handle-1',
|
||||
});
|
||||
const result = await executor.start(launch(prepared, output));
|
||||
assert.equal(result.status, 'started');
|
||||
assert.match(result.executorHandle, /^ql3lp1\./);
|
||||
assert.equal(barrier.logArtifactId, prepared.logArtifactId);
|
||||
assert.equal(barrier.executorStartedAtMs, 100);
|
||||
assert.match(barrier.callbackTokenDigest, /^[a-f0-9]{64}$/);
|
||||
const receipt = await waitForReceipt(roots.receiptRoot);
|
||||
assert.equal(receipt.runId, RUN_ID);
|
||||
assert.equal(receipt.attemptId, ATTEMPT_ID);
|
||||
assert.equal(receipt.callbackSequence, 1);
|
||||
assert.equal(receipt.token, TOKEN.toString('base64url'));
|
||||
assert.equal(receipt.exitCode, 0);
|
||||
const outputPath = path.join(
|
||||
roots.artifactRoot,
|
||||
prepared.logArtifactId.slice(5, 7),
|
||||
`${prepared.logArtifactId}.log`,
|
||||
);
|
||||
assert.equal(await fs.readFile(outputPath, 'utf8'), 'worker-output');
|
||||
});
|
||||
|
||||
test('does not spawn when the durable Worker barrier rejects authority', async (t) => {
|
||||
const roots = await fixture(t);
|
||||
const marker = path.join(roots.root, 'spawned');
|
||||
const { prepared, output } = await preparedOutput(roots.artifactRoot);
|
||||
const executor = new WorkerPosixExecutionExecutor({
|
||||
barrier: { async verify() { throw new Error('stale inbox'); } },
|
||||
receiptRoot: roots.receiptRoot,
|
||||
identityProvider: identityProvider(),
|
||||
});
|
||||
const result = await executor.start(launch(prepared, output, {
|
||||
command: { kind: 'argv', file: '/usr/bin/touch', args: [marker] },
|
||||
}));
|
||||
assert.deepEqual(result, { status: 'rejected' });
|
||||
await assert.rejects(fs.stat(marker), { code: 'ENOENT' });
|
||||
});
|
||||
|
||||
test('rejects timeout without durable control-plane deadline before spawn', async (t) => {
|
||||
const roots = await fixture(t);
|
||||
const { prepared, output } = await preparedOutput(roots.artifactRoot);
|
||||
let barriers = 0;
|
||||
const executor = new WorkerPosixExecutionExecutor({
|
||||
barrier: { async verify() { barriers += 1; } },
|
||||
receiptRoot: roots.receiptRoot,
|
||||
identityProvider: identityProvider(),
|
||||
});
|
||||
const result = await executor.start(launch(prepared, output, {
|
||||
timeoutMs: 1_000,
|
||||
}));
|
||||
assert.deepEqual(result, { status: 'rejected' });
|
||||
assert.equal(barriers, 0);
|
||||
});
|
||||
|
||||
test('accepts timeout only when starting ACK supplied a durable deadline', async (t) => {
|
||||
const roots = await fixture(t);
|
||||
const { prepared, output } = await preparedOutput(roots.artifactRoot);
|
||||
let barriers = 0;
|
||||
const executor = new WorkerPosixExecutionExecutor({
|
||||
barrier: { async verify() { barriers += 1; } },
|
||||
receiptRoot: roots.receiptRoot,
|
||||
identityProvider: identityProvider(),
|
||||
});
|
||||
const result = await executor.start(launch(prepared, output, {
|
||||
timeoutMs: 1_000,
|
||||
executionDeadlineAtMs: 2_000,
|
||||
}));
|
||||
assert.equal(result.status, 'started');
|
||||
assert.equal(barriers, 1);
|
||||
});
|
||||
|
||||
test('propagates unknown outcome when durable identity capture fails after spawn', async (t) => {
|
||||
const roots = await fixture(t);
|
||||
const { prepared, output } = await preparedOutput(roots.artifactRoot);
|
||||
const executor = new WorkerPosixExecutionExecutor({
|
||||
barrier: { async verify() {} },
|
||||
receiptRoot: roots.receiptRoot,
|
||||
identityProvider: {
|
||||
async capture() { throw new Error('procfs unavailable'); },
|
||||
async inspect() { return { status: 'unknown' }; },
|
||||
},
|
||||
});
|
||||
await assert.rejects(
|
||||
executor.start(launch(prepared, output, {
|
||||
command: { kind: 'shell', command: 'sleep 5', shell: '/bin/sh' },
|
||||
})),
|
||||
(error) => error?.spawnOutcome === 'unknown',
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,229 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawn } = require('node:child_process');
|
||||
const { once } = require('node:events');
|
||||
const {
|
||||
chmod,
|
||||
mkdtemp,
|
||||
rm,
|
||||
writeFile,
|
||||
} = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WorkerProcessError,
|
||||
runProductionWorkerProcess,
|
||||
} = require('@qinglong/worker-runtime/process');
|
||||
|
||||
async function environment(t) {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'ql3-worker-process-'));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const capabilities = path.join(root, 'capabilities.json');
|
||||
await writeFile(capabilities, JSON.stringify({
|
||||
architecture: 'x64',
|
||||
operatingSystem: 'linux',
|
||||
executors: ['local_process'],
|
||||
}));
|
||||
await chmod(capabilities, 0o444);
|
||||
return {
|
||||
QL3_WORKER_RUNTIME_ENABLED: 'true',
|
||||
QL_DEPLOYMENT_PROFILE: 'worker',
|
||||
QL3_WORKER_CAPACITY_PROFILE: 'node',
|
||||
QL3_WORKER_ID: 'node-worker-1',
|
||||
QL3_WORKER_CONTROL_ORIGIN: 'https://control.internal:5801',
|
||||
QL3_WORKER_CAPABILITIES_FILE: capabilities,
|
||||
QL3_WORKER_JOURNAL_ROOT: path.join(root, 'journal'),
|
||||
QL3_WORKER_LOG_ROOT: path.join(root, 'logs'),
|
||||
QL3_WORKER_RECEIPT_ROOT: path.join(root, 'receipts'),
|
||||
QL3_WORKER_CERTIFICATE_STORE_ROOT: path.join(root, 'identity'),
|
||||
QL3_WORKER_TRUST_ANCHOR_FILE: path.join(root, 'ca.pem'),
|
||||
QL3_WORKER_CREDENTIAL_TOKEN_FILE: path.join(root, 'token'),
|
||||
QL3_WORKER_DRAIN_TIMEOUT_MS: '1000',
|
||||
};
|
||||
}
|
||||
|
||||
function signals(events) {
|
||||
return {
|
||||
subscribe(listener) {
|
||||
events.push('subscribe');
|
||||
queueMicrotask(() => listener('SIGTERM'));
|
||||
return () => events.push('unsubscribe');
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('assembles one product runtime and preserves authority across deferred drain', async (t) => {
|
||||
const events = [];
|
||||
const facts = [];
|
||||
const configured = await environment(t);
|
||||
let stopCalls = 0;
|
||||
const certificateRenewal = { async run() { return { status: 'not_due' }; } };
|
||||
const result = await runProductionWorkerProcess({
|
||||
environment: configured,
|
||||
signals: signals(events),
|
||||
emit(fact) {
|
||||
facts.push(fact);
|
||||
},
|
||||
async createCredentials(identity) {
|
||||
events.push(`credentials:${identity.certificateStoreRoot}`);
|
||||
return { async load() { throw new Error('not used'); } };
|
||||
},
|
||||
async createCertificateRenewal(config, credentials) {
|
||||
events.push(`renewal:${config.workerId}`);
|
||||
assert.equal(typeof credentials.load, 'function');
|
||||
return certificateRenewal;
|
||||
},
|
||||
async start(options) {
|
||||
events.push('start');
|
||||
assert.equal(options.enabled, true);
|
||||
assert.equal(options.profile, 'worker');
|
||||
assert.equal(options.capacityProfile, 'node');
|
||||
assert.equal(options.workerId, 'node-worker-1');
|
||||
assert.equal(options.maxConcurrentRuns, 8);
|
||||
assert.equal(options.heartbeatIntervalMs, 10_000);
|
||||
assert.equal(options.certificateRenewal, certificateRenewal);
|
||||
options.diagnostic({ code: 'certificate_renewal_failed' });
|
||||
return {
|
||||
status: 'active',
|
||||
async tick() {},
|
||||
async stop() {
|
||||
stopCalls += 1;
|
||||
events.push(`stop:${stopCalls}`);
|
||||
return stopCalls === 1 ? 'drain_timed_out' : 'stopped';
|
||||
},
|
||||
};
|
||||
},
|
||||
async waitBeforeStopRetry() {
|
||||
events.push('wait');
|
||||
},
|
||||
});
|
||||
assert.equal(result, 'stopped');
|
||||
assert.deepEqual(events, [
|
||||
'subscribe',
|
||||
`credentials:${path.dirname(configured.QL3_WORKER_JOURNAL_ROOT)}/identity`,
|
||||
'renewal:node-worker-1',
|
||||
'start',
|
||||
'stop:1',
|
||||
'wait',
|
||||
'stop:2',
|
||||
'unsubscribe',
|
||||
]);
|
||||
assert.deepEqual(
|
||||
facts.map((fact) => fact.event),
|
||||
[
|
||||
'starting',
|
||||
'runtime_diagnostic',
|
||||
'active',
|
||||
'shutdown_requested',
|
||||
'shutdown_deferred',
|
||||
'stopped',
|
||||
],
|
||||
);
|
||||
assert.equal(
|
||||
JSON.stringify(facts).includes(configured.QL3_WORKER_CREDENTIAL_TOKEN_FILE),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('retains the production process until an OS shutdown signal arrives', async (t) => {
|
||||
const configured = await environment(t);
|
||||
const childSource = String.raw`
|
||||
'use strict';
|
||||
const { runProductionWorkerProcess } = require(
|
||||
process.env.QL3_TEST_WORKER_RUNTIME_PATH
|
||||
);
|
||||
void runProductionWorkerProcess({
|
||||
environment: JSON.parse(process.env.QL3_TEST_WORKER_ENV),
|
||||
signals: {
|
||||
subscribe(listener) {
|
||||
const stop = () => listener('SIGTERM');
|
||||
process.once('SIGTERM', stop);
|
||||
return () => process.off('SIGTERM', stop);
|
||||
},
|
||||
},
|
||||
emit(event) {
|
||||
process.stdout.write(event.event + '\n');
|
||||
},
|
||||
async createCredentials() {
|
||||
return { async load() { throw new Error('not used'); } };
|
||||
},
|
||||
async start() {
|
||||
return {
|
||||
status: 'active',
|
||||
async tick() {},
|
||||
async stop() { return 'stopped'; },
|
||||
};
|
||||
},
|
||||
}).catch((error) => {
|
||||
process.stderr.write(String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
`;
|
||||
const child = spawn(process.execPath, ['-e', childSource], {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
QL3_TEST_WORKER_ENV: JSON.stringify(configured),
|
||||
QL3_TEST_WORKER_RUNTIME_PATH: require.resolve(
|
||||
'@qinglong/worker-runtime/process',
|
||||
),
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
t.after(() => {
|
||||
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stdout.on('data', (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
const activeDeadline = Date.now() + 5_000;
|
||||
while (!stdout.includes('active\n') && Date.now() < activeDeadline) {
|
||||
if (child.exitCode !== null || child.signalCode !== null) break;
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
assert.match(stdout, /active\n/);
|
||||
|
||||
const retained = await Promise.race([
|
||||
once(child, 'exit').then(() => false),
|
||||
new Promise((resolve) => setTimeout(() => resolve(true), 250)),
|
||||
]);
|
||||
assert.equal(retained, true);
|
||||
|
||||
const exited = once(child, 'exit');
|
||||
assert.equal(child.kill('SIGTERM'), true);
|
||||
const [exitCode, signal] = await exited;
|
||||
assert.equal(exitCode, 0);
|
||||
assert.equal(signal, null);
|
||||
assert.match(stdout, /shutdown_requested\nstopped\n/);
|
||||
});
|
||||
|
||||
test('disabled process never creates credentials or starts the product runtime', async () => {
|
||||
let credentials = 0;
|
||||
let starts = 0;
|
||||
await assert.rejects(
|
||||
runProductionWorkerProcess({
|
||||
environment: {
|
||||
QL3_WORKER_RUNTIME_ENABLED: 'false',
|
||||
QL_DEPLOYMENT_PROFILE: 'edge',
|
||||
},
|
||||
signals: { subscribe() { return () => {}; } },
|
||||
emit() {},
|
||||
async createCredentials() {
|
||||
credentials += 1;
|
||||
return { async load() {} };
|
||||
},
|
||||
async start() {
|
||||
starts += 1;
|
||||
return { status: 'disabled', async stop() { return 'stopped'; } };
|
||||
},
|
||||
}),
|
||||
WorkerProcessError,
|
||||
);
|
||||
assert.equal(credentials, 0);
|
||||
assert.equal(starts, 0);
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
chmod,
|
||||
mkdtemp,
|
||||
rm,
|
||||
writeFile,
|
||||
} = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WorkerProcessConfigError,
|
||||
loadWorkerProcessConfig,
|
||||
} = require('@qinglong/worker-runtime/process-config');
|
||||
|
||||
async function fixture(t) {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'ql3-worker-config-'));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const capabilitiesFile = path.join(root, 'capabilities.json');
|
||||
await writeFile(
|
||||
capabilitiesFile,
|
||||
JSON.stringify({
|
||||
architecture: 'arm64',
|
||||
operatingSystem: 'linux',
|
||||
executors: ['local_process'],
|
||||
runtimes: [{ name: 'node', version: '24.18.0' }],
|
||||
labels: { site: 'edge-a' },
|
||||
capacity: {
|
||||
cpuCores: 2,
|
||||
memoryBytes: 512 * 1024 * 1024,
|
||||
},
|
||||
features: [],
|
||||
}),
|
||||
);
|
||||
await chmod(capabilitiesFile, 0o444);
|
||||
return {
|
||||
root,
|
||||
capabilitiesFile,
|
||||
environment: {
|
||||
QL3_WORKER_RUNTIME_ENABLED: 'true',
|
||||
QL_DEPLOYMENT_PROFILE: 'worker',
|
||||
QL3_WORKER_CAPACITY_PROFILE: 'edge',
|
||||
QL3_WORKER_ID: 'router-worker-1',
|
||||
QL3_WORKER_CONTROL_ORIGIN: 'https://control.example.internal:5801',
|
||||
QL3_WORKER_CAPABILITIES_FILE: capabilitiesFile,
|
||||
QL3_WORKER_JOURNAL_ROOT: path.join(root, 'journal'),
|
||||
QL3_WORKER_LOG_ROOT: path.join(root, 'logs'),
|
||||
QL3_WORKER_RECEIPT_ROOT: path.join(root, 'receipts'),
|
||||
QL3_WORKER_CERTIFICATE_STORE_ROOT: path.join(root, 'identity'),
|
||||
QL3_WORKER_TRUST_ANCHOR_FILE: path.join(root, 'ca.pem'),
|
||||
QL3_WORKER_CREDENTIAL_TOKEN_FILE: path.join(root, 'token'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('disabled Worker runtime does not read paths, credentials or capabilities', async () => {
|
||||
const reads = [];
|
||||
const environment = new Proxy(
|
||||
{
|
||||
QL3_WORKER_RUNTIME_ENABLED: 'false',
|
||||
QL_DEPLOYMENT_PROFILE: 'edge',
|
||||
},
|
||||
{
|
||||
get(target, property, receiver) {
|
||||
reads.push(String(property));
|
||||
if (
|
||||
/CAPABILITIES_FILE|TOKEN|_ROOT|LAUNCHER_PATH|IDENTITY/.test(
|
||||
String(property),
|
||||
)
|
||||
) {
|
||||
throw new Error('disabled Worker read protected configuration');
|
||||
}
|
||||
return Reflect.get(target, property, receiver);
|
||||
},
|
||||
},
|
||||
);
|
||||
assert.deepEqual(await loadWorkerProcessConfig(environment), {
|
||||
enabled: false,
|
||||
profile: 'edge',
|
||||
});
|
||||
assert.equal(
|
||||
reads.some((name) =>
|
||||
/CAPABILITIES_FILE|TOKEN|_ROOT|LAUNCHER_PATH|IDENTITY/.test(name),
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('loads canonical edge defaults and bounded node overrides', async (t) => {
|
||||
const current = await fixture(t);
|
||||
const edge = await loadWorkerProcessConfig(current.environment);
|
||||
assert.equal(edge.enabled, true);
|
||||
assert.equal(edge.profile, 'worker');
|
||||
assert.equal(edge.capacityProfile, 'edge');
|
||||
assert.equal(edge.workerId, 'router-worker-1');
|
||||
assert.equal(edge.origin, 'https://control.example.internal:5801');
|
||||
assert.deepEqual(edge.capabilities, {
|
||||
architecture: 'arm64',
|
||||
executors: ['local_process'],
|
||||
operatingSystem: 'linux',
|
||||
runtimes: [{ name: 'node', version: '24.18.0' }],
|
||||
labels: { site: 'edge-a' },
|
||||
capacity: {
|
||||
cpuCores: 2,
|
||||
memoryBytes: 512 * 1024 * 1024,
|
||||
},
|
||||
features: [],
|
||||
});
|
||||
assert.equal(edge.maxConcurrentRuns, 1);
|
||||
assert.deepEqual(edge.lifecycle, {
|
||||
cadenceMs: 2_000,
|
||||
leaseDurationMs: 45_000,
|
||||
heartbeatIntervalMs: 10_000,
|
||||
drainTimeoutMs: 60_000,
|
||||
drainPollMs: 500,
|
||||
requestTimeoutMs: 15_000,
|
||||
maximumJournalEntries: 64,
|
||||
maximumRecordsPerTick: 4,
|
||||
maximumSupervisionRecordsPerTick: 4,
|
||||
});
|
||||
|
||||
const node = await loadWorkerProcessConfig({
|
||||
...current.environment,
|
||||
QL3_WORKER_CAPACITY_PROFILE: 'node',
|
||||
QL3_WORKER_MAX_CONCURRENT_RUNS: '32',
|
||||
QL3_WORKER_MAXIMUM_JOURNAL_ENTRIES: '512',
|
||||
QL3_WORKER_IDENTITY_BOOTSTRAP_PRIVATE_KEY_FILE:
|
||||
path.join(current.root, 'client.key'),
|
||||
QL3_WORKER_IDENTITY_BOOTSTRAP_CERTIFICATE_FILE:
|
||||
path.join(current.root, 'client.crt'),
|
||||
QL3_WORKER_EXPECTED_CREDENTIAL_ID: 'worker_primary',
|
||||
QL3_WORKER_LAUNCHER_PATH: '/usr/local/bin/ql3-launcher',
|
||||
QL3_WORKER_LAUNCHER_SHA256: 'a'.repeat(64),
|
||||
});
|
||||
assert.equal(node.maxConcurrentRuns, 32);
|
||||
assert.equal(node.lifecycle.cadenceMs, 500);
|
||||
assert.equal(node.lifecycle.maximumJournalEntries, 512);
|
||||
assert.equal(node.identity.expectedCredentialId, 'worker_primary');
|
||||
assert.equal(
|
||||
node.identity.bootstrap.privateKeyFile,
|
||||
path.join(current.root, 'client.key'),
|
||||
);
|
||||
assert.deepEqual(node.executor, {
|
||||
launcherPath: '/usr/local/bin/ql3-launcher',
|
||||
expectedLauncherSha256: 'a'.repeat(64),
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects widened profiles, origins, heartbeat and filesystem configuration', async (t) => {
|
||||
const current = await fixture(t);
|
||||
for (const patch of [
|
||||
{ QL_DEPLOYMENT_PROFILE: 'standalone' },
|
||||
{ QL3_WORKER_CAPACITY_PROFILE: 'cluster' },
|
||||
{ QL3_WORKER_ID: 'unsafe worker' },
|
||||
{ QL3_WORKER_CONTROL_ORIGIN: 'http://control.internal' },
|
||||
{ QL3_WORKER_CONTROL_ORIGIN: 'https://user@control.internal' },
|
||||
{ QL3_WORKER_JOURNAL_ROOT: 'relative/journal' },
|
||||
{ QL3_WORKER_HEARTBEAT_INTERVAL_MS: '30000' },
|
||||
{ QL3_WORKER_MAX_CONCURRENT_RUNS: '5' },
|
||||
{
|
||||
QL3_WORKER_IDENTITY_BOOTSTRAP_PRIVATE_KEY_FILE:
|
||||
path.join(current.root, 'client.key'),
|
||||
},
|
||||
{
|
||||
QL3_WORKER_LAUNCHER_PATH: '/usr/local/bin/ql3-launcher',
|
||||
},
|
||||
]) {
|
||||
await assert.rejects(
|
||||
loadWorkerProcessConfig({
|
||||
...current.environment,
|
||||
...patch,
|
||||
}),
|
||||
WorkerProcessConfigError,
|
||||
);
|
||||
}
|
||||
|
||||
await chmod(current.capabilitiesFile, 0o666);
|
||||
await assert.rejects(
|
||||
loadWorkerProcessConfig(current.environment),
|
||||
WorkerProcessConfigError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
chmod,
|
||||
copyFile,
|
||||
mkdtemp,
|
||||
readdir,
|
||||
rm,
|
||||
writeFile,
|
||||
} = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WorkerProcessIdentityError,
|
||||
createWorkerProcessCredentialProvider,
|
||||
} = require('@qinglong/worker-runtime/process-identity');
|
||||
|
||||
const FIXTURES = path.resolve(
|
||||
__dirname,
|
||||
'../../ql3-cluster-control/test/fixtures/mtls',
|
||||
);
|
||||
|
||||
async function fixture(t) {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'ql3-worker-identity-'));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
await chmod(root, 0o700);
|
||||
const privateKeyFile = path.join(root, 'client-key.pem');
|
||||
const certificateChainFile = path.join(root, 'client-cert.pem');
|
||||
const trustAnchorFile = path.join(root, 'ca-cert.pem');
|
||||
const credentialTokenFile = path.join(root, 'credential-token');
|
||||
await Promise.all([
|
||||
copyFile(path.join(FIXTURES, 'client-key.pem'), privateKeyFile),
|
||||
copyFile(path.join(FIXTURES, 'client-cert.pem'), certificateChainFile),
|
||||
copyFile(path.join(FIXTURES, 'ca-cert.pem'), trustAnchorFile),
|
||||
writeFile(
|
||||
credentialTokenFile,
|
||||
`ql3w_worker_primary_${Buffer.alloc(32, 7).toString('base64url')}\n`,
|
||||
),
|
||||
]);
|
||||
await Promise.all([
|
||||
chmod(privateKeyFile, 0o600),
|
||||
chmod(certificateChainFile, 0o444),
|
||||
chmod(trustAnchorFile, 0o444),
|
||||
chmod(credentialTokenFile, 0o600),
|
||||
]);
|
||||
return {
|
||||
root,
|
||||
config: {
|
||||
certificateStoreRoot: path.join(root, 'store'),
|
||||
trustAnchorFile,
|
||||
credentialTokenFile,
|
||||
expectedCredentialId: 'worker_primary',
|
||||
bootstrap: {
|
||||
privateKeyFile,
|
||||
certificateChainFile,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('bootstraps one durable identity and returns disposable request credentials', async (t) => {
|
||||
const current = await fixture(t);
|
||||
const provider = await createWorkerProcessCredentialProvider(
|
||||
current.config,
|
||||
);
|
||||
const first = await provider.load();
|
||||
assert.match(first.authorization, /^Worker ql3w_worker_primary_/);
|
||||
assert.equal(Buffer.isBuffer(first.privateKeyPem), true);
|
||||
assert.equal(Buffer.isBuffer(first.certificateChainPem), true);
|
||||
assert.equal(first.trustAnchors.length, 1);
|
||||
first.dispose();
|
||||
|
||||
const generations = await readdir(
|
||||
path.join(current.config.certificateStoreRoot, 'generations'),
|
||||
);
|
||||
assert.equal(generations.length, 1);
|
||||
const reloaded = await createWorkerProcessCredentialProvider(
|
||||
current.config,
|
||||
);
|
||||
const afterReload = await reloaded.load();
|
||||
afterReload.dispose();
|
||||
assert.equal(
|
||||
(
|
||||
await readdir(
|
||||
path.join(current.config.certificateStoreRoot, 'generations'),
|
||||
)
|
||||
).length,
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
test('fails closed for unsafe bootstrap material or absent active identity', async (t) => {
|
||||
const current = await fixture(t);
|
||||
await chmod(current.config.bootstrap.privateKeyFile, 0o644);
|
||||
await assert.rejects(
|
||||
createWorkerProcessCredentialProvider(current.config),
|
||||
WorkerProcessIdentityError,
|
||||
);
|
||||
await assert.rejects(
|
||||
createWorkerProcessCredentialProvider({
|
||||
certificateStoreRoot: path.join(current.root, 'empty-store'),
|
||||
trustAnchorFile: current.config.trustAnchorFile,
|
||||
credentialTokenFile: current.config.credentialTokenFile,
|
||||
}),
|
||||
WorkerProcessIdentityError,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const {
|
||||
chmod,
|
||||
mkdtemp,
|
||||
rename,
|
||||
rm,
|
||||
writeFile,
|
||||
} = require('node:fs/promises');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
generateWorkerCertificateEnrollment,
|
||||
} = require('../dist/credential/workerCertificateEnrollment');
|
||||
const {
|
||||
WorkerCertificateFileStore,
|
||||
} = require('../dist/credential/workerCertificateStore');
|
||||
const {
|
||||
WorkerProductionCredentialProvider,
|
||||
} = require('../dist/credential/workerProductionCredentialProvider');
|
||||
const {
|
||||
createCertificateAuthority,
|
||||
} = require('./helpers/certificateAuthority.cjs');
|
||||
|
||||
function ql3w(credentialId, fill) {
|
||||
return `ql3w_${credentialId}_${Buffer.alloc(32, fill).toString('base64url')}`;
|
||||
}
|
||||
|
||||
async function issueIdentity(ca, workerId, now) {
|
||||
const enrollment = await generateWorkerCertificateEnrollment({ workerId });
|
||||
try {
|
||||
return {
|
||||
privateKeyPem: Buffer.from(enrollment.privateKeyPem),
|
||||
certificateChainPem: await ca.issue(
|
||||
enrollment.certificateSigningRequestPem,
|
||||
),
|
||||
trustAnchors: [ca.certificatePem],
|
||||
now,
|
||||
};
|
||||
} finally {
|
||||
enrollment.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
async function fixture(t) {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), 'ql3-worker-credentials-'));
|
||||
t.after(() => rm(root, { recursive: true, force: true }));
|
||||
const now = Date.now();
|
||||
const ca = await createCertificateAuthority({ now });
|
||||
const store = new WorkerCertificateFileStore({
|
||||
rootDirectory: path.join(root, 'identity'),
|
||||
});
|
||||
const identity = await issueIdentity(ca, 'edge-1', now);
|
||||
await store.install(identity);
|
||||
identity.privateKeyPem.fill(0);
|
||||
const tokenFile = path.join(root, 'worker-token');
|
||||
await writeFile(tokenFile, `${ql3w('worker_primary', 7)}\n`, { mode: 0o600 });
|
||||
let trustLoads = 0;
|
||||
const provider = new WorkerProductionCredentialProvider({
|
||||
certificateStore: store,
|
||||
trustAnchors: {
|
||||
async load() {
|
||||
trustLoads += 1;
|
||||
return [ca.certificatePem];
|
||||
},
|
||||
},
|
||||
credentialTokenFile: tokenFile,
|
||||
expectedCredentialId: 'worker_primary',
|
||||
now: () => now,
|
||||
});
|
||||
return { root, now, ca, store, tokenFile, provider, trustLoads: () => trustLoads };
|
||||
}
|
||||
|
||||
test('loads and disposes the current certificate and ql3w generations', async (t) => {
|
||||
const context = await fixture(t);
|
||||
const first = await context.provider.load();
|
||||
const firstCertificate = Buffer.from(first.certificateChainPem);
|
||||
assert.equal(first.authorization, `Worker ${ql3w('worker_primary', 7)}`);
|
||||
assert.equal(Buffer.isBuffer(first.privateKeyPem), true);
|
||||
first.dispose();
|
||||
assert.equal(
|
||||
first.privateKeyPem.equals(Buffer.alloc(first.privateKeyPem.length)),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
first.certificateChainPem.equals(
|
||||
Buffer.alloc(first.certificateChainPem.length),
|
||||
),
|
||||
true,
|
||||
);
|
||||
|
||||
const replacement = `${context.tokenFile}.next`;
|
||||
await writeFile(replacement, `${ql3w('worker_primary', 8)}\n`, {
|
||||
mode: 0o600,
|
||||
});
|
||||
await rename(replacement, context.tokenFile);
|
||||
const secondIdentity = await issueIdentity(
|
||||
context.ca,
|
||||
'edge-1',
|
||||
context.now + 1_000,
|
||||
);
|
||||
await context.store.install(secondIdentity);
|
||||
secondIdentity.privateKeyPem.fill(0);
|
||||
|
||||
const second = await context.provider.load();
|
||||
try {
|
||||
assert.equal(second.authorization, `Worker ${ql3w('worker_primary', 8)}`);
|
||||
assert.equal(
|
||||
firstCertificate.equals(second.certificateChainPem),
|
||||
false,
|
||||
);
|
||||
assert.equal(context.trustLoads(), 2);
|
||||
} finally {
|
||||
firstCertificate.fill(0);
|
||||
second.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('fails closed for token identity drift and broad file permissions', async (t) => {
|
||||
const context = await fixture(t);
|
||||
const drifted = new WorkerProductionCredentialProvider({
|
||||
certificateStore: context.store,
|
||||
trustAnchors: { async load() { return [context.ca.certificatePem]; } },
|
||||
credentialTokenFile: context.tokenFile,
|
||||
expectedCredentialId: 'different_credential',
|
||||
now: () => context.now,
|
||||
});
|
||||
await assert.rejects(drifted.load(), /credentials_unavailable/);
|
||||
|
||||
await chmod(context.tokenFile, 0o644);
|
||||
await assert.rejects(context.provider.load(), /credentials_unavailable/);
|
||||
});
|
||||
|
||||
test('honors pre-abort before reading trust, certificate or token authority', async () => {
|
||||
let reads = 0;
|
||||
const provider = new WorkerProductionCredentialProvider({
|
||||
certificateStore: {
|
||||
async readActive() { reads += 1; throw new Error('not reached'); },
|
||||
},
|
||||
trustAnchors: {
|
||||
async load() { reads += 1; throw new Error('not reached'); },
|
||||
},
|
||||
credentialTokenFile: '/private/ql3-worker-token',
|
||||
});
|
||||
const controller = new AbortController();
|
||||
const reason = new Error('cancelled');
|
||||
controller.abort(reason);
|
||||
await assert.rejects(provider.load(controller.signal), reason);
|
||||
assert.equal(reads, 0);
|
||||
});
|
||||
|
||||
test('rejects unsafe token paths and credential identifiers at construction', () => {
|
||||
const base = {
|
||||
certificateStore: { async readActive() { return undefined; } },
|
||||
trustAnchors: { async load() { return []; } },
|
||||
};
|
||||
assert.throws(
|
||||
() => new WorkerProductionCredentialProvider({
|
||||
...base,
|
||||
credentialTokenFile: 'relative-token',
|
||||
}),
|
||||
/invalid_configuration/,
|
||||
);
|
||||
assert.throws(
|
||||
() => new WorkerProductionCredentialProvider({
|
||||
...base,
|
||||
credentialTokenFile: '/private/ql3-worker-token',
|
||||
expectedCredentialId: 'invalid id',
|
||||
}),
|
||||
/invalid_configuration/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WorkerIngressHttpsClient,
|
||||
WorkerIngressHttpsClientError,
|
||||
} = require('../dist/remote-execution/transport/workerIngressHttpsClient');
|
||||
const {
|
||||
WorkerSessionHttpsClient,
|
||||
} = require('../dist/session/workerSessionHttpsClient');
|
||||
const {
|
||||
WorkerSessionCoordinator,
|
||||
} = require('../dist/session/workerSessionCoordinator');
|
||||
|
||||
const SESSION_ID = '018f5c64-9b9d-7f1a-8c2d-1234567890ac';
|
||||
|
||||
function capabilities() {
|
||||
return {
|
||||
architecture: 'x64',
|
||||
operatingSystem: 'linux',
|
||||
executors: ['local_process'],
|
||||
runtimes: [{ name: 'node', version: '24.14.0' }],
|
||||
labels: {},
|
||||
capacity: { cpuCores: 1, memoryBytes: 256 * 1024 * 1024 },
|
||||
features: [],
|
||||
};
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
let now = 1_000;
|
||||
let version = -1;
|
||||
let status = 'online';
|
||||
let rejectionStatus;
|
||||
const calls = [];
|
||||
const transport = new WorkerIngressHttpsClient({
|
||||
origin: 'https://worker-control.invalid',
|
||||
credentials: { async load() { throw new Error('not reached'); } },
|
||||
});
|
||||
transport.postJson = async (request) => {
|
||||
calls.push(request.body);
|
||||
if (rejectionStatus !== undefined) {
|
||||
throw new WorkerIngressHttpsClientError(
|
||||
'response_rejected',
|
||||
rejectionStatus,
|
||||
);
|
||||
}
|
||||
if (request.body.schema.endsWith('register@v1')) {
|
||||
version = 0;
|
||||
status = 'online';
|
||||
return Buffer.from(JSON.stringify({
|
||||
schema: request.body.schema,
|
||||
workerId: 'edge-1', sessionId: SESSION_ID,
|
||||
generation: 1, version, status,
|
||||
leaseExpiresAtMs: now + 45_000,
|
||||
replacedSession: false,
|
||||
}));
|
||||
}
|
||||
version += 1;
|
||||
if (request.body.schema.endsWith('transition@v1')) {
|
||||
status = request.body.status;
|
||||
}
|
||||
return Buffer.from(JSON.stringify({
|
||||
schema: request.body.schema,
|
||||
workerId: 'edge-1', sessionId: SESSION_ID,
|
||||
generation: 1, version, status,
|
||||
leaseExpiresAtMs: now + 45_000,
|
||||
}));
|
||||
};
|
||||
const coordinator = new WorkerSessionCoordinator({
|
||||
client: new WorkerSessionHttpsClient({ client: transport }),
|
||||
workerId: 'edge-1',
|
||||
capabilities: capabilities(),
|
||||
maxConcurrentRuns: 2,
|
||||
availableSlots: () => 1,
|
||||
leaseDurationMs: 45_000,
|
||||
heartbeatIntervalMs: 10_000,
|
||||
now: () => now,
|
||||
createSessionId: () => SESSION_ID,
|
||||
});
|
||||
return {
|
||||
coordinator,
|
||||
calls,
|
||||
advance(value) { now += value; },
|
||||
setNow(value) { now = value; },
|
||||
rejectWith(value) { rejectionStatus = value; },
|
||||
};
|
||||
}
|
||||
|
||||
test('registers canonical capabilities and exposes one live execution Session', async () => {
|
||||
const context = fixture();
|
||||
const registered = await context.coordinator.register();
|
||||
assert.equal(registered.status, 'available');
|
||||
assert.equal(context.coordinator.current().sessionId, SESSION_ID);
|
||||
assert.equal(context.calls.length, 1);
|
||||
assert.equal(context.calls[0].availableSlots, 1);
|
||||
assert.equal(
|
||||
require('node:crypto').createHash('sha256')
|
||||
.update(context.calls[0].capabilitiesJson).digest('hex'),
|
||||
context.calls[0].capabilitiesHash,
|
||||
);
|
||||
});
|
||||
|
||||
test('uses caller-driven due heartbeats without creating a timer', async () => {
|
||||
const context = fixture();
|
||||
await context.coordinator.register();
|
||||
assert.equal((await context.coordinator.tick()).status, 'not_due');
|
||||
context.advance(10_000);
|
||||
const result = await context.coordinator.tick();
|
||||
assert.equal(result.status, 'heartbeat');
|
||||
assert.equal(result.session.version, 1);
|
||||
assert.equal(context.calls.length, 2);
|
||||
});
|
||||
|
||||
test('drains with zero capacity, heartbeats, then disconnects in order', async () => {
|
||||
const context = fixture();
|
||||
await context.coordinator.register();
|
||||
await context.coordinator.beginDrain();
|
||||
assert.equal(context.coordinator.current().status, 'draining');
|
||||
context.advance(10_000);
|
||||
await context.coordinator.tick();
|
||||
assert.equal(context.calls.at(-1).availableSlots, 0);
|
||||
await context.coordinator.disconnect();
|
||||
assert.equal(context.coordinator.currentRecord().status, 'offline');
|
||||
assert.equal(context.coordinator.current().status, 'offline');
|
||||
const completedCalls = context.calls.length;
|
||||
await context.coordinator.beginDrain();
|
||||
await context.coordinator.disconnect();
|
||||
assert.equal(context.calls.length, completedCalls);
|
||||
});
|
||||
|
||||
test('fails closed locally after the observed Session lease expires', async () => {
|
||||
const context = fixture();
|
||||
await context.coordinator.register();
|
||||
context.advance(45_000);
|
||||
assert.equal(context.coordinator.current(), undefined);
|
||||
assert.equal((await context.coordinator.tick()).status, 'lease_expired');
|
||||
await assert.rejects(context.coordinator.beginDrain(), /lease_expired/);
|
||||
});
|
||||
|
||||
test('pauses Pull on credential/fence rejection and recovers the same Session', async () => {
|
||||
const context = fixture();
|
||||
await context.coordinator.register();
|
||||
context.advance(10_000);
|
||||
context.rejectWith(401);
|
||||
await assert.rejects(
|
||||
context.coordinator.tick(),
|
||||
(error) => error.reason === 'credential_rejected',
|
||||
);
|
||||
assert.equal(context.coordinator.current(), undefined);
|
||||
|
||||
context.rejectWith(undefined);
|
||||
const recovered = await context.coordinator.tick();
|
||||
assert.equal(recovered.status, 'heartbeat');
|
||||
assert.equal(context.coordinator.current().sessionId, SESSION_ID);
|
||||
|
||||
context.advance(10_000);
|
||||
context.rejectWith(409);
|
||||
await assert.rejects(
|
||||
context.coordinator.tick(),
|
||||
(error) => error.reason === 'session_fenced',
|
||||
);
|
||||
assert.equal(context.coordinator.current(), undefined);
|
||||
});
|
||||
|
||||
test('keeps certificate fail-closed until an authenticated heartbeat succeeds', async () => {
|
||||
const context = fixture();
|
||||
await context.coordinator.register();
|
||||
context.coordinator.failClosed();
|
||||
assert.equal(context.coordinator.current(), undefined);
|
||||
assert.equal((await context.coordinator.tick()).status, 'not_due');
|
||||
assert.equal(context.coordinator.current(), undefined);
|
||||
context.advance(10_000);
|
||||
assert.equal((await context.coordinator.tick()).status, 'heartbeat');
|
||||
assert.equal(context.coordinator.current().sessionId, SESSION_ID);
|
||||
});
|
||||
|
||||
test('keeps a live Session available across transient server failure', async () => {
|
||||
const context = fixture();
|
||||
await context.coordinator.register();
|
||||
context.advance(10_000);
|
||||
context.rejectWith(503);
|
||||
await assert.rejects(
|
||||
context.coordinator.tick(),
|
||||
(error) => error.reason === 'transport_unavailable',
|
||||
);
|
||||
assert.equal(context.coordinator.current().sessionId, SESSION_ID);
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
const {
|
||||
WORKER_SESSION_HEARTBEAT_SCHEMA,
|
||||
WORKER_SESSION_REGISTER_SCHEMA,
|
||||
WORKER_SESSION_TRANSITION_SCHEMA,
|
||||
} = require('@qinglong/runtime-core/worker-session-transport');
|
||||
const {
|
||||
WorkerIngressHttpsClient,
|
||||
WorkerIngressHttpsClientError,
|
||||
} = require('../dist/remote-execution/transport/workerIngressHttpsClient');
|
||||
const {
|
||||
WorkerSessionHttpsClient,
|
||||
} = require('../dist/session/workerSessionHttpsClient');
|
||||
|
||||
const authority = {
|
||||
workerId: 'edge-1',
|
||||
sessionId: '018f5c64-9b9d-7f1a-8c2d-1234567890ac',
|
||||
};
|
||||
const capabilitiesJson = '{}';
|
||||
const capabilitiesHash =
|
||||
'44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a';
|
||||
|
||||
function client(exchange) {
|
||||
const transport = new WorkerIngressHttpsClient({
|
||||
origin: 'https://worker-control.invalid',
|
||||
credentials: { async load() { throw new Error('not reached'); } },
|
||||
});
|
||||
transport.postJson = exchange;
|
||||
return new WorkerSessionHttpsClient({ client: transport });
|
||||
}
|
||||
|
||||
test('registers one exact path-bound Session over the shared client', async () => {
|
||||
let observed;
|
||||
const session = client(async (request) => {
|
||||
observed = request;
|
||||
return Buffer.from(JSON.stringify({
|
||||
schema: WORKER_SESSION_REGISTER_SCHEMA,
|
||||
...authority,
|
||||
generation: 1,
|
||||
version: 0,
|
||||
status: 'online',
|
||||
leaseExpiresAtMs: 50_000,
|
||||
replacedSession: false,
|
||||
}));
|
||||
});
|
||||
const result = await session.register({
|
||||
...authority,
|
||||
capabilitiesJson,
|
||||
capabilitiesHash,
|
||||
maxConcurrentRuns: 2,
|
||||
availableSlots: 1,
|
||||
leaseDurationMs: 30_000,
|
||||
});
|
||||
assert.equal(result.status, 'online');
|
||||
assert.match(observed.path, /\/register$/);
|
||||
assert.equal(observed.body.schema, WORKER_SESSION_REGISTER_SCHEMA);
|
||||
assert.equal('workerId' in observed.body, false);
|
||||
assert.equal(observed.maximumRequestBytes, 20 * 1024);
|
||||
});
|
||||
|
||||
test('heartbeats and transitions under exact next-version fences', async () => {
|
||||
const operations = [];
|
||||
const session = client(async (request) => {
|
||||
operations.push(request.body.schema);
|
||||
const transition = request.body.schema === WORKER_SESSION_TRANSITION_SCHEMA;
|
||||
return Buffer.from(JSON.stringify({
|
||||
schema: request.body.schema,
|
||||
...authority,
|
||||
generation: 2,
|
||||
version: request.body.expectedVersion + 1,
|
||||
status: transition ? request.body.status : 'online',
|
||||
leaseExpiresAtMs: 60_000,
|
||||
}));
|
||||
});
|
||||
const heartbeat = await session.heartbeat({
|
||||
...authority, generation: 2, expectedVersion: 3,
|
||||
availableSlots: 1, leaseDurationMs: 30_000,
|
||||
});
|
||||
assert.equal(heartbeat.version, 4);
|
||||
const drained = await session.transition({
|
||||
...authority, generation: 2, expectedVersion: 4, status: 'draining',
|
||||
});
|
||||
assert.equal(drained.status, 'draining');
|
||||
assert.deepEqual(operations, [
|
||||
WORKER_SESSION_HEARTBEAT_SCHEMA,
|
||||
WORKER_SESSION_TRANSITION_SCHEMA,
|
||||
]);
|
||||
});
|
||||
|
||||
test('rejects response authority and version drift', async () => {
|
||||
const session = client(async () => Buffer.from(JSON.stringify({
|
||||
schema: WORKER_SESSION_HEARTBEAT_SCHEMA,
|
||||
...authority,
|
||||
generation: 2,
|
||||
version: 99,
|
||||
status: 'online',
|
||||
leaseExpiresAtMs: 60_000,
|
||||
})));
|
||||
await assert.rejects(
|
||||
session.heartbeat({
|
||||
...authority, generation: 2, expectedVersion: 3,
|
||||
availableSlots: 1, leaseDurationMs: 30_000,
|
||||
}),
|
||||
/response_invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test('classifies credential rejection and Session fencing without error bodies', async () => {
|
||||
for (const [statusCode, reason] of [
|
||||
[401, 'credential_rejected'],
|
||||
[403, 'credential_rejected'],
|
||||
[409, 'session_fenced'],
|
||||
[503, 'transport_unavailable'],
|
||||
]) {
|
||||
const session = client(async () => {
|
||||
throw new WorkerIngressHttpsClientError(
|
||||
'response_rejected',
|
||||
statusCode,
|
||||
);
|
||||
});
|
||||
await assert.rejects(
|
||||
session.heartbeat({
|
||||
...authority, generation: 2, expectedVersion: 3,
|
||||
availableSlots: 1, leaseDurationMs: 30_000,
|
||||
}),
|
||||
(error) => error.reason === reason,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user