@atomist/automation-client
Version:
Atomist API for software low-level client
248 lines • 11.1 kB
JavaScript
"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