feat(ql3): expose bounded worker session observation

This commit is contained in:
whyour
2026-08-20 12:55:44 +08:00
parent 6265e31dce
commit 4a4fa85f51
24 changed files with 1437 additions and 28 deletions
+8
View File
@@ -56,6 +56,9 @@
"worker-session": [
"dist/worker/workerSession.d.ts"
],
"worker-session-observation": [
"dist/worker/workerSessionObservation.d.ts"
],
"worker-session-transport": [
"dist/worker/workerSessionTransport.d.ts"
],
@@ -804,6 +807,11 @@
"require": "./dist/worker/workerSession.js",
"default": "./dist/worker/workerSession.js"
},
"./worker-session-observation": {
"types": "./dist/worker/workerSessionObservation.d.ts",
"require": "./dist/worker/workerSessionObservation.js",
"default": "./dist/worker/workerSessionObservation.js"
},
"./worker-session-transport": {
"types": "./dist/worker/workerSessionTransport.d.ts",
"require": "./dist/worker/workerSessionTransport.js",
@@ -0,0 +1,144 @@
import {
REMOTE_WORKER_PROTOCOL_RANGE,
parseRemoteWorkerCapabilities,
remoteWorkerProtocolIsCompatible,
type RemoteWorkerArchitecture,
type RemoteWorkerRuntimeCapability,
type RemoteWorkerSupportTier,
} from '../remote-execution/remoteWorkerPlacement';
import {
assertWorkerSessionRecord,
type WorkerSessionRecord,
} from './workerSession';
export const MAX_WORKER_SESSION_OBSERVATION_PAGE_SIZE = 16;
export type WorkerSessionObservedLifecycle =
| 'online'
| 'draining'
| 'offline'
| 'lease_expired';
export type WorkerSessionObservedCompatibility =
| 'default_placement'
| 'explicit_placement_required'
| 'protocol_incompatible';
export interface WorkerSessionObservationSummary {
readonly workerId: string;
readonly sessionId: string;
readonly generation: number;
readonly sessionVersion: number;
readonly lifecycle: WorkerSessionObservedLifecycle;
readonly compatibility: WorkerSessionObservedCompatibility;
readonly architecture: RemoteWorkerArchitecture;
readonly supportTier: RemoteWorkerSupportTier;
readonly protocolVersion: string;
readonly operatingSystem: string | null;
readonly maxConcurrentRuns: number;
readonly availableSlots: number;
readonly registeredAtMs: number;
readonly lastHeartbeatAtMs: number;
readonly leaseExpiresAtMs: number;
readonly updatedAtMs: number;
readonly observedAtMs: number;
}
export interface WorkerSessionObservation
extends WorkerSessionObservationSummary {
readonly runtimes: readonly Readonly<RemoteWorkerRuntimeCapability>[];
readonly declaredCapacity: Readonly<{
readonly cpuCores: number | null;
readonly memoryBytes: number | null;
readonly diskBytes: number | null;
readonly gpuCount: number;
}>;
}
export interface WorkerSessionObservationPage {
readonly observedAtMs: number;
readonly workers: readonly Readonly<WorkerSessionObservationSummary>[];
readonly nextCursor: string | null;
}
export interface WorkerSessionInspection {
readonly observedAtMs: number;
readonly worker: Readonly<WorkerSessionObservation> | null;
}
function invalid(message: string): never {
throw new TypeError(`Worker Session observation is invalid: ${message}`);
}
function observedLifecycle(
worker: Readonly<WorkerSessionRecord>,
observedAtMs: number,
): WorkerSessionObservedLifecycle {
if (worker.status === 'offline') return 'offline';
if (worker.leaseExpiresAtMs <= observedAtMs) return 'lease_expired';
return worker.status;
}
export function projectWorkerSessionObservation(
worker: Readonly<WorkerSessionRecord>,
observedAtMs: number,
): Readonly<WorkerSessionObservation> {
assertWorkerSessionRecord(worker);
if (
!Number.isSafeInteger(observedAtMs) ||
observedAtMs < worker.updatedAtMs
) {
return invalid('observedAtMs is invalid');
}
const capabilities = parseRemoteWorkerCapabilities(worker);
const protocolCompatible = remoteWorkerProtocolIsCompatible(
capabilities.protocolVersion,
REMOTE_WORKER_PROTOCOL_RANGE,
);
const compatibility: WorkerSessionObservedCompatibility = protocolCompatible
? capabilities.supportTier === 'tier1'
? 'default_placement'
: 'explicit_placement_required'
: 'protocol_incompatible';
return Object.freeze({
workerId: worker.workerId,
sessionId: worker.sessionId,
generation: worker.generation,
sessionVersion: worker.version,
lifecycle: observedLifecycle(worker, observedAtMs),
compatibility,
architecture: capabilities.architecture,
supportTier: capabilities.supportTier,
protocolVersion: capabilities.protocolVersion,
operatingSystem: capabilities.operatingSystem ?? null,
maxConcurrentRuns: worker.maxConcurrentRuns,
availableSlots: worker.availableSlots,
registeredAtMs: worker.registeredAtMs,
lastHeartbeatAtMs: worker.lastHeartbeatAtMs,
leaseExpiresAtMs: worker.leaseExpiresAtMs,
updatedAtMs: worker.updatedAtMs,
observedAtMs,
runtimes: Object.freeze(
(capabilities.runtimes ?? []).map((runtime) =>
Object.freeze({ ...runtime }),
),
),
declaredCapacity: Object.freeze({
cpuCores: capabilities.capacity?.cpuCores ?? null,
memoryBytes: capabilities.capacity?.memoryBytes ?? null,
diskBytes: capabilities.capacity?.diskBytes ?? null,
gpuCount: capabilities.capacity?.gpu?.length ?? 0,
}),
});
}
export function summarizeWorkerSessionObservation(
observation: Readonly<WorkerSessionObservation>,
): Readonly<WorkerSessionObservationSummary> {
const {
runtimes: _runtimes,
declaredCapacity: _declaredCapacity,
...summary
} = observation;
return Object.freeze(summary);
}
@@ -0,0 +1,143 @@
const assert = require('node:assert/strict');
const { test } = require('node:test');
const {
canonicalRemoteWorkerCapabilities,
} = require('@qinglong/runtime-core/remote-dispatch');
const {
projectWorkerSessionObservation,
summarizeWorkerSessionObservation,
} = require('@qinglong/runtime-core/worker-session-observation');
function worker(overrides = {}) {
const canonical = canonicalRemoteWorkerCapabilities(
overrides.capabilities ?? {
architecture: 'arm64',
executors: ['remote-worker'],
protocolVersion: '1.0.0',
supportTier: 'tier1',
operatingSystem: 'linux',
runtimes: [{ name: 'node', version: '24.18.0' }],
labels: { private: 'must-not-project' },
capacity: {
cpuCores: 2,
memoryBytes: 512 * 1024 * 1024,
diskBytes: 4 * 1024 * 1024 * 1024,
gpu: [{ vendor: 'example', model: 'must-not-project' }],
},
features: ['must-not-project'],
},
);
return Object.freeze({
workerId: 'worker-a',
sessionId: '018f0f5d-7b6a-7a11-8f4d-2f7b4f477001',
generation: 3,
status: 'online',
version: 9,
capabilitiesJson: canonical.json,
capabilitiesHash: canonical.hash,
maxConcurrentRuns: 4,
availableSlots: 2,
registeredAtMs: 1_000,
lastHeartbeatAtMs: 1_900,
leaseExpiresAtMs: 3_000,
updatedAtMs: 1_900,
...Object.fromEntries(
Object.entries(overrides).filter(([key]) => key !== 'capabilities'),
),
});
}
test('projects only bounded Worker compatibility, runtime and capacity facts', () => {
const observation = projectWorkerSessionObservation(worker(), 2_000);
assert.deepEqual(observation, {
workerId: 'worker-a',
sessionId: '018f0f5d-7b6a-7a11-8f4d-2f7b4f477001',
generation: 3,
sessionVersion: 9,
lifecycle: 'online',
compatibility: 'default_placement',
architecture: 'arm64',
supportTier: 'tier1',
protocolVersion: '1.0.0',
operatingSystem: 'linux',
maxConcurrentRuns: 4,
availableSlots: 2,
registeredAtMs: 1_000,
lastHeartbeatAtMs: 1_900,
leaseExpiresAtMs: 3_000,
updatedAtMs: 1_900,
observedAtMs: 2_000,
runtimes: [{ name: 'node', version: '24.18.0' }],
declaredCapacity: {
cpuCores: 2,
memoryBytes: 512 * 1024 * 1024,
diskBytes: 4 * 1024 * 1024 * 1024,
gpuCount: 1,
},
});
assert.doesNotMatch(
JSON.stringify(observation),
/must-not-project|labels|features|model/,
);
const summary = summarizeWorkerSessionObservation(observation);
assert.equal(Object.hasOwn(summary, 'runtimes'), false);
assert.equal(Object.hasOwn(summary, 'declaredCapacity'), false);
});
test('distinguishes explicit Tier, incompatible protocol and lifecycle state', () => {
const candidate = projectWorkerSessionObservation(
worker({
capabilities: {
architecture: 's390x',
executors: ['remote-worker'],
protocolVersion: '1.0.0',
supportTier: 'candidate',
},
}),
2_000,
);
assert.equal(candidate.compatibility, 'explicit_placement_required');
const incompatible = projectWorkerSessionObservation(
worker({
capabilities: {
architecture: 'amd64',
executors: ['remote-worker'],
protocolVersion: '2.0.0',
supportTier: 'tier1',
},
}),
2_000,
);
assert.equal(incompatible.compatibility, 'protocol_incompatible');
assert.equal(
projectWorkerSessionObservation(worker(), 3_000).lifecycle,
'lease_expired',
);
assert.equal(
projectWorkerSessionObservation(
worker({ status: 'draining', availableSlots: 0 }),
2_000,
).lifecycle,
'draining',
);
assert.equal(
projectWorkerSessionObservation(
worker({
status: 'offline',
availableSlots: 0,
leaseExpiresAtMs: 1_900,
}),
2_000,
).lifecycle,
'offline',
);
});
test('rejects an observation clock before the durable Session head', () => {
assert.throws(
() => projectWorkerSessionObservation(worker(), 1_899),
/observedAtMs is invalid/,
);
});