From b99c1edc6c75d2f972708b04bd49ad5ebf0fd775 Mon Sep 17 00:00:00 2001 From: Martin Karkowski Date: Thu, 20 Jan 2022 12:59:32 +0100 Subject: [PATCH] Adding comments and allowing base-services to be skipped --- lib/dispatcher/ConnectivityManager/index.ts | 219 +++++++++++++++++++- lib/dispatcher/getDispatcher.ts | 37 +++- lib/loader/generateNopeBasicPackage.ts | 6 +- wiki/13-ConnectivityManager.ipynb | 190 +++++++++++++---- 4 files changed, 394 insertions(+), 58 deletions(-) diff --git a/lib/dispatcher/ConnectivityManager/index.ts b/lib/dispatcher/ConnectivityManager/index.ts index 7341662..e9deee5 100644 --- a/lib/dispatcher/ConnectivityManager/index.ts +++ b/lib/dispatcher/ConnectivityManager/index.ts @@ -2,10 +2,221 @@ * @module connectivityManager * @author Martin Karkowski * @email m.karkowski@zema.de - * - * # NopeConnectivityManager - * - * + * + * # NoPE - Connectivity Manager + * + * The NoPE-Dispatcher uses one `ConnectivityManager`. The manager observes the connection and remotly connected dispatchers (and their `ConnectivityManager`). The Manager detects newly connected dispatchers and disconnected dispatchers. Additionally, it sends a StatusMessage (in the form of `INopeStatusInfo`). This status message is interpreted as heartbeat. The `ConnectivityManager` checks those heartbeats with a defined interval. If a specific amount of time is ellapsed, the remote dispatcher is marked as `slow` -> `warning` -> `dead`. After an additional delay in the state `dead` the dispatcher is altough removed. + * + * ## Master + * + * Defaultly a `ConnectivityManager` is elected as `master`. The master is defined as the `ConnectivityManager` with the highest `upTime`. + * + * > Alternativly a master can be forced. + * + * ## Synchronizing time + * + * Because we asume, that **NoPE** is running on different computing nodes, we have to be able to synchronize the time between those elements. Therefore the `ConnectivityManager` is able to sync the time (by providing a `timestamp` and an additional `delay` that was needed to get to the call (for instance `ping / 2`)) + * + * + * + * ```javascript + * // First lets install nope using npm + * const nope = require("../dist-nodejs/index.nodejs") + * + * // Create a communicator: + * // We will use the event layer (which just runs internally) + * const communicator = nope.getLayer("event"); + * + * // Lets create our dispatcher + * + * // 1. Dispatcher simulates our local system + * const localDispatcher = nope.dispatcher.getDispatcher({ + * communicator, + * id: "local" + * }, { + * singleton: false, + * useBaseServices: false + * }); + * ``` + * + * > For Jupyter we need an extra async wrapper to wait for initalizing the dispatcher: + * + * see here for the details in Jupyter: https://n-riesco.github.io/ijavascript/doc/async.ipynb.html + * + * + * ```javascript + * $$.async(); + * // Lets wait for our element to be ready. + * localDispatcher.ready.waitFor().then($$.done); + * ``` + * + * Now we want to listen to newly connected dispatchers. For this purpose, we create an observer, which will listen to changes. + * + * + * ```javascript + * // Subscribe to changes + * const observer = localDispatcher.connectivityManager.dispatchers.onChange.subscribe(data => { + * // Log the changes + * console.log((new Date()).toISOString(),"onChange - listener"); + * console.log("\tadded =", data.added); + * console.log("\tremoved =", data.removed); + * }); + * ``` + * + * Additionally we want to show the currently connected dispatchers. In this data the own dispatcher will **allways** be included: + * + * + * ```javascript + * // Show our connected Dispatchers + * let connectedDispatchers = localDispatcher.connectivityManager.dispatchers.data.getContent(); + * let localDispatcherIncluded = connectedDispatchers.includes(localDispatcher.id); + * + * // Now lets log our results. + * console.log("connectedDispatchers =", connectedDispatchers); + * console.log("localDispatcherIncluded =", localDispatcherIncluded); + * ``` + * + * >``` + * > connectedDispatchers = [ 'local' ] + * > localDispatcherIncluded = true + * >``` + * + * Now that we have implemented our listeners and have seen the connected dispatchers (which is only the `"local"`-dispatchre), We will add an additional dispatcher. This should result in calling our `onChange`-listener. Additionally, we wait until our `remoteDispatcher` is initalized + * + * + * ```javascript + * // 2. Dispatcher simulates our remote system + * const remoteDispatcher = nope.dispatcher.getDispatcher({ + * communicator, + * id: "remote" + * }, { + * singleton: false, + * useBaseServices: false + * }); + * + * ``` + * + * >``` + * > 2022-01-20T11:39:55.766Z onChange - listener + * > added = [ 'remote' ] + * > removed = [] + * >``` + * + * Now we want to see, which system is the current master. This should be our `local`. + * + * + * ```javascript + * // We expect to be the master, because the localDispatcher has been created first. + * console.log("master =", localDispatcher.connectivityManager.master.id); + * ``` + * + * > `master = local` + * + * + * We can now force the remote dispatcher to be our master, by setting the master. (For this purpose we can later use a base service ==> then we just have to call the service) + * + * + * ```javascript + * $$.async(); + * + * remoteDispatcher.connectivityManager.isMaster = true; + * localDispatcher.connectivityManager.isMaster = false; + * + * // Our messaging is async ==> we wait an amount of time + * setTimeout(() => $$.done(),1000); + * ``` + * + * + * ```javascript + * // We expect the master to be the remote. + * console.log("master =", localDispatcher.connectivityManager.master.id); + * console.log("master-info =", localDispatcher.connectivityManager.master); + * ``` + * + * >``` + * > master = remote + * > master-info = { + * > id: 'remote', + * > env: 'javascript', + * > version: '1.0.0', + * > isMaster: true, + * > host: { + * > cores: 8, + * > cpu: { + * > model: 'Intel(R) Core(TM) i7-8565U CPU', + * > speed: 1992, + * > usage: 0.0038778477944740875 + * > }, + * > os: 'win32', + * > ram: { usedPerc: 0.362681220626356, free: 20676, total: 32442 }, + * > name: 'nz-078' + * > }, + * > pid: 18068, + * > timestamp: 1642678798813, + * > upTime: 3049, + * > status: 0 + * > } + * >``` + * + * + * Now lets see what happens if we adapt the heartbeat intervall of our *local* instance. We want to receive every 50 ms a heartbeat: + * + * + * ```javascript + * $$.async() + * + * const renderStatus = () => { + * console.log((new Date()).toISOString(),"master-info =", localDispatcher.connectivityManager.master.status) + * } + * + * setTimeout(renderStatus, 50); + * setTimeout(renderStatus, 750); + * setTimeout(renderStatus, 1500); + * setTimeout(renderStatus, 2500); + * + * + * localDispatcher.connectivityManager.setTimings({ + * // our system will send every 50 ms an heartbeat. + * sendAliveInterval: 250, + * // we will check that after + * checkInterval: 125, + * // will mark dispatchers as slow after not receiving heartbeats for 50ms + * slow: 500, + * // we will mark dispatchers with a warning flag after 50 ms + * warn: 1000, + * // we mark it as dead after 0.5 s + * dead: 2000, + * // We will remove the dispatcher after 1 s + * remove: 3000, + * }); + * + * remoteDispatcher.connectivityManager.setTimings({ + * // our system will send every 50 ms an heartbeat. + * sendAliveInterval: 5000, + * }); + * + * + * + * // We reset the timeouts. + * setTimeout(() => localDispatcher.connectivityManager.setTimings({}), 3000); + * setTimeout(() => remoteDispatcher.connectivityManager.setTimings({}), 3000); + * setTimeout(() => $$.done(), 5000); + * + * ``` + * + * >``` + * > 2022-01-20T11:40:01.089Z master-info = 0 + * > 2022-01-20T11:40:01.789Z master-info = 1 + * > 2022-01-20T11:40:02.536Z master-info = 2 + * > 2022-01-20T11:40:03.543Z master-info = 3 + * > 2022-01-20T11:40:03.977Z onChange - listener + * > added = [] + * > removed = [ 'remote' ] + * > 2022-01-20T11:40:04.547Z onChange - listener + * > added = [ 'remote' ] + * > removed = [] + * >``` + * */ export { diff --git a/lib/dispatcher/getDispatcher.ts b/lib/dispatcher/getDispatcher.ts index 8180c8b..7084c79 100644 --- a/lib/dispatcher/getDispatcher.ts +++ b/lib/dispatcher/getDispatcher.ts @@ -19,24 +19,39 @@ import { NopeDispatcher } from "./nopeDispatcher"; /** * Function to extract a Singleton Dispatcher - * @param options The provided options for the Dispatcher + * @param dispatcherOptions The provided options for the Dispatcher * * ```typescript * * ``` */ export function getDispatcher( - options: INopeDispatcherOptions, - constructorClass: IDispatcherConstructor = null, - singleton = true + dispatcherOptions: INopeDispatcherOptions, + options: { + constructorClass?: IDispatcherConstructor; + singleton?: boolean; + useBaseServices?: boolean; + } = {} ): INopeDispatcher { - if (constructorClass === null || constructorClass === undefined) { - constructorClass = NopeDispatcher; + if ( + options.constructorClass === null || + options.constructorClass === undefined + ) { + options.constructorClass = NopeDispatcher; } + options = Object.assign( + { + constructorClass: null, + singleton: true, + useBaseServices: true, + }, + options + ); + const create = () => { - const dispatcher = new constructorClass( - options, + const dispatcher = new options.constructorClass( + dispatcherOptions, () => new NopeObservable() ); @@ -52,13 +67,15 @@ export function getDispatcher( } ); - addAllBaseServices(dispatcher); + if (options.useBaseServices) { + addAllBaseServices(dispatcher); + } // Return the Dispathcer return dispatcher as INopeDispatcher; }; - if (singleton) { + if (options.singleton) { // Create a singaleton if required. // use the container to receive the // singleton object diff --git a/lib/loader/generateNopeBasicPackage.ts b/lib/loader/generateNopeBasicPackage.ts index a06530e..96c986c 100644 --- a/lib/loader/generateNopeBasicPackage.ts +++ b/lib/loader/generateNopeBasicPackage.ts @@ -17,7 +17,7 @@ import { DISPATCHER_INSTANCE, DISPATCHER_OPTIONS, OBSERVABLE_FACTORY, - OBSERVABLE_INSTANCE, + OBSERVABLE_INSTANCE } from "../symbols/identifiers"; import { INopeDispatcherOptions } from "../types/nope/nopeDispatcher.interface"; import { IPackageDescription } from "../types/nope/nopePackage.interface"; @@ -58,7 +58,9 @@ export function generateNopeBasicPackage( selector: TYPES.dispatcher, // We want to provide in this Situation allways the same dispatcher. // type: !singleton ? InjectableNopeDispatcher : getDispatcher(options, null, singleton), - type: getDispatcher(options, null, singleton), + type: getDispatcher(options, { + singleton + }), options: { // Shouldn't be required: // scope: singleton ? "inSingletonScope" : undefined, diff --git a/wiki/13-ConnectivityManager.ipynb b/wiki/13-ConnectivityManager.ipynb index df51bc8..a05b758 100644 --- a/wiki/13-ConnectivityManager.ipynb +++ b/wiki/13-ConnectivityManager.ipynb @@ -21,7 +21,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -38,7 +38,10 @@ "const localDispatcher = nope.dispatcher.getDispatcher({\n", " communicator,\n", " id: \"local\"\n", - "}, null, false);" + "}, {\n", + " singleton: false,\n", + " useBaseServices: false\n", + "});" ] }, { @@ -52,9 +55,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "true" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "$$.async();\n", "// Lets wait for our element to be ready.\n", @@ -70,17 +84,16 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "// Subscribe to changes\n", "const observer = localDispatcher.connectivityManager.dispatchers.onChange.subscribe(data => {\n", " // Log the changes\n", - " console.log(\"onChange - listener\");\n", - " console.log(\"-------------------\");\n", - " console.log(\"added =\", data.added);\n", - " console.log(\"removed =\", data.removed);\n", + " console.log((new Date()).toISOString(),\"onChange - listener\");\n", + " console.log(\"\\tadded =\", data.added);\n", + " console.log(\"\\tremoved =\", data.removed);\n", "});" ] }, @@ -93,9 +106,18 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "connectedDispatchers = [ 'local' ]\n", + "localDispatcherIncluded = true\n" + ] + } + ], "source": [ "// Show our connected Dispatchers\n", "let connectedDispatchers = localDispatcher.connectivityManager.dispatchers.data.getContent();\n", @@ -115,15 +137,28 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2022-01-20T11:39:55.766Z onChange - listener\n", + "\tadded = [ 'remote' ]\n", + "\tremoved = []\n" + ] + } + ], "source": [ "// 2. Dispatcher simulates our remote system\n", "const remoteDispatcher = nope.dispatcher.getDispatcher({\n", " communicator,\n", " id: \"remote\"\n", - "}, null, false);\n" + "}, {\n", + " singleton: false,\n", + " useBaseServices: false\n", + "});\n" ] }, { @@ -135,9 +170,17 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "master = local\n" + ] + } + ], "source": [ "// We expect to be the master, because the localDispatcher has been created first.\n", "console.log(\"master =\", localDispatcher.connectivityManager.master.id);" @@ -152,7 +195,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -162,14 +205,43 @@ "localDispatcher.connectivityManager.isMaster = false;\n", "\n", "// Our messaging is async ==> we wait an amount of time\n", - "setTimeout($$.done,1000)" + "setTimeout(() => $$.done(),1000);" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "master = remote\n", + "master-info = {\n", + " id: 'remote',\n", + " env: 'javascript',\n", + " version: '1.0.0',\n", + " isMaster: true,\n", + " host: {\n", + " cores: 8,\n", + " cpu: {\n", + " model: 'Intel(R) Core(TM) i7-8565U CPU',\n", + " speed: 1992,\n", + " usage: 0.0038778477944740875\n", + " },\n", + " os: 'win32',\n", + " ram: { usedPerc: 0.362681220626356, free: 20676, total: 32442 },\n", + " name: 'nz-078'\n", + " },\n", + " pid: 18068,\n", + " timestamp: 1642678798813,\n", + " upTime: 3049,\n", + " status: 0\n", + "}\n" + ] + } + ], "source": [ "// We expect the master to be the remote.\n", "console.log(\"master =\", localDispatcher.connectivityManager.master.id);\n", @@ -185,39 +257,73 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "2022-01-20T11:40:01.089Z master-info = 0\n", + "2022-01-20T11:40:01.789Z master-info = 1\n", + "2022-01-20T11:40:02.536Z master-info = 2\n", + "2022-01-20T11:40:03.543Z master-info = 3\n", + "2022-01-20T11:40:03.977Z onChange - listener\n", + "\tadded = []\n", + "\tremoved = [ 'remote' ]\n", + "2022-01-20T11:40:04.547Z onChange - listener\n", + "\tadded = [ 'remote' ]\n", + "\tremoved = []\n" + ] + } + ], "source": [ "$$.async()\n", "\n", - "localDispatcher.connectivityManager.setTimings({\n", - " // our system will send every 50 ms an heartbeat.\n", - " sendAliveInterval: 500,\n", - " // we will check that after\n", - " checkInterval: 25,\n", - " // will mark dispatchers as slow after not receiving heartbeats for 50ms\n", - " slow: 50,\n", - " // we will mark dispatchers with a warning flag after 50 ms\n", - " warn: 100,\n", - " // we mark it as dead after 0.5 s\n", - " dead: 500,\n", - " // We will remove the dispatcher after 1 s\n", - " remove: 1000,\n", - "});\n", - "\n", "const renderStatus = () => {\n", - " console.log(\"master-info =\", localDispatcher.connectivityManager.master.status)\n", + " console.log((new Date()).toISOString(),\"master-info =\", localDispatcher.connectivityManager.master.status)\n", "}\n", "\n", - "setTimeout(renderStatus, 75);\n", - "setTimeout(renderStatus, 250);\n", + "setTimeout(renderStatus, 50);\n", "setTimeout(renderStatus, 750);\n", + "setTimeout(renderStatus, 1500);\n", + "setTimeout(renderStatus, 2500);\n", + "\n", + "\n", + "localDispatcher.connectivityManager.setTimings({\n", + " // our system will send every 50 ms an heartbeat.\n", + " sendAliveInterval: 250,\n", + " // we will check that after\n", + " checkInterval: 125,\n", + " // will mark dispatchers as slow after not receiving heartbeats for 50ms\n", + " slow: 500,\n", + " // we will mark dispatchers with a warning flag after 50 ms\n", + " warn: 1000,\n", + " // we mark it as dead after 0.5 s\n", + " dead: 2000,\n", + " // We will remove the dispatcher after 1 s\n", + " remove: 3000,\n", + "});\n", + "\n", + "remoteDispatcher.connectivityManager.setTimings({\n", + " // our system will send every 50 ms an heartbeat.\n", + " sendAliveInterval: 5000,\n", + "});\n", + "\n", + "\n", "\n", "// We reset the timeouts.\n", - "setTimeout(() => localDispatcher.connectivityManager.setTimings({}), 1200);\n", - "setTimeout(() => $$.done(), 2000);\n" + "setTimeout(() => localDispatcher.connectivityManager.setTimings({}), 3000);\n", + "setTimeout(() => remoteDispatcher.connectivityManager.setTimings({}), 3000);\n", + "setTimeout(() => $$.done(), 5000);\n" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": {