import { satisfies, valid, validRange } from 'semver'; import { isWorkerLeaseActive, type WorkerCapabilities, type WorkerRecord, } from './worker'; export const MAX_PLACEMENT_VALUES = 16; export const MAX_PLACEMENT_PREFERENCES = 16; export const MAX_PLACEMENT_CANDIDATES = 64; export interface WorkerRuntimeRequirement { name: string; versionRange?: string; } export interface WorkerPlacementRequired { architectures?: readonly string[]; operatingSystems?: readonly string[]; executors?: readonly string[]; runtimes?: readonly WorkerRuntimeRequirement[]; labels?: Readonly>; minMemoryBytes?: number; minDiskBytes?: number; gpuVendor?: string; features?: readonly string[]; } export interface WorkerPlacementPreference { labels: Readonly>; weight: number; } export interface WorkerPlacementSpec { required?: WorkerPlacementRequired; preferred?: readonly WorkerPlacementPreference[]; } export type WorkerPlacementMismatch = | 'worker_unavailable' | 'architecture' | 'operating_system' | 'executor' | 'runtime' | 'label' | 'memory' | 'disk' | 'gpu' | 'feature'; export interface WorkerPlacementDecision { matches: boolean; score: number; mismatches: readonly WorkerPlacementMismatch[]; } export interface WorkerPlacementCandidate { worker: WorkerRecord; score: number; } function invalid(message: string): never { throw new TypeError(`Worker PlacementSpec is invalid: ${message}`); } function object(value: unknown, name: string): Record { if (!value || typeof value !== 'object' || Array.isArray(value)) { invalid(`${name} must be an object`); } return value as Record; } function assertKeys( value: Record, name: string, keys: readonly string[], ): void { if (Object.keys(value).some((key) => !keys.includes(key))) { invalid(`${name} contains an unknown field`); } } function stringValue(value: unknown, name: string, maximum = 128): string { if ( typeof value !== 'string' || value.length < 1 || value.length > maximum || value.includes('\0') || /[\u0000-\u001f\u007f]/.test(value) ) { invalid(`${name} is invalid`); } return value; } function stringList(value: unknown, name: string): string[] | undefined { if (value === undefined) return undefined; if (!Array.isArray(value) || value.length > MAX_PLACEMENT_VALUES) { invalid(`${name} must contain at most ${MAX_PLACEMENT_VALUES} values`); } const values = value.map((item, index) => stringValue(item, `${name}[${index}]`, 64), ); if (new Set(values).size !== values.length) { invalid(`${name} must not contain duplicates`); } return values.sort(); } function labels( value: unknown, name: string, ): Record | undefined { if (value === undefined) return undefined; const source = object(value, name); const entries = Object.entries(source); if (entries.length > MAX_PLACEMENT_VALUES) { invalid(`${name} must contain at most ${MAX_PLACEMENT_VALUES} labels`); } return Object.fromEntries( entries .map(([key, candidate]) => [ stringValue(key, `${name} key`, 128), stringValue(candidate, `${name}.${key}`, 256), ]) .sort(([left], [right]) => left.localeCompare(right)), ); } function positiveInteger(value: unknown, name: string): number | undefined { if (value === undefined) return undefined; if (!Number.isSafeInteger(value) || (value as number) < 1) { invalid(`${name} must be a positive safe integer`); } return value as number; } function runtimes(value: unknown): WorkerRuntimeRequirement[] | undefined { if (value === undefined) return undefined; if (!Array.isArray(value) || value.length > MAX_PLACEMENT_VALUES) { invalid( `required.runtimes must contain at most ${MAX_PLACEMENT_VALUES} values`, ); } const requirements = value.map((candidate, index) => { const requirement = object(candidate, `required.runtimes[${index}]`); assertKeys(requirement, `required.runtimes[${index}]`, [ 'name', 'versionRange', ]); const name = stringValue( requirement.name, `required.runtimes[${index}].name`, 64, ); if (requirement.versionRange === undefined) return { name }; const versionRange = stringValue( requirement.versionRange, `required.runtimes[${index}].versionRange`, 128, ); if (!validRange(versionRange)) { invalid(`required.runtimes[${index}].versionRange is not semver`); } return { name, versionRange }; }); if ( new Set(requirements.map((item) => item.name)).size !== requirements.length ) { invalid('required.runtimes must not repeat a runtime name'); } return requirements.sort((left, right) => left.name.localeCompare(right.name), ); } function normalizeRequired( value: unknown, ): WorkerPlacementRequired | undefined { if (value === undefined) return undefined; const required = object(value, 'required'); assertKeys(required, 'required', [ 'architectures', 'operatingSystems', 'executors', 'runtimes', 'labels', 'minMemoryBytes', 'minDiskBytes', 'gpuVendor', 'features', ]); const architectures = stringList( required.architectures, 'required.architectures', ); const operatingSystems = stringList( required.operatingSystems, 'required.operatingSystems', ); const executors = stringList(required.executors, 'required.executors'); const runtimeRequirements = runtimes(required.runtimes); const requiredLabels = labels(required.labels, 'required.labels'); const minMemoryBytes = positiveInteger( required.minMemoryBytes, 'required.minMemoryBytes', ); const minDiskBytes = positiveInteger( required.minDiskBytes, 'required.minDiskBytes', ); const gpuVendor = required.gpuVendor === undefined ? undefined : stringValue(required.gpuVendor, 'required.gpuVendor', 64); const features = stringList(required.features, 'required.features'); return { ...(architectures === undefined ? {} : { architectures }), ...(operatingSystems === undefined ? {} : { operatingSystems }), ...(executors === undefined ? {} : { executors }), ...(runtimeRequirements === undefined ? {} : { runtimes: runtimeRequirements }), ...(requiredLabels === undefined ? {} : { labels: requiredLabels }), ...(minMemoryBytes === undefined ? {} : { minMemoryBytes }), ...(minDiskBytes === undefined ? {} : { minDiskBytes }), ...(gpuVendor === undefined ? {} : { gpuVendor }), ...(features === undefined ? {} : { features }), }; } function normalizePreferred( value: unknown, ): WorkerPlacementPreference[] | undefined { if (value === undefined) return undefined; if (!Array.isArray(value) || value.length > MAX_PLACEMENT_PREFERENCES) { invalid( `preferred must contain at most ${MAX_PLACEMENT_PREFERENCES} values`, ); } return value.map((candidate, index) => { const preference = object(candidate, `preferred[${index}]`); assertKeys(preference, `preferred[${index}]`, ['labels', 'weight']); const preferredLabels = labels( preference.labels, `preferred[${index}].labels`, ); if (!preferredLabels || Object.keys(preferredLabels).length === 0) { invalid(`preferred[${index}].labels must not be empty`); } if ( !Number.isSafeInteger(preference.weight) || (preference.weight as number) < 1 || (preference.weight as number) > 100 ) { invalid(`preferred[${index}].weight must be between 1 and 100`); } return { labels: preferredLabels, weight: preference.weight as number, }; }); } export function normalizeWorkerPlacementSpec( value: unknown, ): WorkerPlacementSpec { const placement = object(value, 'placement'); assertKeys(placement, 'placement', ['required', 'preferred']); const required = normalizeRequired(placement.required); const preferred = normalizePreferred(placement.preferred); return { ...(required === undefined ? {} : { required }), ...(preferred === undefined ? {} : { preferred }), }; } function hasLabels( capabilities: WorkerCapabilities, expected: Readonly>, ): boolean { return Object.entries(expected).every( ([key, value]) => capabilities.labels[key] === value, ); } function hasRuntime( capabilities: WorkerCapabilities, requirement: WorkerRuntimeRequirement, ): boolean { return capabilities.runtimes.some((runtime) => { if (runtime.name !== requirement.name) return false; if (!requirement.versionRange) return true; return ( valid(runtime.version) !== null && satisfies(runtime.version, requirement.versionRange, { includePrerelease: true, }) ); }); } function matchNormalizedWorkerPlacement( worker: WorkerRecord, placement: WorkerPlacementSpec, observedAtMs: number, ): WorkerPlacementDecision { const required = placement.required ?? {}; const capabilities = worker.capabilities; const mismatches: WorkerPlacementMismatch[] = []; if ( worker.status !== 'online' || worker.availableSlots < 1 || !isWorkerLeaseActive(worker, observedAtMs) ) { mismatches.push('worker_unavailable'); } if ( required.architectures?.length && !required.architectures.includes(capabilities.architecture) ) { mismatches.push('architecture'); } if ( required.operatingSystems?.length && !required.operatingSystems.includes(capabilities.operatingSystem) ) { mismatches.push('operating_system'); } if ( required.executors?.some( (executor) => !capabilities.executors.includes(executor), ) ) { mismatches.push('executor'); } if ( required.runtimes?.some((runtime) => !hasRuntime(capabilities, runtime)) ) { mismatches.push('runtime'); } if (required.labels && !hasLabels(capabilities, required.labels)) { mismatches.push('label'); } if ( required.minMemoryBytes !== undefined && (capabilities.capacity.memoryBytes ?? 0) < required.minMemoryBytes ) { mismatches.push('memory'); } if ( required.minDiskBytes !== undefined && (capabilities.capacity.diskBytes ?? 0) < required.minDiskBytes ) { mismatches.push('disk'); } if ( required.gpuVendor !== undefined && !capabilities.capacity.gpu?.some((gpu) => gpu.vendor === required.gpuVendor) ) { mismatches.push('gpu'); } if ( required.features?.some( (feature) => !capabilities.features.includes(feature), ) ) { mismatches.push('feature'); } const score = (placement.preferred ?? []).reduce( (total, preference) => total + (hasLabels(capabilities, preference.labels) ? preference.weight : 0), 0, ); return { matches: mismatches.length === 0, score, mismatches }; } export function matchesWorkerPlacement( worker: WorkerRecord, placementValue: unknown, observedAtMs: number, ): WorkerPlacementDecision { return matchNormalizedWorkerPlacement( worker, normalizeWorkerPlacementSpec(placementValue), observedAtMs, ); } export function selectWorkerCandidates( workers: readonly WorkerRecord[], placement: unknown, observedAtMs: number, limit = 16, ): WorkerPlacementCandidate[] { if (workers.length > MAX_PLACEMENT_CANDIDATES) { throw new RangeError( `workers must contain at most ${MAX_PLACEMENT_CANDIDATES} candidates`, ); } if ( !Number.isSafeInteger(limit) || limit < 1 || limit > MAX_PLACEMENT_CANDIDATES ) { throw new RangeError( `limit must be between 1 and ${MAX_PLACEMENT_CANDIDATES}`, ); } const normalizedPlacement = normalizeWorkerPlacementSpec(placement); return workers .map((worker) => ({ worker, decision: matchNormalizedWorkerPlacement( worker, normalizedPlacement, observedAtMs, ), })) .filter((candidate) => candidate.decision.matches) .sort( (left, right) => right.decision.score - left.decision.score || right.worker.availableSlots - left.worker.availableSlots || left.worker.id.localeCompare(right.worker.id), ) .slice(0, limit) .map(({ worker, decision }) => ({ worker, score: decision.score })); }