UNPKG

@atomist/automation-client

Version:
248 lines • 11.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const cluster = require("cluster"); const stringify = require("json-stringify-safe"); const namespace = require("../../util/cls"); const Deferred_1 = require("../../util/Deferred"); const health_1 = require("../../util/health"); const logger_1 = require("../../util/logger"); const shutdown_1 = require("../../util/shutdown"); const AbstractRequestProcessor_1 = require("../AbstractRequestProcessor"); const RequestProcessor_1 = require("../RequestProcessor"); const WebSocketMessageClient_1 = require("../websocket/WebSocketMessageClient"); const messages_1 = require("./messages"); /** * A RequestProcessor that delegates to Node.JS Cluster workers to do the actual * command and event processing. * @see ClusterWorkerRequestProcessor */ class ClusterMasterRequestProcessor extends AbstractRequestProcessor_1.AbstractRequestProcessor { constructor(automations, configuration, listeners = [], numWorkers = require("os").cpus().length) { super(automations, configuration, listeners); this.automations = automations; this.configuration = configuration; this.listeners = listeners; this.numWorkers = numWorkers; this.commands = new Map(); this.events = new Map(); this.shutdownInitiated = false; health_1.registerHealthIndicator(() => { if (this.webSocket && this.registration) { return { status: health_1.HealthStatus.Up, detail: "WebSocket connection established" }; } else { return { status: health_1.HealthStatus.Down, detail: "WebSocket disconnected" }; } }); shutdown_1.registerShutdownHook(() => { this.shutdownInitiated = true; return Promise.resolve(0); }, Number.MIN_VALUE); } onRegistration(registration) { logger_1.logger.info("Registration successful: %s", stringify(registration)); this.configuration.ws.session = registration; this.registration = registration; messages_1.broadcast({ type: "registration", registration: this.registration, context: null, }); } onConnect(ws) { logger_1.logger.info("WebSocket connection established. Listening for incoming messages"); this.webSocket = ws; this.listeners.forEach(l => l.registrationSuccessful(this)); } onDisconnect() { this.webSocket = null; this.registration = null; } run() { const ws = () => this.webSocket; const listeners = this.listeners; const commands = this.commands; const events = this.events; const clearNamespace = this.clearNamespace; function attachEvents(worker, deferred) { worker.on("message", message => { const msg = message; // Wait for online message to come in if (msg.type === "online") { deferred.resolve(); return; } const ses = namespace.init(); ses.run(() => { namespace.set(msg.context); logger_1.logger.debug("Received '%s' message from worker '%s': %j", msg.type, worker.id, msg.context); const invocationId = namespace.get().invocationId; const ctx = hydrateContext(msg); if (msg.type === "message") { let messageClient; if (commands.has(invocationId)) { messageClient = commands.get(invocationId).context.messageClient; } else if (events.has(invocationId)) { messageClient = events.get(invocationId).context.messageClient; } else { logger_1.logger.error("Can't handle message from worker due to missing messageClient"); clearNamespace(); return; } if (msg.data.destinations && msg.data.destinations.length > 0) { messageClient.send(msg.data.message, msg.data.destinations, msg.data.options) .then(clearNamespace, clearNamespace); } else { messageClient.respond(msg.data.message, msg.data.options) .then(clearNamespace, clearNamespace); } } else if (msg.type === "status") { WebSocketMessageClient_1.sendMessage(msg.data, ws()); } else if (msg.type === "command_success") { listeners.map(l => () => l.commandSuccessful(msg.event, ctx, msg.data)) .reduce((p, f) => p.then(f), Promise.resolve()) .then(() => { if (commands.has(invocationId)) { commands.get(invocationId).result.resolve(msg.data); commands.delete(invocationId); } clearNamespace(); }) .catch(clearNamespace); } else if (msg.type === "command_failure") { listeners.map(l => () => l.commandFailed(msg.event, ctx, msg.data)) .reduce((p, f) => p.then(f), Promise.resolve()) .then(() => { if (commands.has(invocationId)) { commands.get(invocationId).result.resolve(msg.data); commands.delete(invocationId); } clearNamespace(); }) .catch(clearNamespace); } else if (msg.type === "event_success") { listeners.map(l => () => l.eventSuccessful(msg.event, ctx, msg.data)) .reduce((p, f) => p.then(f), Promise.resolve()) .then(() => { if (events.has(invocationId)) { events.get(invocationId).result.resolve(msg.data); events.delete(invocationId); } clearNamespace(); }) .catch(clearNamespace); } else if (msg.type === "event_failure") { listeners.map(l => () => l.eventFailed(msg.event, ctx, msg.data)) .reduce((p, f) => p.then(f), Promise.resolve()) .then(() => { if (events.has(invocationId)) { events.get(invocationId).result.resolve(msg.data); events.delete(invocationId); } clearNamespace(); }) .catch(clearNamespace); } }); }); } const promises = []; for (let i = 0; i < this.numWorkers; i++) { const worker = cluster.fork(); const deferred = new Deferred_1.Deferred(); promises.push(deferred.promise); attachEvents(worker, deferred); } cluster.on("disconnect", worker => { logger_1.logger.warn(`Worker '${worker.id}' disconnected`); }); cluster.on("online", worker => { logger_1.logger.debug(`Worker '${worker.id}' connected`); }); cluster.on("exit", (worker, code, signal) => { if (code !== 0 && !this.shutdownInitiated) { logger_1.logger.warn(`Worker '${worker.id}' exited with '${code}' '${signal}'. Restarting ...`); attachEvents(cluster.fork(), new Deferred_1.Deferred()); } }); return Promise.all(promises); } invokeCommand(ci, ctx, command, callback) { const message = { type: "command", registration: this.registration, context: ctx.context, data: command, }; const dispatched = new Dispatched(new Deferred_1.Deferred(), ctx); this.commands.set(ctx.context.invocationId, dispatched); const worker = this.assignWorker(); logger_1.logger.debug("Incoming command handler request '%s' dispatching to worker '%s'", ci.name, worker.id); worker.send(message); callback(dispatched.result.promise); } invokeEvent(ef, ctx, event, callback) { const message = { type: "event", registration: this.registration, context: ctx.context, data: event, }; const dispatched = new Dispatched(new Deferred_1.Deferred(), ctx); this.events.set(ctx.context.invocationId, dispatched); const worker = this.assignWorker(); logger_1.logger.debug("Incoming event handler subscription '%s' dispatching to worker '%s'", ef.extensions.operationName, worker.id); worker.send(message); callback(dispatched.result.promise); } sendStatusMessage(payload, ctx) { return Promise.resolve(WebSocketMessageClient_1.sendMessage(payload, this.webSocket)); } createGraphClient(event) { return null; } createMessageClient(event) { if (RequestProcessor_1.isCommandIncoming(event)) { return new WebSocketMessageClient_1.WebSocketCommandMessageClient(event, this.webSocket); } else if (RequestProcessor_1.isEventIncoming(event)) { return new WebSocketMessageClient_1.WebSocketEventMessageClient(event, this.webSocket); } } assignWorker() { const workers = []; for (const id in cluster.workers) { if (cluster.workers.hasOwnProperty(id)) { const worker = cluster.workers[id]; if (worker.isConnected()) { workers.push(worker); } } } return workers[Math.floor(Math.random() * workers.length)]; } } exports.ClusterMasterRequestProcessor = ClusterMasterRequestProcessor; class Dispatched { constructor(result, context) { this.result = result; this.context = context; } } function hydrateContext(msg) { return { invocationId: msg.context.invocationId, correlationId: msg.context.correlationId, workspaceId: msg.context.workspaceId, context: msg.context, }; } //# sourceMappingURL=ClusterMasterRequestProcessor.js.map