mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): expose bounded worker session observation
This commit is contained in:
@@ -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/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user