189 lines
6.5 KiB
JavaScript
189 lines
6.5 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.HealthDaemon = exports.EXIT_SUCCESS = void 0;
|
|
const defaultTrigger_1 = require("../trigger/defaultTrigger");
|
|
exports.EXIT_SUCCESS = 'EXIT_SUCCESS';
|
|
/**
|
|
* Wanted a structure that deals with controlling daemons by their health status
|
|
* States:
|
|
* -- Waiting for dependencies to be success
|
|
* -- Running: Daemon is running and the status is in the health
|
|
*
|
|
*/
|
|
class HealthDaemon {
|
|
constructor(daemon, dependencies, id, ready, effects) {
|
|
this.daemon = daemon;
|
|
this.dependencies = dependencies;
|
|
this.id = id;
|
|
this.ready = ready;
|
|
this.effects = effects;
|
|
this._health = { result: 'waiting', message: null };
|
|
this.healthWatchers = [];
|
|
this.running = false;
|
|
this.resolvedReady = false;
|
|
this.session = null;
|
|
this.readyPromise = new Promise((resolve) => (this.resolveReady = () => {
|
|
resolve();
|
|
this.resolvedReady = true;
|
|
}));
|
|
this.dependencies.forEach((d) => d.addWatcher(() => this.updateStatus()));
|
|
}
|
|
/** Run after we want to do cleanup */
|
|
async term(termOptions) {
|
|
this.healthWatchers = [];
|
|
this.running = false;
|
|
await this.stopSession();
|
|
await this.daemon?.term({
|
|
...termOptions,
|
|
});
|
|
}
|
|
/** Want to add another notifier that the health might have changed */
|
|
addWatcher(watcher) {
|
|
this.healthWatchers.push(watcher);
|
|
}
|
|
get health() {
|
|
return Object.freeze(this._health);
|
|
}
|
|
async changeRunning(newStatus) {
|
|
if (this.running === newStatus)
|
|
return;
|
|
this.running = newStatus;
|
|
if (newStatus) {
|
|
console.debug(`Launching ${this.id}...`);
|
|
this.startSession();
|
|
this.daemon?.start();
|
|
this.started = performance.now();
|
|
}
|
|
else {
|
|
console.debug(`Stopping ${this.id}...`);
|
|
await this.stopSession();
|
|
await this.daemon?.term();
|
|
}
|
|
}
|
|
async stopSession() {
|
|
if (!this.session)
|
|
return;
|
|
this.session.abort.abort();
|
|
await this.session.done;
|
|
this.session = null;
|
|
this.resetReady();
|
|
}
|
|
resetReady() {
|
|
this.resolvedReady = false;
|
|
this.readyPromise = new Promise((resolve) => (this.resolveReady = () => {
|
|
resolve();
|
|
this.resolvedReady = true;
|
|
}));
|
|
}
|
|
startSession() {
|
|
this.session?.abort.abort();
|
|
const abort = new AbortController();
|
|
this.daemon?.onExit((success) => {
|
|
if (abort.signal.aborted)
|
|
return;
|
|
if (success && this.ready === 'EXIT_SUCCESS') {
|
|
this.setHealth({ result: 'success', message: null });
|
|
}
|
|
else if (!success) {
|
|
this.setHealth({
|
|
result: 'failure',
|
|
message: `${this.id} daemon crashed`,
|
|
});
|
|
}
|
|
else if (!this.daemon?.isOneshot()) {
|
|
this.setHealth({
|
|
result: 'failure',
|
|
message: `${this.id} daemon exited`,
|
|
});
|
|
}
|
|
});
|
|
const done = this.ready === 'EXIT_SUCCESS'
|
|
? Promise.resolve()
|
|
: this.runHealthCheckLoop(abort.signal);
|
|
this.session = { abort, done };
|
|
}
|
|
async runHealthCheckLoop(signal) {
|
|
if (this.ready === 'EXIT_SUCCESS')
|
|
return;
|
|
const trigger = (this.ready.trigger ?? defaultTrigger_1.defaultTrigger)(() => ({
|
|
lastResult: this._health.result,
|
|
}));
|
|
const aborted = new Promise((resolve) => signal.addEventListener('abort', () => resolve({ done: true }), {
|
|
once: true,
|
|
}));
|
|
try {
|
|
for (let res = await Promise.race([aborted, trigger.next()]); !res.done; res = await Promise.race([aborted, trigger.next()])) {
|
|
const response = await Promise.resolve(this.ready.fn()).catch((err) => {
|
|
return {
|
|
result: 'failure',
|
|
message: 'message' in err ? err.message : String(err),
|
|
};
|
|
});
|
|
if (signal.aborted)
|
|
break;
|
|
await this.setHealth(response);
|
|
}
|
|
}
|
|
catch (err) {
|
|
if (!signal.aborted) {
|
|
console.error(`Daemon ${this.id} health check failed: ${err}`);
|
|
}
|
|
}
|
|
}
|
|
onReady() {
|
|
return this.readyPromise;
|
|
}
|
|
get isReady() {
|
|
return this.resolvedReady;
|
|
}
|
|
async setHealth(health) {
|
|
const changed = this._health.result !== health.result;
|
|
this._health = health;
|
|
if (this.resolveReady && health.result === 'success') {
|
|
this.resolveReady();
|
|
}
|
|
if (changed)
|
|
this.healthWatchers.forEach((watcher) => watcher());
|
|
if (this.ready === 'EXIT_SUCCESS')
|
|
return;
|
|
const display = this.ready.display;
|
|
if (!display) {
|
|
return;
|
|
}
|
|
let result = health.result;
|
|
if (result === 'failure' &&
|
|
this.started &&
|
|
performance.now() - this.started <= (this.ready.gracePeriod ?? 10_000))
|
|
result = 'starting';
|
|
if (result === 'failure') {
|
|
console.error(`Health Check ${this.id} failed:`, health.message);
|
|
}
|
|
await this.effects.setHealth({
|
|
...health,
|
|
id: this.id,
|
|
name: display,
|
|
result,
|
|
});
|
|
}
|
|
async updateStatus() {
|
|
const healths = this.dependencies.map((d) => ({
|
|
health: d.running && d._health,
|
|
id: d.id,
|
|
display: typeof d.ready === 'object' ? d.ready.display : null,
|
|
}));
|
|
const waitingOn = healths.filter((h) => !h.health || h.health.result !== 'success');
|
|
if (waitingOn.length)
|
|
console.debug(`daemon ${this.id} waiting on ${waitingOn.map((w) => w.id)}`);
|
|
if (waitingOn.length) {
|
|
const waitingOnNames = waitingOn.flatMap((w) => w.display ? [w.display] : []);
|
|
const message = waitingOnNames.length ? waitingOnNames.join(', ') : null;
|
|
await this.setHealth({ result: 'waiting', message });
|
|
}
|
|
else {
|
|
await this.setHealth({ result: 'starting', message: null });
|
|
}
|
|
await this.changeRunning(!waitingOn.length);
|
|
}
|
|
}
|
|
exports.HealthDaemon = HealthDaemon;
|
|
//# sourceMappingURL=HealthDaemon.js.map
|