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);
}