@vzeta/zeebe-node-test
Version:
The Node.js client library for the Zeebe Workflow Automation Engine.
382 lines • 16.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 _debug = __importStar(require("debug"));
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 TypedEmitter_1 = require("./TypedEmitter");
const debug = _debug.default('worker');
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, _c;
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 => 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 =
(_c = (_b = options.failWorkflowOnException) !== null && _b !== void 0 ? _b : options.failProcessOnException) !== null && _c !== void 0 ? _c : 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.logger = log;
this.capacityEmitter = new events_1.EventEmitter();
this.pollLoop = setInterval(() => this.poll(), typed_duration_1.Duration.milliseconds.from(this.pollInterval));
}
/**
* 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;
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) => (errorMessageOrFailureConfig, retries) => {
var _a;
const errorMessage = (typeof errorMessageOrFailureConfig === "string") ? errorMessageOrFailureConfig : errorMessageOrFailureConfig.errorMessage;
const retryBackOff = (typeof errorMessageOrFailureConfig === "string") ? 0 : (_a = errorMessageOrFailureConfig.retryBackOff) !== null && _a !== void 0 ? _a : 0;
return this.failJob({ job, errorMessage, retries, retryBackOff });
};
const succeedJob = (job) => (completedVariables) => this.completeJob(job.key, completedVariables !== null && completedVariables !== void 0 ? completedVariables : {});
const errorJob = (job) => (errorCode, errorMessage = '') => this.errorJob({
errorCode,
errorMessage,
job,
});
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),
failure: errorMsgOnPriorMessageCall('complete.failure', fail),
forward: errorMsgOnPriorMessageCall('job.forward', () => {
this.drainOne();
return ZB.JOB_ACTION_ACKNOWLEDGEMENT;
}),
forwarded: errorMsgOnPriorMessageCall('complete.forwarded', () => {
this.drainOne();
return ZB.JOB_ACTION_ACKNOWLEDGEMENT;
}),
success: errorMsgOnPriorMessageCall('complete.success', succeed),
};
}
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, }) {
return this.zbClient
.throwError({
errorCode,
errorMessage,
jobKey: job.key,
})
.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) {
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);
if (error.code === 8) {
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) {
return { stalled: true };
}
if (this.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,
};
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) {
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