UNPKG

zeebe-node

Version:

The Node.js client library for the Zeebe Workflow Automation Engine.

403 lines 17.6 kB
"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