@camunda8/sdk
Version:
[](https://www.npmjs.com/package/@camunda8/sdk)
205 lines • 9.58 kB
JavaScript
"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