syndicate-js/packages/core/src/runtime/supervise.ts

85 lines
3.1 KiB
TypeScript

/// SPDX-License-Identifier: GPL-3.0-or-later
/// SPDX-FileCopyrightText: Copyright © 2021-2023 Tony Garnock-Jones <tonyg@leastfixedpoint.com>
import { State } from '../gen/service';
import { Actor, AnyValue, LocalAction, Turn } from './actor';
import { Field } from './dataflow';
type StateName = State['_variant'];
export enum SupervisorRestartPolicy {
ALWAYS,
ON_ERROR_ONLY,
}
export type SupervisorConfiguration = {
intensity: number,
period: number, /* seconds */
pauseTime: number, /* seconds */
sleepTime: number, /* seconds */
restartPolicy: SupervisorRestartPolicy,
};
export const DEFAULT_CONFIG: SupervisorConfiguration = {
intensity: 1,
period: 5,
pauseTime: 0.2,
sleepTime: 10,
restartPolicy: SupervisorRestartPolicy.ON_ERROR_ONLY,
};
export class Supervisor {
readonly config: SupervisorConfiguration;
readonly nameFunction: () => AnyValue;
readonly bootFunction: LocalAction;
readonly restarts: number[] = []; /* timestamps */
readonly state: Field<StateName> = Turn.active.field('started');
supervisee: Actor | null = null;
static always(nameFunction: () => AnyValue, bootFunction: LocalAction): Supervisor {
return new Supervisor({ restartPolicy: SupervisorRestartPolicy.ALWAYS },
nameFunction,
bootFunction);
}
constructor(config: Partial<SupervisorConfiguration>,
nameFunction: () => AnyValue,
bootFunction: LocalAction)
{
this.config = Object.assign({}, DEFAULT_CONFIG, config);
this.nameFunction = nameFunction;
this.bootFunction = bootFunction;
this.startSupervisee();
}
startSupervisee() {
Turn.active.facet(() => {
this.state.value = 'started';
this.supervisee = Turn.active.spawnLink(this.bootFunction);
if (this.supervisee) this.supervisee.name = this.nameFunction();
Turn.activeFacet.onStop(() => {
const exitReason = this.supervisee?.exitReason;
if (!exitReason) {
// Supervisor shutdown. Supervisee will exit soon.
return;
}
if (exitReason.ok && (this.config.restartPolicy === SupervisorRestartPolicy.ON_ERROR_ONLY)) {
this.state.value = 'complete';
} else {
this.state.value = exitReason.ok ? 'complete' : 'failed';
const now = +(new Date());
const oldestToKeep = now - this.config.period * 1000.0;
this.restarts.push(now);
while (this.restarts.length && (this.restarts[0] < oldestToKeep)) {
this.restarts.shift();
}
const waitTime = (this.restarts.length > this.config.intensity)
? this.config.sleepTime
: this.config.pauseTime;
Turn.active.after(waitTime * 1000.0, () => this.startSupervisee());
}
});
});
}
}