feat(ql3): version worker support tier admission

This commit is contained in:
whyour
2026-08-20 12:02:15 +08:00
parent 6e11258d45
commit 6265e31dce
28 changed files with 616 additions and 57 deletions
@@ -0,0 +1,144 @@
import { semver } from '../versioning/pinnedSemver';
export const REMOTE_WORKER_PROTOCOL_VERSION = '1.0.0';
export const REMOTE_WORKER_PROTOCOL_RANGE = '>=1.0.0 <2.0.0';
export const REMOTE_WORKER_SUPPORT_TIERS = [
'tier1',
'candidate',
'experimental',
'legacy-only',
] as const;
export type RemoteWorkerSupportTier =
(typeof REMOTE_WORKER_SUPPORT_TIERS)[number];
export const REMOTE_WORKER_ARCHITECTURES_BY_SUPPORT_TIER = Object.freeze({
tier1: Object.freeze(['amd64', 'arm64'] as const),
candidate: Object.freeze(['ppc64le', 's390x'] as const),
experimental: Object.freeze(['arm/v7'] as const),
'legacy-only': Object.freeze(['arm/v6', '386'] as const),
});
export type RemoteWorkerArchitecture =
(typeof REMOTE_WORKER_ARCHITECTURES_BY_SUPPORT_TIER)[RemoteWorkerSupportTier][number];
const REMOTE_WORKER_ARCHITECTURES = Object.freeze(
Object.values(REMOTE_WORKER_ARCHITECTURES_BY_SUPPORT_TIER).flat(),
);
function invalid(message: string): never {
throw new TypeError(
`Remote Worker compatibility value is invalid: ${message}`,
);
}
export function normalizeRemoteWorkerProtocolVersion(value: unknown): string {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > 64 ||
semver().valid(value) === null
) {
invalid('protocolVersion is not semver');
}
return value;
}
export function normalizeRemoteWorkerArchitecture(
value: unknown,
): RemoteWorkerArchitecture {
if (
typeof value !== 'string' ||
!REMOTE_WORKER_ARCHITECTURES.includes(value as RemoteWorkerArchitecture)
) {
invalid('architecture is outside the release support policy');
}
return value as RemoteWorkerArchitecture;
}
export function remoteWorkerSupportTierForArchitecture(
value: unknown,
): RemoteWorkerSupportTier {
const architecture = normalizeRemoteWorkerArchitecture(value);
for (const supportTier of REMOTE_WORKER_SUPPORT_TIERS) {
if (
(
REMOTE_WORKER_ARCHITECTURES_BY_SUPPORT_TIER[
supportTier
] as readonly string[]
).includes(architecture)
) {
return supportTier;
}
}
return invalid('architecture has no supportTier');
}
export function remoteWorkerArchitectureForNodeRuntime(
nodeArchitecture: string,
armVersion?: unknown,
): RemoteWorkerArchitecture {
if (nodeArchitecture === 'x64') return 'amd64';
if (nodeArchitecture === 'arm64') return 'arm64';
if (nodeArchitecture === 'ppc64') return 'ppc64le';
if (nodeArchitecture === 's390x') return 's390x';
if (nodeArchitecture === 'ia32') return '386';
if (nodeArchitecture === 'arm' && (armVersion === 6 || armVersion === '6')) {
return 'arm/v6';
}
if (nodeArchitecture === 'arm' && (armVersion === 7 || armVersion === '7')) {
return 'arm/v7';
}
return invalid('Node runtime architecture is unsupported or ambiguous');
}
export function normalizeRemoteWorkerProtocolVersionRange(
value: unknown,
): string {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > 128 ||
semver().validRange(value) === null
) {
invalid('protocolVersionRange is not semver');
}
return value;
}
export function normalizeRemoteWorkerSupportTier(
value: unknown,
): RemoteWorkerSupportTier {
if (
typeof value !== 'string' ||
!REMOTE_WORKER_SUPPORT_TIERS.includes(value as RemoteWorkerSupportTier)
) {
invalid('supportTier is unknown');
}
return value as RemoteWorkerSupportTier;
}
export function assertRemoteWorkerCompatibilityCapability(
value: unknown,
): void {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
invalid('capabilities must be an object');
}
const source = value as Record<string, unknown>;
const architecture = normalizeRemoteWorkerArchitecture(source.architecture);
normalizeRemoteWorkerProtocolVersion(source.protocolVersion);
const supportTier = normalizeRemoteWorkerSupportTier(source.supportTier);
if (remoteWorkerSupportTierForArchitecture(architecture) !== supportTier) {
invalid('architecture does not belong to supportTier');
}
}
export function remoteWorkerProtocolIsCompatible(
protocolVersion: string,
requiredRange = REMOTE_WORKER_PROTOCOL_RANGE,
): boolean {
const version = normalizeRemoteWorkerProtocolVersion(protocolVersion);
const range = normalizeRemoteWorkerProtocolVersionRange(requiredRange);
return semver().satisfies(version, range, { includePrerelease: true });
}
@@ -2,6 +2,20 @@ import { createHash } from 'node:crypto';
import type { WorkerSessionRecord } from '../worker/workerSession';
import { assertWorkerSessionRecord } from '../worker/workerSession';
import { semver } from '../versioning/pinnedSemver';
import {
REMOTE_WORKER_PROTOCOL_RANGE,
REMOTE_WORKER_SUPPORT_TIERS,
normalizeRemoteWorkerArchitecture,
normalizeRemoteWorkerProtocolVersion,
normalizeRemoteWorkerProtocolVersionRange,
normalizeRemoteWorkerSupportTier,
remoteWorkerProtocolIsCompatible,
remoteWorkerSupportTierForArchitecture,
type RemoteWorkerArchitecture,
type RemoteWorkerSupportTier,
} from './remoteWorkerCompatibility';
export * from './remoteWorkerCompatibility';
export const REMOTE_WORKER_EXECUTOR_CAPABILITY = 'remote-worker';
export const MAX_REMOTE_PLACEMENT_VALUES = 16;
@@ -20,8 +34,10 @@ export interface RemoteWorkerRuntimeCapability {
}
export interface RemoteWorkerCapabilities {
readonly architecture: string;
readonly architecture: RemoteWorkerArchitecture;
readonly executors: readonly string[];
readonly protocolVersion: string;
readonly supportTier: RemoteWorkerSupportTier;
readonly operatingSystem?: string;
readonly runtimes?: readonly RemoteWorkerRuntimeCapability[];
readonly labels?: Readonly<Record<string, string>>;
@@ -45,10 +61,12 @@ export interface RemoteWorkerRuntimeRequirement {
export interface RemoteWorkerPlacementSpec {
readonly required?: Readonly<{
readonly architectures?: readonly string[];
readonly architectures?: readonly RemoteWorkerArchitecture[];
readonly operatingSystems?: readonly string[];
readonly executors?: readonly string[];
readonly protocolVersionRange?: string;
readonly runtimes?: readonly RemoteWorkerRuntimeRequirement[];
readonly supportTiers?: readonly RemoteWorkerSupportTier[];
readonly labels?: Readonly<Record<string, string>>;
readonly minMemoryBytes?: number;
readonly minDiskBytes?: number;
@@ -63,6 +81,8 @@ export interface RemoteWorkerPlacementSpec {
export type RemoteWorkerPlacementMismatch =
| 'worker_unavailable'
| 'support_tier'
| 'protocol_version'
| 'architecture'
| 'operating_system'
| 'executor'
@@ -186,17 +206,19 @@ export function normalizeRemoteWorkerCapabilities(
const source = object(value, 'capabilities');
exactKeys(
source,
['architecture', 'executors'],
['architecture', 'executors', 'protocolVersion', 'supportTier'],
['capacity', 'features', 'labels', 'operatingSystem', 'runtimes'],
'capabilities',
);
const architecture = boundedString(
source.architecture,
'architecture',
32,
CAPABILITY_NAME,
);
const architecture = normalizeRemoteWorkerArchitecture(source.architecture);
const executors = sortedStrings(source.executors, 'executors', 16, false);
const protocolVersion = normalizeRemoteWorkerProtocolVersion(
source.protocolVersion,
);
const supportTier = normalizeRemoteWorkerSupportTier(source.supportTier);
if (remoteWorkerSupportTierForArchitecture(architecture) !== supportTier) {
invalid('architecture does not belong to supportTier');
}
const operatingSystem =
source.operatingSystem === undefined
? undefined
@@ -330,6 +352,8 @@ export function normalizeRemoteWorkerCapabilities(
return Object.freeze({
architecture,
executors,
protocolVersion,
supportTier,
...(operatingSystem === undefined ? {} : { operatingSystem }),
...(runtimes === undefined ? {} : { runtimes }),
...(labels === undefined ? {} : { labels }),
@@ -377,6 +401,21 @@ function optionalStringList(
: sortedStrings(value, label, MAX_REMOTE_PLACEMENT_VALUES);
}
function optionalArchitectureList(
value: unknown,
label: string,
): readonly RemoteWorkerArchitecture[] | undefined {
if (value === undefined) return undefined;
if (!Array.isArray(value) || value.length > MAX_REMOTE_PLACEMENT_VALUES) {
invalid(`${label} is invalid`);
}
const result = value.map((item) => normalizeRemoteWorkerArchitecture(item));
if (new Set(result).size !== result.length) {
invalid(`${label} contains duplicates`);
}
return Object.freeze([...result].sort());
}
export function normalizeRemoteWorkerPlacement(
value: unknown,
): RemoteWorkerPlacementSpec {
@@ -397,11 +436,13 @@ export function normalizeRemoteWorkerPlacement(
'minDiskBytes',
'minMemoryBytes',
'operatingSystems',
'protocolVersionRange',
'runtimes',
'supportTiers',
],
'placement.required',
);
const architectures = optionalStringList(
const architectures = optionalArchitectureList(
candidate.architectures,
'placement.required.architectures',
);
@@ -413,6 +454,23 @@ export function normalizeRemoteWorkerPlacement(
candidate.executors,
'placement.required.executors',
);
const protocolVersionRange = candidate.protocolVersionRange === undefined
? undefined
: normalizeRemoteWorkerProtocolVersionRange(
candidate.protocolVersionRange,
);
let supportTiers: readonly RemoteWorkerSupportTier[] | undefined;
if (candidate.supportTiers !== undefined) {
if (
!Array.isArray(candidate.supportTiers) ||
candidate.supportTiers.length > REMOTE_WORKER_SUPPORT_TIERS.length
) invalid('placement.required.supportTiers is invalid');
const mapped = candidate.supportTiers.map((value) =>
normalizeRemoteWorkerSupportTier(value));
if (new Set(mapped).size !== mapped.length)
invalid('placement.required.supportTiers contains duplicates');
supportTiers = Object.freeze([...mapped].sort());
}
const features = optionalStringList(
candidate.features,
'placement.required.features',
@@ -468,7 +526,9 @@ export function normalizeRemoteWorkerPlacement(
...(architectures === undefined ? {} : { architectures }),
...(operatingSystems === undefined ? {} : { operatingSystems }),
...(executors === undefined ? {} : { executors }),
...(protocolVersionRange === undefined ? {} : { protocolVersionRange }),
...(runtimes === undefined ? {} : { runtimes }),
...(supportTiers === undefined ? {} : { supportTiers }),
...(candidate.labels === undefined
? {}
: {
@@ -594,6 +654,20 @@ export function evaluateRemoteWorkerPlacement(
worker.leaseExpiresAtMs <= observedAtMs
)
mismatches.push('worker_unavailable');
const requiredSupportTiers = required.supportTiers ?? ['tier1'];
if (!requiredSupportTiers.includes(capabilities.supportTier))
mismatches.push('support_tier');
if (
!remoteWorkerProtocolIsCompatible(
capabilities.protocolVersion,
REMOTE_WORKER_PROTOCOL_RANGE,
) ||
(required.protocolVersionRange !== undefined &&
!remoteWorkerProtocolIsCompatible(
capabilities.protocolVersion,
required.protocolVersionRange,
))
) mismatches.push('protocol_version');
if (
required.architectures?.length &&
!required.architectures.includes(capabilities.architecture)
@@ -1,4 +1,5 @@
import { createHash } from 'node:crypto';
import { assertRemoteWorkerCompatibilityCapability } from '../remote-execution/remoteWorkerCompatibility';
export const WORKER_SESSION_STATUSES = ['online', 'draining', 'offline'] as const;
export type WorkerSessionStatus = (typeof WORKER_SESSION_STATUSES)[number];
@@ -141,6 +142,11 @@ export function assertWorkerCapabilitiesSnapshot(
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
invalid('Worker capabilities snapshot must be an object');
}
try {
assertRemoteWorkerCompatibilityCapability(parsed);
} catch {
invalid('Worker capabilities compatibility contract is invalid');
}
if (
createHash('sha256').update(capabilitiesJson, 'utf8').digest('hex') !==
capabilitiesHash
@@ -13,6 +13,7 @@ import {
type WorkerSessionRecord,
type WorkerSessionStatus,
} from './workerSession';
import { canonicalRemoteWorkerCapabilities } from '../remote-execution/remoteWorkerPlacement';
export const WORKER_SESSION_REGISTER_SCHEMA =
'qinglong/worker-session-register@v1';
@@ -153,6 +154,13 @@ function validateRegister(
command?.capabilitiesJson,
command?.capabilitiesHash,
);
const canonical = canonicalRemoteWorkerCapabilities(
JSON.parse(command.capabilitiesJson) as unknown,
);
if (
canonical.json !== command.capabilitiesJson ||
canonical.hash !== command.capabilitiesHash
) throw new TypeError('capabilities snapshot is not canonical');
assertWorkerConcurrency(
command?.maxConcurrentRuns,
command?.availableSlots,
@@ -20,6 +20,8 @@ function worker() {
const snapshot = canonicalRemoteWorkerCapabilities({
architecture: 'arm64',
executors: ['remote-worker'],
protocolVersion: '1.0.0',
supportTier: 'tier1',
operatingSystem: 'linux',
runtimes: [{ name: 'node', version: '24.18.0' }],
labels: { region: 'cn-east', tier: 'edge' },
@@ -90,7 +92,10 @@ test('rejects non-canonical snapshots and reports bounded mismatch classes', ()
const value = worker();
const reordered = {
...value,
capabilitiesJson: JSON.stringify({ executors: ['remote-worker'], architecture: 'arm64' }),
capabilitiesJson: JSON.stringify({
executors: ['remote-worker'], architecture: 'arm64',
protocolVersion: '1.0.0', supportTier: 'tier1',
}),
};
reordered.capabilitiesHash = require('node:crypto')
.createHash('sha256')
@@ -102,7 +107,7 @@ test('rejects non-canonical snapshots and reports bounded mismatch classes', ()
);
const decision = evaluateRemoteWorkerPlacement(
value,
{ required: { architectures: ['x64'], labels: { region: 'eu' } } },
{ required: { architectures: ['amd64'], labels: { region: 'eu' } } },
20_000,
);
assert.deepEqual(decision.mismatches, ['architecture', 'label']);
@@ -36,6 +36,8 @@ test('uses pinned SemVer for remote runtime range admission', () => {
const candidate = worker({
architecture: 'arm64',
executors: ['remote-worker'],
protocolVersion: '1.0.0',
supportTier: 'tier1',
runtimes: [{ name: 'node', version: '24.18.0' }],
});
@@ -54,3 +56,51 @@ test('uses pinned SemVer for remote runtime range admission', () => {
/versionRange is not semver/,
);
});
test('defaults to Tier 1 protocol v1 and requires explicit legacy placement', () => {
const legacy = worker({
architecture: 'arm/v6', executors: ['remote-worker'],
protocolVersion: '1.0.0', supportTier: 'legacy-only',
});
assert.deepEqual(evaluateRemoteWorkerPlacement(legacy, {}, 500), {
matches: false, score: 0, mismatches: ['support_tier'],
});
assert.deepEqual(evaluateRemoteWorkerPlacement(
legacy, { required: { supportTiers: ['legacy-only'] } }, 500,
), { matches: true, score: 0, mismatches: [] });
const incompatible = worker({
architecture: 'arm64', executors: ['remote-worker'],
protocolVersion: '2.0.0', supportTier: 'tier1',
});
assert.deepEqual(evaluateRemoteWorkerPlacement(incompatible, {}, 500), {
matches: false, score: 0, mismatches: ['protocol_version'],
});
});
test('rejects unversioned capabilities and unknown support policy', () => {
assert.throws(() => canonicalRemoteWorkerCapabilities({
architecture: 'arm64', executors: ['remote-worker'],
}), /shape is invalid/);
assert.throws(() => normalizeRemoteWorkerPlacement({
required: { supportTiers: ['unsupported'] },
}), /supportTier is unknown/);
assert.throws(() => normalizeRemoteWorkerPlacement({
required: { protocolVersionRange: 'not-a-range' },
}), /protocolVersionRange is not semver/);
assert.throws(() => canonicalRemoteWorkerCapabilities({
architecture: 'arm/v7', executors: ['remote-worker'],
protocolVersion: '1.0.0', supportTier: 'tier1',
}), /does not belong to supportTier/);
});
test('keeps Worker architecture tiers aligned with the release identity', () => {
const { REMOTE_WORKER_ARCHITECTURES_BY_SUPPORT_TIER } =
require('../dist/remote-execution/remoteWorkerCompatibility');
const release = require('../../../ql3-release.json');
assert.deepEqual(REMOTE_WORKER_ARCHITECTURES_BY_SUPPORT_TIER, {
tier1: release.architectureSupport.tier1,
candidate: release.architectureSupport.candidates,
experimental: release.architectureSupport.experimentalBlocked,
'legacy-only': release.architectureSupport.legacyOnly,
});
});
@@ -8,7 +8,7 @@ const {
} = require('../dist');
function capabilities() {
const json = '{"architecture":"arm64","executors":["remote-worker"]}';
const json = '{"architecture":"arm64","executors":["remote-worker"],"protocolVersion":"1.0.0","supportTier":"tier1"}';
return {
json,
hash: createHash('sha256').update(json).digest('hex'),
@@ -42,6 +42,10 @@ test('rejects forged capabilities, partial status capacity and invalid time', ()
() => assertWorkerCapabilitiesSnapshot(snapshot.json, '0'.repeat(64)),
InvalidWorkerSessionValueError,
);
const unversioned = '{"architecture":"arm64","executors":["remote-worker"]}';
assert.throws(() => assertWorkerCapabilitiesSnapshot(
unversioned, createHash('sha256').update(unversioned).digest('hex'),
), /compatibility contract is invalid/);
assert.throws(
() =>
assertWorkerSessionRecord({
@@ -1,6 +1,7 @@
'use strict';
const assert = require('node:assert/strict');
const { createHash } = require('node:crypto');
const { test } = require('node:test');
const {
createWorkerSessionHeartbeatRequestBody,
@@ -21,9 +22,10 @@ const authority = Object.freeze({
workerId: 'edge-1',
sessionId: '018f5c64-9b9d-7f1a-8c2d-1234567890ac',
});
const capabilitiesJson = '{}';
const capabilitiesHash =
'44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a';
const capabilitiesJson =
'{"architecture":"arm64","executors":["remote-worker"],"protocolVersion":"1.0.0","supportTier":"tier1"}';
const capabilitiesHash = createHash('sha256')
.update(capabilitiesJson).digest('hex');
function record(overrides = {}) {
return {