UNPKG

@camunda8/sdk

Version:

[![NPM](https://nodei.co/npm/@camunda8/sdk.png)](https://www.npmjs.com/package/@camunda8/sdk)

205 lines 9.58 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CamundaJobWorker = void 0; const events_1 = require("events"); const C8Logger_1 = require("./C8Logger"); class CamundaJobWorker extends events_1.EventEmitter { constructor(config, restClient) { super(); this.config = config; this.restClient = restClient; this.currentlyActiveJobCount = 0; this.pollLock = false; this.backoffRetryCount = 0; this.stopping = false; this.pollInterval = config.pollIntervalMs ?? 300; this.capacity = this.config.maxJobsToActivate; this.maxBackoffTimeMs = config.maxBackoffTimeMs ?? restClient.getConfig().CAMUNDA_JOB_WORKER_MAX_BACKOFF_MS; this.log = (0, C8Logger_1.getLogger)({ logger: config.logger ?? restClient.log }); this.logMeta = () => ({ worker: this.config.worker, type: this.config.type, pollIntervalMs: this.pollInterval, capacity: this.config.maxJobsToActivate, currentload: this.currentlyActiveJobCount, }); this.log.debug(`Created REST Job Worker`, this.logMeta()); if (config.autoStart ?? true) { this.start(); } } start() { this.log.debug(`Starting poll loop`, this.logMeta()); this.emit('start'); this.stopping = false; this.poll(); this.loopHandle = setInterval(() => this.poll(), this.pollInterval); } /** Stops the Job Worker polling for more jobs. If await this call, and it will return as soon as all currently active jobs are completed. * The deadline for all currently active jobs to complete is 30s by default. If the active jobs do not complete by the deadline, this method will throw. */ async stop(deadlineMs = 30000) { this.log.debug(`Stop requested`, this.logMeta()); this.stopping = true; /** Stopping polling for new jobs */ clearInterval(this.loopHandle); clearTimeout(this.backoffTimer); /** Do not allow the backoff retry to restart polling */ clearTimeout(this.backoffTimer); // Cancel the active poll if it exists // See: https://github.com/camunda/camunda-8-js-sdk/issues/424 if (this.activePoll && !this.activePoll.isCanceled) { // I'm not sure that this will actually catch any thrown error, because the cancellation happens in a different context. try { this.activePoll.cancel('Worker stopped'); this.log.debug(`Active poll cancelled`, this.logMeta()); } catch (error) { this.log.error( // eslint-disable-next-line @typescript-eslint/no-explicit-any `Error while cancelling active poll: ${error.message}`, { error, ...this.logMeta(), }); } finally { this.activePoll = undefined; } } return new Promise((resolve, reject) => { if (this.currentlyActiveJobCount === 0) { this.log.debug(`All jobs drained. Worker stopped.`, this.logMeta()); this.emit('stop'); return resolve(null); } /** This is an error timeout - if we don't complete all active jobs before the specified deadline, we reject the Promise */ const timeout = setTimeout(() => { clearInterval(wait); this.log.debug(`Failed to drain all jobs in ${deadlineMs}ms`, this.logMeta()); return reject(new Error(`Failed to drain all jobs in ${deadlineMs}ms`)); }, deadlineMs); /** Check every 500ms to see if our active job count has hit zero, i.e: all active work is stopped */ const wait = setInterval(() => { if (this.currentlyActiveJobCount === 0) { this.log.debug(`All jobs drained. Worker stopped.`, this.logMeta()); clearInterval(wait); clearTimeout(timeout); this.emit('stop'); return resolve(null); } this.log.debug(`Stopping - waiting for active jobs to complete.`, this.logMeta()); }, 500); }); } /** This is an alias for stop(). Provided for compatibility with the gRPC worker implementation. * Stops the Job Worker polling for more jobs. If await this call, and it will return as soon as all currently active jobs are completed. * The deadline for all currently active jobs to complete is 30s by default. If the active jobs do not complete by the deadline, this method will throw. */ close(deadlineMs = 30000) { return this.stop(deadlineMs); } poll() { if (this.pollLock || this.stopping) { return; } this.emit('poll', { currentlyActiveJobCount: this.currentlyActiveJobCount, maxJobsToActivate: this.config.maxJobsToActivate, worker: this.config.worker, }); this.pollLock = true; if (this.currentlyActiveJobCount >= this.config.maxJobsToActivate) { this.log.debug(`At capacity - not requesting more jobs`, this.logMeta()); this.pollLock = false; return; } this.log.trace(`Polling for jobs`, this.logMeta()); const remainingJobCapacity = this.config.maxJobsToActivate - this.currentlyActiveJobCount; const req = { maxJobsToActivate: this.config.maxJobsToActivate, timeout: this.config.timeout, type: this.config.type, worker: this.config.worker, fetchVariable: this.config.fetchVariable, requestTimeout: this.config.requestTimeout, tenantIds: this.config.tenantIds, }; this.activePoll = this.restClient.activateJobs({ ...req, maxJobsToActivate: remainingJobCapacity, }); this.activePoll .then((jobs) => { const count = jobs.length; this.currentlyActiveJobCount += count; this.log.debug(`Activated ${count} jobs`, this.logMeta()); this.emit('work', jobs); // The job handlers for the activated jobs will run in parallel jobs.forEach((job) => this.handleJob(job)); // if the handler throws, the job will be failed and the error logged this.pollLock = false; this.backoffRetryCount = 0; }) .catch((e) => { // If the poll was cancelled because the worker is stopping, we don't need to log an error — the canceled promise rejection is expected. // https://github.com/camunda/camunda-8-js-sdk/issues/432 if (this.stopping && e.code === 'ERR_CANCELED') { return; } // This can throw a 400, 401, or 500 REST Error with the Content-Type application/problem+json // The schema is: // { type: string, title: string, status: number, detail: string, instance: string } if (e.statusCode === 500 && e.toString().includes('RESOURCE_EXHAUSTED')) { this.log.warn('The server responded with a back pressure signal RESOURCE_EXHAUSTED. Check the server resource allocation and current load.'); } else { this.log.error('Error during job worker poll'); this.log.error(e); } this.emit('pollError', e); this.activePoll = undefined; const backoffDuration = Math.min(2000 * ++this.backoffRetryCount, this.maxBackoffTimeMs); this.log.warn(`Backing off worker poll due to failure. Next attempt will be made in ${backoffDuration}ms...`); this.emit('backoff', backoffDuration); this.backoffTimer = setTimeout(() => { this.pollLock = false; this.poll(); }, backoffDuration); }); } async handleJob(job) { try { this.log.debug(`Invoking job handler for job ${job.jobKey}`, this.logMeta()); await this.config.jobHandler(job, this.log); this.log.debug(`Completed job handler for job ${job.jobKey}.`, this.logMeta()); } catch (e) { /** Unhandled exception in the job handler */ if (e instanceof Error) { // If err is an instance of Error, we can safely access its properties this.log.error(`Unhandled exception in job handler for job ${job.jobKey}`, this.logMeta()); this.log.error(`Error: ${e.message}`, { stack: e.stack, ...this.logMeta(), }); } else { // If err is not an Error, log it as is this.log.error('An unknown error occurred while executing a job handler', { error: e, ...this.logMeta() }); } this.log.error(`Failing the job`, this.logMeta()); await job.fail({ errorMessage: e.toString(), retries: job.retries - 1, }); } finally { /** Decrement the active job count in all cases */ this.currentlyActiveJobCount--; } } } exports.CamundaJobWorker = CamundaJobWorker; //# sourceMappingURL=CamundaJobWorker.js.map