torotask
Version:
Task queue processing in NodeJS based on BullMQ and Redis
230 lines • 8.7 kB
JavaScript
import { Job } from 'bullmq';
import { TaskQueue } from './queue.js';
import { convertJobOptions } from './utils/convert-job-options.js';
export class TaskJob extends Job {
options;
logger;
taskClient;
taskQueue;
/**
* The array of real TaskJob instances that constitute a batch.
*/
batch;
payload;
state;
constructor(queue, name, data, options = {}, id) {
const opts = convertJobOptions(options);
super(queue, name, data, opts, id);
this.options = options;
this.payload = this.data.payload;
this.state = this.data.state;
// Check if the queue is an instance of TaskQueue
if (queue instanceof TaskQueue) {
this.taskQueue = queue;
this.logger = queue.logger.child({ taskRun: this.name, taskId: this.id });
this.taskClient = queue.taskClient;
}
this.batch = [];
}
/**
* Sets a job's payload
*
* @param payload - the payload that will replace the current jobs payload.
*/
async setPayload(payload) {
this.payload = payload;
const data = {
...this.data,
payload: this.payload,
};
return this.updateData(data);
}
/**
* Partially updates a job's payload
*
* @param payload - the payload that will merge with the current jobs payload.
*/
async updatePayload(payload) {
const newPayload = {
...this.payload,
...payload,
};
return this.setPayload(newPayload);
}
/**
* Sets a job's state
*
* @param state - the state that will replace the current jobs state.
*/
async setState(state) {
this.state = state;
const data = {
...this.data,
state: this.state,
};
return this.updateData(data);
}
/**
* Partially updates a job's state
*
* @param state - the state that will merge with the current jobs state.
*/
async updateState(state) {
const newState = {
...this.state,
...state,
};
return this.setState(newState);
}
/**
* Sets/replaces the internal list of Job instances managed by this container.
* @param jobs The array of Job instances representing the batch.
*/
setBatch(batch) {
this.batch = batch;
}
/**
* Adds a single job to the internal list for this batch container.
* @param job The job to add.
*/
addBatchJob(job) {
this.batch.push(job);
}
/**
* Adds multiple jobs to the internal list for this batch container.
* @param jobs The jobs to add.
*/
addBatchJobs(jobs) {
this.batch.push(...jobs);
}
/**
* Returns the array of actual TaskJob instances managed by this batch container.
* @returns The array of jobs.
*/
getBatch() {
return this.batch;
}
/**
* Returns the array of actual TaskJob instances managed by this batch container.
* @returns The array of jobs.
*/
get isBatch() {
return this.batch.length > 0;
}
/**
* Returns the number of jobs currently in the batch.
* @returns The number of jobs.
*/
get batchLength() {
return this.batch.length;
}
/**
* **Manual Lock Extension:** Extends the lock for all individual jobs currently held within this batch container.
*
* **Usage Note:** Generally **not required**. Rely on the Worker's automatic lock renewal
* by configuring `lockDuration` appropriately. Use this only for explicit manual control
* during very long-running steps within your handler.
*
* @param duration - Duration (in milliseconds) to extend the lock by. Uses the job's configured lock duration if omitted.
* @returns A promise that resolves when all lock extensions have been attempted.
*/
async extendLocks(duration) {
if (!this.isBatch)
return;
this.logger?.debug(`Manually extending locks for ${this.batch.length} jobs in batch ${this.id} by ${duration}ms`);
const promises = this.batch.map((job) =>
// Each job needs its token for lock extension
job.token
? job.extendLock(job.token, duration).catch((err) => {
// Log or handle individual extension errors
this.logger?.error(`Failed to extend lock for job ${job.id} within batch ${this.id}:`, err);
})
: // Handle case where job token might be missing (shouldn't happen if fetched correctly)
Promise.reject(new Error(`Job ${job.id} missing token for lock extension.`)));
// Use Promise.allSettled to wait for all attempts and see individual results/errors
await Promise.allSettled(promises);
}
/**
* Updates the progress for all individual jobs currently held within this batch container.
*
* @param progress The progress value (number or object).
* @returns A promise that resolves when all progress updates have been attempted.
*/
async updateProgress(progress) {
if (!this.isBatch) {
return super.updateProgress(progress);
}
const promises = this.batch.map((job) => job.updateProgress(progress).catch((err) => {
this.logger?.error(`Failed to update progress for job ${job.id} within batch ${this.id}:`, err);
}));
await Promise.allSettled(promises);
}
/**
* Sends the same log entry to all individual jobs currently held within this batch container
* using the underlying `job.log()` method.
*
* @param logRow The string log entry to add to each job's log in Redis.
* @returns A promise that resolves when all log additions have been attempted.
*/
async log(logRow) {
if (!this.isBatch) {
return super.log(logRow);
}
let firstLogCount = 0; // S
const promises = this.batch.map((job) => job.log(logRow).catch((err) => {
this.logger?.error(`Failed to add log entry for job ${job.id} within batch ${this.id}:`, err);
return null; // Return null or another indicator for failed logs
}));
const results = await Promise.allSettled(promises);
for (const result of results) {
if (result.status === 'fulfilled' && result.value !== null) {
firstLogCount = result.value;
break;
}
}
return firstLogCount;
}
/**
* Clears all batched job's logs
*
* @param keepLogs - the amount of log entries to preserve
*/
async clearLogs(keepLogs) {
if (!this.isBatch) {
return super.clearLogs(keepLogs);
}
const promises = this.batch.map((job) => job.clearLogs(keepLogs).catch((err) => {
this.logger?.error(`Failed to add log entry for job ${job.id} within batch ${this.id}:`, err);
}));
await Promise.allSettled(promises);
}
/**
* Attempts to move all individual jobs currently held within this batch container to the 'failed' state in BullMQ.
*
* **Use Case:** Useful if you detect a non-recoverable error *within* your batch handler
* and want to explicitly mark all jobs as failed *before* throwing an error to signal the overall batch failure.
* Often, just throwing an error from the handler is sufficient.
*
* **Requires Job Tokens:** This operation requires the lock `token` for each individual job.
*
* @param error The Error object representing the reason for failure.
* @returns A promise that resolves when all `moveToFailed` operations have been attempted.
*/
async moveToFailed(error, token, fetchNext = false) {
if (!this.isBatch) {
return super.moveToFailed(error, token, fetchNext);
}
this.logger?.warn(`Attempting to move ${this.batchLength} jobs in batch ${this.id} to failed state due to error: ${error.message}`);
const promises = this.batch.map(async (job) => {
if (!job.token) {
this.logger?.error(`Job ${job.id} inside batch ${this.id} is missing its lock token. Cannot move to failed.`);
return Promise.resolve(); // Skip this job
}
return job.moveToFailed(error, job.token).catch((moveError) => {
this.logger?.error(`Failed to move job ${job.id} to 'failed' state within batch ${this.id}:`, moveError);
});
});
await Promise.allSettled(promises);
}
}
//# sourceMappingURL=job.js.map