/** * @author Martin Karkowski * @email m.karkowski@zema.de * @create date 2022-01-03 21:21:45 * @modify date 2022-01-04 13:56:37 * @desc [description] */ import { ILogger } from "js-logger"; import { avgOfArray } from "../../helpers/arrayMethods"; import { sleep } from "../../helpers/async"; import { generateId } from "../../helpers/idMethods"; import { MapBasedMergeData } from "../../helpers/mergedData"; import { RUNNINGINNODE } from "../../helpers/runtimeMethods"; import { defineNopeLogger } from "../../logger/getLogger"; import { ENopeDispatcherStatus, ICommunicationBridge, IMapBasedMergeData, INopeConnectivityManager, INopeINopeConnectivityOptions, INopeINopeConnectivityTimeOptions, INopeObservable, INopeStatusInfo, } from "../../types/nope"; // Chached Moduls, which will be loaded in nodejs let os = null; let cpus = null; /** * A Modul to manage the status of other statusmanagers. * Dispatcher should have a status manager, to ensure, the * system is online etc. * * @author M.Karkowski * @export * @class NopeConnectivityManager * @implements {INopeConnectivityManager} */ export class NopeConnectivityManager implements INopeConnectivityManager { protected _logger: ILogger; protected _deltaTime = 0; protected _isMaster: string = null; /** * The used Communication interface * * @type {ICommunicationBridge} * @memberof NopeConnectivityManager */ protected readonly _communicator: ICommunicationBridge; /** * A Map holding the current Status of external dispatchers. * Key = Dispatcher-ID * Value = Last Known status of the dispatcher * * @protected * @type {Map} * @memberof NopeConnectivityManager */ protected _externalDispatchers: Map; /** * Timeout settings. This will define the Timers etc. * * @author M.Karkowski * @protected * @type {INopeINopeConnectivityTimeOptions} * @memberof NopeConnectivityManager */ protected _timeouts: INopeINopeConnectivityTimeOptions; protected _checkInterval: any = null; // Timer to check the status protected _sendInterval: any = null; // Timer to send the status protected _cpuInterval: any = null; // Timer to update the CPU-Load /** * Internal var to hold the cpu-load * * @author M.Karkowski * @protected * @memberof NopeConnectivityManager */ protected _cpuLoad = -1; public readonly ready: INopeObservable; public readonly dispatchers: IMapBasedMergeData< INopeStatusInfo, string, INopeStatusInfo >; /** * Generates the current Status Message of the Dispatcher. * * @author M.Karkowski * @protected * @return {*} {IDispatcherInfo} The current status of our dispatcher. * @memberof NopeConnectivityManager */ public get info(): INopeStatusInfo { if (RUNNINGINNODE) { // If we are running our programm in node, // we will load the corresponding libs, // to calc the cpu load etc. if (os === null) { // eslint-disable-next-line os = require("os"); } if (cpus === null) { // eslint-disable-next-line cpus = os.cpus(); } // Now lets return our status message return { id: this.id, env: "javascript", version: "1.0.0", host: { cores: cpus.length, cpu: { model: `${cpus[0].model}`.slice( 0, (cpus[0].model as string).indexOf("@") - 1 ), speed: avgOfArray(cpus, "speed"), usage: this._cpuLoad, }, os: os.platform(), ram: { // Return the used Memory usedPerc: 1 - os.freemem() / os.totalmem(), // The Values are given in Byte but we want MByte free: Math.round(os.freemem() / 1048576), total: Math.round(os.totalmem() / 1048576), }, name: os.hostname(), }, pid: process.pid, timestamp: Date.now() + this._deltaTime, status: ENopeDispatcherStatus.HEALTHY, }; } return { env: "javascript", version: "1.0.0", host: { cores: -1, cpu: { model: "unkown", speed: -1, usage: -1, }, name: navigator.appCodeName + " " + navigator.appName, os: navigator.platform, ram: { free: -1, usedPerc: -1, total: -1, }, }, id: this.id, pid: this.id, timestamp: Date.now() + this._deltaTime, status: ENopeDispatcherStatus.HEALTHY, }; } /** * Creates an instance of nopeDispatcher. * @param {nopeRpcDispatcherOptions} options The Options, used by the Dispatcher. * @param {() => INopeObservable} _generateObservable A Helper, to generate Observables. * @memberof NopeConnectivityManager */ constructor( public options: INopeINopeConnectivityOptions, protected _generateObservable: () => INopeObservable, public readonly id: string = null ) { this._communicator = options.communicator; if (id === null) { this.id = generateId(); } this._logger = defineNopeLogger(options.logger, "core.status.manager"); // Update the Timesettings this.setTimings(options.timeouts || {}); // Flag to show if the system is ready or not. this.ready = this._generateObservable(); this.ready.setContent(false); // Observable containing all Dispatcher Informations. this._externalDispatchers = new Map(); this.dispatchers = new MapBasedMergeData(this._externalDispatchers); if (this._logger) { this._logger.info("core.status.manager", this.id, "is online"); } this.reset(); const _this = this; this._init().catch((error) => { if (_this._logger) { _this._logger.error("Failed to intialize status manager"); _this._logger.error(error); // Now we should exit the program (if we are running in nodejs) if (RUNNINGINNODE) { process.exit(1); } } }); } /** * Internal Function, used to initialize the Dispatcher. * It subscribes to the "Messages" of the communicator. * * @protected * @memberof NopeConnectivityManager */ protected async _init(): Promise { const _this = this; this.ready.setContent(false); // Wait until the Element is connected. await this._communicator.connected.waitFor((value) => value); await this._communicator.onStatusChanged((info) => { _this._externalDispatchers.set(info.id, info); _this.dispatchers.update(); }); await this._communicator.onBonjour((info) => { _this._externalDispatchers.set(info.id, info); _this.dispatchers.update(); if (_this.id !== info.id) { _this._sendStatus(); if (_this._logger?.enabledFor(_this._logger.DEBUG)) { // If there is a Logger: _this._logger.debug( 'Remote Dispatcher "' + info.id + '" went online' ); } } }); await this._communicator.onAurevoir((dispatcher: string) => { // Remove the Dispatcher. _this._externalDispatchers.delete(dispatcher); _this.dispatchers.update(); }); if (this._logger) { this._logger.info(this.id, "initialized"); } // We sleep 500 ms // await sleep(500); await this.emitBonjour(); // await sleep(500); this.ready.setContent(true); } /** * Function, which will be called to update the * Status to the Dispatchers * * @author M.Karkowski * @protected * @memberof NopeConnectivityManager */ protected _checkDispatcherHealth(): void { const currentTime = Date.now(); let changes = false; for (const status of this._externalDispatchers.values()) { // determine the Difference const diff = currentTime - status.timestamp; // Based on the Difference Determine the Status if (diff > this._timeouts.remove) { // remove the Dispatcher. But be quite. // Perhaps more dispatchers will be removed this._removeDispatcher(status.id, true); changes = true; } else if ( diff > this._timeouts.dead && status.status !== ENopeDispatcherStatus.DEAD ) { status.status = ENopeDispatcherStatus.DEAD; changes = true; } else if ( diff > this._timeouts.warn && diff <= this._timeouts.dead && status.status !== ENopeDispatcherStatus.WARNING ) { status.status = ENopeDispatcherStatus.WARNING; changes = true; } else if ( diff > this._timeouts.slow && diff <= this._timeouts.warn && status.status !== ENopeDispatcherStatus.SLOW ) { status.status = ENopeDispatcherStatus.SLOW; changes = true; } else if ( diff <= this._timeouts.slow && status.status !== ENopeDispatcherStatus.HEALTHY ) { status.status = ENopeDispatcherStatus.HEALTHY; changes = true; } } if (changes) { // Update the External Dispatchers this.dispatchers.update(); } } /** * Removes a Dispatcher. * * @author M.Karkowski * @protected * @param {string} dispatcher * @param {boolean} [quite=false] * @memberof NopeConnectivityManager */ protected _removeDispatcher(dispatcher: string, quite = false): void { // Delete the Generators of the Instances. const dispatcherInfo = this._externalDispatchers.get(dispatcher); const deleted = this._externalDispatchers.delete(dispatcher); if (!quite) { this.dispatchers.update(); } if (deleted && this._logger?.enabledFor(this._logger?.WARN)) { // If there is a Logger: this._logger.warn( "a dispatcher on", dispatcherInfo?.host.name || "unkown", "went offline. ID of the Dispatcher: ", dispatcher ); } } /** * Helper to send the current status to other statusmanagers. */ protected _sendStatus(): void { this._communicator.emitStatusChanged(this.info); } /** * Helper function, which will synchronize the Timestamp. * Timestamp must be provided in UTC (https://www.timeanddate.de/stadt/info/zeitzone/utc) * * @author M.Karkowski * @param {number} timestamp The UTC-Timestamp * @param {number} [delay=0] The Delay, since the Timestamp has been generated * @memberof NopeConnectivityManager */ public syncTime(timestamp: number, delay = 0) { const _internalTimestamp = Date.now(); this._deltaTime = _internalTimestamp - timestamp - delay; } public getStatus(id: string) { return this._externalDispatchers.get(id); } /** * Helper Function to manually emit a Bonjour! * * @return {*} {Promise} * @memberof NopeConnectivityManager */ public async emitBonjour(): Promise { // Emit the Bonjour Message. this._communicator.emitBonjour(this.info); } /** * Function to reset the Dispatcher. * * @memberof NopeConnectivityManager */ public reset(): void { this._externalDispatchers.clear(); this.dispatchers.update(this._externalDispatchers); } /** * Adapts the Timing Options and resets the internally used * Timers etc. * * @author M.Karkowski * @param {Partial} options * @memberof NopeConnectivityManager */ public setTimings(options: Partial): void { // Clear all Intervals etc. this.dispose(true); const _this = this; this._timeouts = { sendAliveInterval: 500, checkInterval: 250, slow: 1000, warn: 2000, dead: 5000, remove: 10000, }; // Define the Timeouts. if (options) { this._timeouts = Object.assign(this._timeouts, options); } if (RUNNINGINNODE) { // eslint-disable-next-line const os = require("os"); const getLoad = () => { const cpus = os.cpus(); let totalTime = 0, idleTime = 0; // Determine the current load: for (const cpu of cpus) { for (const name in cpu.times) { totalTime += cpu.times[name]; } idleTime += cpu.times.idle; } return { totalTime, idleTime, }; }; // Initally store the load let oldTimes = getLoad(); this._cpuInterval = setInterval(() => { // Get the current CPU Times. const currentTimes = getLoad(); // Determine the difference between the old Times an the current Times. _this._cpuLoad = 1 - (currentTimes.idleTime - oldTimes.idleTime) / (currentTimes.totalTime - oldTimes.totalTime); // Store the current CPU-Times oldTimes = currentTimes; }, this._timeouts.sendAliveInterval); } // Setup Test Intervals: if (this._timeouts.checkInterval > 0) { // Define a Checker, which will test the status // of the external Dispatchers. this._checkInterval = setInterval( () => _this._checkDispatcherHealth(), this._timeouts.checkInterval ); } if (this._timeouts.sendAliveInterval > 0) { // Define a Timer, which will emit Status updates with // the disered delay. this._sendInterval = setInterval( () => _this._sendStatus(), this._timeouts.sendAliveInterval ); } } /** * Will dispose the Dispatcher. Must be called on exit for a clean exit. Otherwise it is defined as dirty exits */ public async dispose(quite = false): Promise { if (this._sendInterval) { clearInterval(this._sendInterval); } if (this._checkInterval) { clearInterval(this._checkInterval); } if (this._cpuInterval) { clearInterval(this._cpuInterval); } // Emits the aurevoir Message. if (!quite) { this._communicator.emitAurevoir(this.id); } } }