@camunda8/sdk
Version:
[](https://www.npmjs.com/package/@camunda8/sdk)
157 lines • 7.06 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);
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(`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);
});
}
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;
this.restClient
.activateJobs({
...this.config,
maxJobsToActivate: remainingJobCapacity,
})
.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));
this.pollLock = false;
this.backoffRetryCount = 0;
})
.catch((e) => {
// This can throw a 400 or 500 REST Error with the Content-Type application/problem+json
// The schema is:
// { type: string, title: string, status: number, detail: string, instance: string }
this.log.error('Error during job worker poll');
this.log.error(e);
this.emit('pollError', e);
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