zeebe-node
Version:
The Node.js client library for the Zeebe Workflow Automation Engine.
403 lines • 17.6 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ZBWorkerBase = void 0;
const chalk_1 = __importDefault(require("chalk"));
const events_1 = require("events");
const typed_duration_1 = require("typed-duration");
const uuid = __importStar(require("uuid"));
const lib_1 = require("../lib");
const ZB = __importStar(require("../lib/interfaces-1.0"));
const ZBClient_1 = require("../zb/ZBClient");
const GrpcError_1 = require("./GrpcError");
const TypedEmitter_1 = require("./TypedEmitter");
const debug = require('debug')('worker');
debug('Loaded ZBWorkerBase');
const MIN_ACTIVE_JOBS_RATIO_BEFORE_ACTIVATING_JOBS = 0.3;
const CapacityEvent = {
Available: 'AVAILABLE',
Empty: 'CAPACITY_EMPTY',
};
class ZBWorkerBase extends TypedEmitter_1.TypedEmitter {
constructor({ grpcClient, id, log, options, taskHandler, taskType, zbClient, }) {
var _a, _b;
super();
this.activeJobs = 0;
this.pollCount = 0;
this.cancelWorkflowOnException = false;
this.closing = false;
this.closed = false;
this.id = uuid.v4();
this.stalled = false;
this.connected = true;
this.readied = false;
this.pollMutex = false;
this.backPressureRetryCount = 0;
this.handleStreamEnd = id => {
this.jobStream = undefined;
this.logger.logDebug(`Deleted job stream [${id}] listeners and job stream reference`);
};
this.handleJobResponse = (res) => {
// If we are closing, don't start working on these jobs. They will have to be timed out by the server.
if (this.closing) {
return;
}
this.activeJobs += res.jobs.length;
const jobs = res.jobs.map(job => (0, lib_1.parseVariablesAndCustomHeadersToJSON)(job));
this.handleJobs(jobs);
};
options = options || {};
if (!taskType) {
throw new Error('Missing taskType');
}
if (!taskHandler) {
throw new Error('Missing taskHandler');
}
this.taskHandler = taskHandler;
this.taskType = taskType;
this.maxJobsToActivate =
options.maxJobsToActivate || ZBWorkerBase.DEFAULT_MAX_ACTIVE_JOBS;
this.activeJobsThresholdForReactivation =
this.maxJobsToActivate *
MIN_ACTIVE_JOBS_RATIO_BEFORE_ACTIVATING_JOBS;
this.jobBatchMinSize = Math.min((_a = options.jobBatchMinSize) !== null && _a !== void 0 ? _a : 0, this.maxJobsToActivate);
this.timeout =
options.timeout || ZBWorkerBase.DEFAULT_JOB_ACTIVATION_TIMEOUT;
this.pollInterval = options.pollInterval;
this.longPoll = options.longPoll;
this.pollInterval = options.pollInterval;
this.id = id || uuid.v4();
// Set options.debug to true to count the number of poll requests for testing
// See the Worker-LongPoll test
this.debugMode = options.debug === true;
this.grpcClient = grpcClient;
const onError = () => {
// options.onConnectionError?.()
var _a;
if (this.connected) {
this.emit(ZBClient_1.ConnectionStatusEvent.connectionError);
(_a = options.onConnectionError) === null || _a === void 0 ? void 0 : _a.call(options);
this.connected = false;
this.readied = false;
}
};
this.grpcClient.on(ZBClient_1.ConnectionStatusEvent.connectionError, onError);
const onReady = async () => {
var _a;
if (!this.readied) {
this.emit(ZBClient_1.ConnectionStatusEvent.ready);
(_a = options.onReady) === null || _a === void 0 ? void 0 : _a.call(options);
this.readied = true;
this.connected = true;
}
};
this.grpcClient.on(ZBClient_1.ConnectionStatusEvent.unknown, onReady);
this.grpcClient.on(ZBClient_1.ConnectionStatusEvent.ready, onReady);
this.cancelWorkflowOnException = (_b = options.failProcessOnException) !== null && _b !== void 0 ? _b : false;
this.zbClient = zbClient;
this.grpcClient.topologySync().catch(e => {
// Swallow exception to avoid throwing if retries are off
if (e.thisWillNeverHappenYo) {
this.emit(ZBClient_1.ConnectionStatusEvent.unknown);
}
});
this.fetchVariable = options.fetchVariable;
this.logger = log;
this.capacityEmitter = new events_1.EventEmitter();
this.pollLoop = setInterval(() => this.poll(), typed_duration_1.Duration.milliseconds.from(this.pollInterval));
debug(`Created worker for task type ${taskType}`);
}
/**
* Returns a promise that the worker has stopped accepting tasks and
* has drained all current active tasks. Will reject if you try to call it more than once.
*/
close(timeout) {
if (this.closePromise) {
return this.closePromise;
}
this.closePromise = new Promise(async (resolve) => {
var _a, _b;
// this.closing prevents the worker from starting work on any new tasks
this.closing = true;
clearInterval(this.pollLoop);
if (this.activeJobs <= 0) {
await this.grpcClient.close(timeout);
this.grpcClient.removeAllListeners();
(_b = (_a = this.jobStream) === null || _a === void 0 ? void 0 : _a.cancel) === null || _b === void 0 ? void 0 : _b.call(_a);
this.jobStream = undefined;
this.logger.logDebug('Cancelled Job Stream');
resolve(null);
}
else {
this.capacityEmitter.once(CapacityEvent.Empty, async () => {
await this.grpcClient.close(timeout);
this.grpcClient.removeAllListeners();
this.emit(ZBClient_1.ConnectionStatusEvent.close);
this.removeAllListeners();
resolve(null);
});
}
});
return this.closePromise;
}
log(msg) {
this.logger.logInfo(msg);
}
debug(msg) {
this.logger.logDebug(msg);
}
error(msg) {
this.logger.logError(msg);
}
drainOne() {
this.activeJobs--;
this.logger.logDebug(`Load: ${this.activeJobs}/${this.maxJobsToActivate}`);
const hasSufficientAvailableCapacityToRequestMoreJobs = this.activeJobs <= this.activeJobsThresholdForReactivation;
if (!this.closing && hasSufficientAvailableCapacityToRequestMoreJobs) {
this.capacityEmitter.emit(CapacityEvent.Available);
}
if (this.closing && this.activeJobs === 0) {
this.capacityEmitter.emit(CapacityEvent.Empty);
}
// If we are closing and hit zero active jobs, resolve the closing promise.
if (this.activeJobs <= 0 && this.closing) {
if (this.closeCallback && !this.closed) {
this.closeCallback();
}
}
}
handleJobs(_) {
this.log(`This method must be declared in a class that extends this base`);
}
makeCompleteHandlers(thisJob) {
let methodCalled;
/**
* This is a wrapper that allows us to throw an error if a job acknowledgement function is called more than once,
* for these functions should be called once only (and only one should be called, but we don't handle that case).
* */
const errorMsgOnPriorMessageCall = (thisMethod, wrappedFunction) => {
return (...args) => {
if (methodCalled !== undefined) {
// tslint:disable-next-line: no-console
console.log(chalk_1.default.red(`WARNING: Call to ${thisMethod}() after ${methodCalled}() was called.
You should call only one job action method in the worker handler. This is a bug in the ${this.taskType} worker handler.`));
// tslint:disable-next-line: no-console
console.log('handler', this.taskHandler.toString()); // @DEBUG
return wrappedFunction(...args);
}
methodCalled = thisMethod;
return wrappedFunction(...args);
};
};
const cancelWorkflow = (job) => () => this.zbClient
.cancelProcessInstance(job.processInstanceKey)
.then(() => ZB.JOB_ACTION_ACKNOWLEDGEMENT);
const failJob = (job) => (conf, retries) => {
var _a, _b;
const isFailureConfig = (_conf) => typeof _conf === 'object';
const errorMessage = isFailureConfig(conf) ? conf.errorMessage : conf;
const retryBackOff = isFailureConfig(conf) ? (_a = conf.retryBackOff) !== null && _a !== void 0 ? _a : 0 : 0;
const _retries = isFailureConfig(conf) ? (_b = conf.retries) !== null && _b !== void 0 ? _b : 0 : retries;
return this.failJob({ job, errorMessage, retries: _retries, retryBackOff });
};
const succeedJob = (job) => (completedVariables) => this.completeJob(job.key, completedVariables !== null && completedVariables !== void 0 ? completedVariables : {});
const errorJob = (job) => (e, errorMessage = '') => {
var _a;
const isErrorJobWithVariables = (s) => typeof s === 'object';
const errorCode = isErrorJobWithVariables(e) ? e.errorCode : e;
errorMessage = isErrorJobWithVariables(e) ? (_a = e.errorMessage) !== null && _a !== void 0 ? _a : '' : errorMessage;
const variables = isErrorJobWithVariables(e) ? e.variables : {};
return this.errorJob({
errorCode,
errorMessage,
job,
variables
});
};
const fail = failJob(thisJob);
const succeed = succeedJob(thisJob);
return {
cancelWorkflow: cancelWorkflow(thisJob),
complete: errorMsgOnPriorMessageCall('job.complete', succeed),
error: errorMsgOnPriorMessageCall('error', errorJob(thisJob)),
fail: errorMsgOnPriorMessageCall('job.fail', fail),
forward: errorMsgOnPriorMessageCall('job.forward', () => {
this.drainOne();
return ZB.JOB_ACTION_ACKNOWLEDGEMENT;
}),
};
}
failJob({ job, errorMessage, retries, retryBackOff, }) {
return this.zbClient
.failJob({
errorMessage,
jobKey: job.key,
retries: retries !== null && retries !== void 0 ? retries : job.retries - 1,
retryBackOff: retryBackOff !== null && retryBackOff !== void 0 ? retryBackOff : 0,
})
.then(() => ZB.JOB_ACTION_ACKNOWLEDGEMENT)
.finally(() => {
this.logger.logDebug(`Failed job ${job.key} - ${errorMessage}`);
this.drainOne();
});
}
completeJob(jobKey, completedVariables = {}) {
return this.zbClient
.completeJob({
jobKey,
variables: completedVariables,
})
.then(res => {
this.logger.logDebug(`Completed job ${jobKey} for ${this.taskType}`);
return res;
})
.catch(e => {
this.logger.logDebug(`Completing job ${jobKey} for ${this.taskType} threw ${e.message}`);
return e;
})
.then(() => ZB.JOB_ACTION_ACKNOWLEDGEMENT)
.finally(() => {
this.drainOne();
});
}
errorJob({ errorCode, errorMessage, job, variables }) {
return this.zbClient
.throwError({
errorCode,
errorMessage,
jobKey: job.key,
variables
})
.then(() => this.logger.logDebug(`Errored job ${job.key} - ${errorMessage}`))
.catch(e => {
this.logger.logError(`Exception while attempting to raise BPMN Error for job ${job.key} - ${errorMessage}`);
this.logger.logError(e);
})
.then(() => {
this.drainOne();
return ZB.JOB_ACTION_ACKNOWLEDGEMENT;
});
}
async poll() {
var _a;
const pollAlreadyInProgress = this.pollMutex || this.jobStream !== undefined;
const workerIsClosing = this.closePromise !== undefined || this.closing;
const insufficientCapacityAvailable = this.activeJobs > this.activeJobsThresholdForReactivation;
if (pollAlreadyInProgress ||
workerIsClosing ||
insufficientCapacityAvailable) {
debug('Worker polling blocked', {
pollAlreadyInProgress,
workerIsClosing,
insufficientCapacityAvailable
});
return;
}
this.pollMutex = true;
debug('Polling...');
this.logger.logDebug('Activating Jobs...');
const id = uuid.v4();
const jobStream = await this.activateJobs(id);
const start = Date.now();
this.logger.logDebug(`Long poll loop. this.longPoll: ${typed_duration_1.Duration.value.of(this.longPoll)}`, id, start);
if (jobStream.stream) {
this.logger.logDebug(`Stream [${id}] opened...`);
this.jobStream = jobStream.stream;
// This event happens when the server cancels the call after the deadline
// And when it has completed a response with work
jobStream.stream.on('end', () => {
this.logger.logDebug(`Stream [${id}] ended after ${(Date.now() - start) /
1000} seconds`);
this.handleStreamEnd(id);
this.backPressureRetryCount = 0;
});
jobStream.stream.on('error', error => {
this.logger.logDebug(`Stream [${id}] error after ${(Date.now() - start) /
1000} seconds`, error);
// Backoff on
if (error.code === GrpcError_1.GrpcError.RESOURCE_EXHAUSTED || error.code === GrpcError_1.GrpcError.INTERNAL) {
setTimeout(() => this.handleStreamEnd(id), 1000 * 2 ** this.backPressureRetryCount++);
}
else {
this.handleStreamEnd(id);
}
});
}
if (jobStream.error) {
const error = (_a = jobStream.error) === null || _a === void 0 ? void 0 : _a.message;
this.logger.logError({ id, error });
}
this.pollMutex = false;
}
async activateJobs(id) {
if (this.stalled) {
debug('Stalled');
return { stalled: true };
}
if (this.closing) {
debug('Closing');
return {
closing: true,
};
}
if (this.debugMode) {
this.logger.logDebug(`Activating Jobs...`);
}
debug('Activating Jobs');
let stream;
const amount = this.maxJobsToActivate - this.activeJobs;
const requestTimeout = this.longPoll || -1;
const activateJobsRequest = {
maxJobsToActivate: amount,
requestTimeout,
timeout: this.timeout,
type: this.taskType,
worker: this.id,
fetchVariable: this.fetchVariable,
};
this.logger.logDebug(`Requesting ${amount} jobs on [${id}] with requestTimeout ${typed_duration_1.Duration.value.of(requestTimeout)}, job timeout: ${typed_duration_1.Duration.value.of(this.timeout)}`);
debug(`Requesting ${amount} jobs on [${id}] with requestTimeout ${typed_duration_1.Duration.value.of(requestTimeout)}, job timeout: ${typed_duration_1.Duration.value.of(this.timeout)}`);
try {
stream = await this.grpcClient.activateJobsStream(activateJobsRequest);
if (this.debugMode) {
this.pollCount++;
}
}
catch (error) {
return {
error,
};
}
if (stream.error) {
debug(`Stream error`, stream.error);
return { error: stream.error };
}
stream.on('data', this.handleJobResponse);
return { stream };
}
}
exports.ZBWorkerBase = ZBWorkerBase;
ZBWorkerBase.DEFAULT_JOB_ACTIVATION_TIMEOUT = typed_duration_1.Duration.seconds.of(60);
ZBWorkerBase.DEFAULT_MAX_ACTIVE_JOBS = 32;
//# sourceMappingURL=ZBWorkerBase.js.map