287 lines
10 KiB
TypeScript
287 lines
10 KiB
TypeScript
import { MatrixActor } from '@open-matrix/core/core/MatrixActor.js';
|
|
import { MatrixRuntime } from '@open-matrix/core/runtime/MatrixRuntime.js';
|
|
import {
|
|
LoadedPackageActor,
|
|
RuntimeControlActor,
|
|
RUNTIME_CONTROL_SERVICE,
|
|
loadedPackageMount,
|
|
loadedPackageServiceName,
|
|
packageKeyFromPackageRef,
|
|
runtimeControlMount,
|
|
type ILoadedPackageService,
|
|
type IRuntimeControlService,
|
|
type LoadedPackageManifest,
|
|
type LoadedPackageRecord,
|
|
type RuntimeControlState,
|
|
} from '@open-matrix/system-runtimes';
|
|
|
|
type MatrixIntrospect = ReturnType<MatrixActor['on$introspect']>;
|
|
type MatrixIntrospectChild = NonNullable<MatrixIntrospect['children']>[number];
|
|
|
|
export type RunnerRuntimeLifecycleState =
|
|
| 'starting'
|
|
| 'ready'
|
|
| 'live'
|
|
| 'stopping'
|
|
| 'stopped'
|
|
| 'failed';
|
|
|
|
export interface IRunnerRuntimeControlService {
|
|
readonly runtimeId: string;
|
|
readonly startedAt: string;
|
|
getState(): RunnerRuntimeLifecycleState;
|
|
getRuntime(): MatrixRuntime;
|
|
requestShutdown(reason: string): void;
|
|
}
|
|
|
|
const RUNNER_RUNTIME_CONTROL_SERVICE = 'runner-runtime-control';
|
|
|
|
export interface IMountedLoadedPackage {
|
|
readonly packageRef: string;
|
|
readonly packageKey: string;
|
|
readonly mount: string;
|
|
}
|
|
|
|
export interface IMountRunnerRuntimeControlOptions {
|
|
readonly runtime: MatrixRuntime;
|
|
readonly runtimeId: string;
|
|
readonly runtimeMount: string;
|
|
readonly controlMount: string;
|
|
readonly startedAt: string;
|
|
getState(): RunnerRuntimeLifecycleState;
|
|
requestShutdown(reason: string): void;
|
|
/**
|
|
* Optional loaded-package descriptor. When provided, a per-loaded-package
|
|
* actor is mounted at `system.runtimes.<runtimeId>.packages.<packageKey>`
|
|
* alongside the runtime control actor. Slice 1a wires this for runtimes
|
|
* that pass a single manifest at startup; multi-package runtimes are
|
|
* future work.
|
|
*/
|
|
readonly loadedPackage?: {
|
|
readonly packageRef: string;
|
|
readonly version?: string;
|
|
readonly manifest?: LoadedPackageManifest;
|
|
};
|
|
}
|
|
|
|
export interface IMountRunnerRuntimeControlResult {
|
|
readonly controlMount: string;
|
|
readonly runtimeMount: string;
|
|
readonly loadedPackages: readonly IMountedLoadedPackage[];
|
|
}
|
|
|
|
export class RunnerRuntimeControlActor extends MatrixActor {
|
|
static readonly hasDynamicChildren = true;
|
|
static description = 'Runtime control actor for Host-managed runner processes.';
|
|
|
|
static override accepts: Record<string, unknown> = {
|
|
'runtime.status': { description: 'Report runtime lifecycle and inventory status', options: 'object?' },
|
|
'runtime.ready': { description: 'Report whether the runtime is ready for traffic', options: 'object?' },
|
|
'runtime.health': { description: 'Report runtime health', options: 'object?' },
|
|
'runtime.shutdown': { description: 'Request graceful runtime shutdown', reason: 'string?' },
|
|
};
|
|
|
|
protected override onIntrospect(payload?: { depth?: 'basic' | 'full' }): MatrixIntrospect {
|
|
const base = super.onIntrospect(payload);
|
|
const service = this._service();
|
|
const currentMount = this.mount;
|
|
const children = service.getRuntime().getAllComponents()
|
|
.map((component): MatrixIntrospectChild | null => {
|
|
const mount = component.mount.trim();
|
|
if (!mount || mount === currentMount) {
|
|
return null;
|
|
}
|
|
return {
|
|
id: mount,
|
|
slot: 'runtime',
|
|
mount,
|
|
componentClass: component.constructor.name || 'MatrixActor',
|
|
accepts: [],
|
|
emits: [],
|
|
tracks: [],
|
|
};
|
|
})
|
|
.filter((child): child is MatrixIntrospectChild => child !== null)
|
|
.sort((left, right) => left.mount.localeCompare(right.mount));
|
|
return {
|
|
...base,
|
|
children: [...(base.children ?? []), ...children],
|
|
};
|
|
}
|
|
|
|
onRuntimeStatus(): Record<string, unknown> {
|
|
const service = this._service();
|
|
const runtime = service.getRuntime();
|
|
return {
|
|
ok: true,
|
|
runtimeId: service.runtimeId,
|
|
pid: process.pid,
|
|
...(process.env.MATRIX_HOST_HOME ? { hostHome: process.env.MATRIX_HOST_HOME } : {}),
|
|
state: service.getState(),
|
|
startedAt: service.startedAt,
|
|
uptimeMs: Date.now() - Date.parse(service.startedAt),
|
|
actorCount: runtime.getAllComponents().length,
|
|
mounts: runtime.getAllComponents().map((component) => component.mount),
|
|
};
|
|
}
|
|
|
|
onRuntimeReady(): Record<string, unknown> {
|
|
const service = this._service();
|
|
const state = service.getState();
|
|
return {
|
|
ok: true,
|
|
runtimeId: service.runtimeId,
|
|
ready: state === 'ready' || state === 'live',
|
|
state,
|
|
pid: process.pid,
|
|
...(process.env.MATRIX_HOST_HOME ? { hostHome: process.env.MATRIX_HOST_HOME } : {}),
|
|
};
|
|
}
|
|
|
|
onRuntimeHealth(): Record<string, unknown> {
|
|
const service = this._service();
|
|
const state = service.getState();
|
|
return {
|
|
ok: true,
|
|
healthy: state === 'ready' || state === 'live',
|
|
runtimeId: service.runtimeId,
|
|
state,
|
|
pid: process.pid,
|
|
...(process.env.MATRIX_HOST_HOME ? { hostHome: process.env.MATRIX_HOST_HOME } : {}),
|
|
};
|
|
}
|
|
|
|
onRuntimeShutdown(payload?: { reason?: string }): Record<string, unknown> {
|
|
const service = this._service();
|
|
const reason = typeof payload?.reason === 'string' && payload.reason.trim().length > 0
|
|
? payload.reason.trim()
|
|
: 'runtime.shutdown';
|
|
setTimeout(() => service.requestShutdown(reason), 0);
|
|
return {
|
|
ok: true,
|
|
accepted: true,
|
|
runtimeId: service.runtimeId,
|
|
state: 'stopping',
|
|
reason,
|
|
};
|
|
}
|
|
|
|
private _service(): IRunnerRuntimeControlService {
|
|
const service = this.context?.getService<IRunnerRuntimeControlService>(RUNNER_RUNTIME_CONTROL_SERVICE);
|
|
if (!service) {
|
|
throw new Error('Runner runtime control service is not registered');
|
|
}
|
|
return service;
|
|
}
|
|
}
|
|
|
|
export async function mountRunnerRuntimeControl(
|
|
options: IMountRunnerRuntimeControlOptions,
|
|
): Promise<IMountRunnerRuntimeControlResult> {
|
|
const service: IRunnerRuntimeControlService = {
|
|
runtimeId: options.runtimeId,
|
|
startedAt: options.startedAt,
|
|
getState: options.getState,
|
|
getRuntime: () => options.runtime,
|
|
requestShutdown: options.requestShutdown,
|
|
};
|
|
options.runtime.registerService(RUNNER_RUNTIME_CONTROL_SERVICE, service);
|
|
|
|
const canonicalRuntimeMount = runtimeControlMount(options.runtimeId);
|
|
|
|
// Build the loaded-package record set (Slice 1a: zero or one package).
|
|
const loadedPackages: LoadedPackageRecord[] = [];
|
|
if (options.loadedPackage && options.loadedPackage.packageRef.trim().length > 0) {
|
|
const packageRef = options.loadedPackage.packageRef.trim();
|
|
const packageKey = packageKeyFromPackageRef(packageRef);
|
|
const record: LoadedPackageRecord = {
|
|
packageRef,
|
|
packageKey,
|
|
mount: loadedPackageMount(options.runtimeId, packageRef),
|
|
state: 'loaded',
|
|
loadedAt: Date.parse(options.startedAt) || Date.now(),
|
|
...(options.loadedPackage.version ? { version: options.loadedPackage.version } : {}),
|
|
...(options.loadedPackage.manifest ? { manifest: options.loadedPackage.manifest } : {}),
|
|
};
|
|
loadedPackages.push(record);
|
|
}
|
|
|
|
// Register the runtime-control service consumed by RuntimeControlActor.
|
|
const runtimeControlService: IRuntimeControlService = {
|
|
getRuntimeState: (): RuntimeControlState => ({
|
|
runtimeId: options.runtimeId,
|
|
...(process.env.MATRIX_RUNTIME_DISPLAY_NAME ? { displayName: process.env.MATRIX_RUNTIME_DISPLAY_NAME } : {}),
|
|
...(loadedPackages[0]?.packageRef ? { packageRef: loadedPackages[0].packageRef } : {}),
|
|
controlMount: canonicalRuntimeMount,
|
|
state: lifecycleStateToControlState(options.getState()),
|
|
health: {
|
|
status: 'ok',
|
|
updatedAt: Date.now(),
|
|
},
|
|
registeredAt: Date.parse(options.startedAt) || Date.now(),
|
|
lastHeartbeat: Date.now(),
|
|
}),
|
|
getLoadedPackages: () => loadedPackages,
|
|
requestShutdown: options.requestShutdown,
|
|
requestReload: async () => ({ accepted: false, reason: 'runtime.reload is not implemented in Slice 1a' }),
|
|
};
|
|
options.runtime.registerService(RUNTIME_CONTROL_SERVICE, runtimeControlService);
|
|
|
|
// Mount the canonical Runtime control actor at system.runtimes.<runtimeId>.
|
|
// Slice 1b (control-actor-nav): the legacy `.control` sibling mount has been
|
|
// removed. The canonical runtime control mount is `system.runtimes.<runtimeId>`.
|
|
if (!options.runtime.hasComponent(canonicalRuntimeMount)) {
|
|
await options.runtime.createSupervised(RuntimeControlActor, canonicalRuntimeMount);
|
|
}
|
|
|
|
// Mount per-loaded-package actors and register their per-package services.
|
|
const mountedLoadedPackages: IMountedLoadedPackage[] = [];
|
|
for (const record of loadedPackages) {
|
|
const loadedPackageService: ILoadedPackageService = {
|
|
getRuntimeId: () => options.runtimeId,
|
|
getLoadedPackage: () => record,
|
|
requestReload: async () => ({ accepted: false, reason: 'package.reload is not implemented in Slice 1a' }),
|
|
};
|
|
options.runtime.registerService(loadedPackageServiceName(record.packageKey), loadedPackageService);
|
|
if (!options.runtime.hasComponent(record.mount)) {
|
|
await options.runtime.createSupervised(LoadedPackageActor, record.mount);
|
|
}
|
|
mountedLoadedPackages.push({
|
|
packageRef: record.packageRef,
|
|
packageKey: record.packageKey,
|
|
mount: record.mount,
|
|
});
|
|
}
|
|
|
|
return {
|
|
controlMount: canonicalRuntimeMount,
|
|
runtimeMount: canonicalRuntimeMount,
|
|
loadedPackages: mountedLoadedPackages,
|
|
};
|
|
}
|
|
|
|
function lifecycleStateToControlState(state: RunnerRuntimeLifecycleState): RuntimeControlState['state'] {
|
|
switch (state) {
|
|
case 'starting':
|
|
return 'starting';
|
|
case 'ready':
|
|
case 'live':
|
|
return 'running';
|
|
case 'stopping':
|
|
return 'stopping';
|
|
case 'stopped':
|
|
case 'failed':
|
|
return 'stopped';
|
|
}
|
|
}
|
|
|
|
export function defaultRunnerRuntimeMount(runtimeId: string): string {
|
|
return `system.runtimes.${runtimeId}`;
|
|
}
|
|
|
|
export function defaultRunnerControlMount(runtimeId: string): string {
|
|
// Slice 1b (control-actor-nav): the canonical runtime control mount is
|
|
// `system.runtimes.<runtimeId>`. The legacy `.control` sibling is gone.
|
|
return defaultRunnerRuntimeMount(runtimeId);
|
|
}
|