node-resque
Version:
an opinionated implementation of resque in node
330 lines (329 loc) • 12.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Worker = void 0;
const events_1 = require("events");
const os = require("os");
const pluginRunner_1 = require("./pluginRunner");
const queue_1 = require("./queue");
function prepareJobs(jobs) {
return Object.keys(jobs).reduce(function (h, k) {
const job = jobs[k];
h[k] = typeof job === "function" ? { perform: job } : job;
return h;
}, {});
}
class Worker extends events_1.EventEmitter {
constructor(options, jobs = {}) {
super();
const defaults = {
name: os.hostname() + ":" + process.pid,
queues: "*",
timeout: 5000,
looping: true,
id: 1,
};
for (const i in defaults) {
if (options[i] === undefined || options[i] === null) {
options[i] = defaults[i];
}
}
this.options = options;
this.jobs = prepareJobs(jobs);
this.name = this.options.name;
this.queues = this.options.queues;
this.queue = null;
this.originalQueue = null;
this.error = null;
this.result = null;
this.ready = true;
this.running = false;
this.working = false;
this.job = null;
this.pingTimer = null;
this.started = false;
this.queueObject = new queue_1.Queue({ connection: options.connection }, this.jobs);
this.queueObject.on("error", (error) => {
this.emit("error", error);
});
}
async connect() {
await this.queueObject.connect();
this.connection = this.queueObject.connection;
await this.checkQueues();
}
async start() {
if (this.ready) {
this.started = true;
this.emit("start", new Date());
await this.init();
this.poll();
}
}
async init() {
await this.track();
await this.connection.redis.set(this.connection.key("worker", this.name, this.stringQueues(), "started"), Math.round(new Date().getTime() / 1000));
await this.ping();
this.pingTimer = setInterval(this.ping.bind(this), this.options.timeout);
}
async end() {
this.running = false;
if (this.working === true) {
await new Promise((resolve) => {
setTimeout(() => {
resolve();
}, this.options.timeout);
});
return this.end();
}
if (this.connection &&
(this.connection.connected === true ||
this.connection.connected === undefined ||
this.connection.connected === null)) {
clearInterval(this.pingTimer);
await this.untrack();
}
await this.queueObject.end();
this.emit("end", new Date());
}
async poll(nQueue = 0) {
if (!this.running)
return;
this.queue = this.queues[nQueue];
this.emit("poll", this.queue);
if (this.queue === null || this.queue === undefined) {
await this.checkQueues();
await this.pause();
return null;
}
if (this.working === true) {
const error = new Error("refusing to get new job, already working");
this.emit("error", error, this.queue);
return null;
}
this.working = true;
try {
const currentJob = await this.getJob();
if (currentJob) {
if (this.options.looping) {
this.result = null;
return this.perform(currentJob);
}
else {
return currentJob;
}
}
else {
this.working = false;
if (nQueue === this.queues.length - 1) {
if (this.originalQueue === "*")
await this.checkQueues();
await this.pause();
return null;
}
else {
return this.poll(nQueue + 1);
}
}
}
catch (error) {
this.emit("error", error, this.queue);
this.working = false;
await this.pause();
return null;
}
}
async perform(job) {
this.job = job;
this.error = null;
let toRun;
const startedAt = new Date().getTime();
if (!this.jobs[job.class]) {
this.error = new Error(`No job defined for class "${job.class}"`);
return this.completeJob(false, startedAt);
}
const perform = this.jobs[job.class].perform;
if (!perform || typeof perform !== "function") {
this.error = new Error(`Missing Job: "${job.class}"`);
return this.completeJob(false, startedAt);
}
this.emit("job", this.queue, this.job);
let triedAfterPerform = false;
try {
toRun = await pluginRunner_1.RunPlugins(this, "beforePerform", job.class, this.queue, this.jobs[job.class], job.args);
if (toRun === false) {
return this.completeJob(false, startedAt);
}
let callableArgs = [job.args];
if (job.args === undefined || job.args instanceof Array) {
callableArgs = job.args;
}
for (const i in callableArgs) {
if (typeof callableArgs[i] === "object" && callableArgs[i] !== null) {
Object.freeze(callableArgs[i]);
}
}
this.result = await perform.apply(this, callableArgs);
triedAfterPerform = true;
toRun = await pluginRunner_1.RunPlugins(this, "afterPerform", job.class, this.queue, this.jobs[job.class], job.args);
return this.completeJob(true, startedAt);
}
catch (error) {
this.error = error;
if (!triedAfterPerform) {
try {
await pluginRunner_1.RunPlugins(this, "afterPerform", job.class, this.queue, this.jobs[job.class], job.args);
}
catch (error) {
if (error && !this.error) {
this.error = error;
}
}
}
return this.completeJob(!this.error, startedAt);
}
}
// #performInline is used to run a job payload directly.
// If you are planning on running a job via #performInline, this worker should also not be started, nor should be using event emitters to monitor this worker.
// This method will also not write to redis at all, including logging errors, modify resque's stats, etc.
async performInline(func, args = []) {
const q = "_direct-queue-" + this.name;
let toRun;
if (!(args instanceof Array)) {
args = [args];
}
if (this.started) {
throw new Error("Worker#performInline can not be used on a started worker");
}
if (!this.jobs[func]) {
throw new Error(`No job defined for class "${func}"`);
}
if (!this.jobs[func].perform) {
throw new Error(`Missing Job: "${func}"`);
}
try {
toRun = await pluginRunner_1.RunPlugins(this, "beforePerform", func, q, this.jobs[func], args);
if (toRun === false) {
return;
}
this.result = await this.jobs[func].perform.apply(this, args);
toRun = await pluginRunner_1.RunPlugins(this, "afterPerform", func, q, this.jobs[func], args);
return this.result;
}
catch (error) {
this.error = error;
throw error;
}
}
async completeJob(toRespond, startedAt) {
const duration = new Date().getTime() - startedAt;
if (this.error) {
await this.fail(this.error, duration);
}
else if (toRespond) {
await this.succeed(this.job, duration);
}
this.working = false;
await this.connection.redis.del(this.connection.key("worker", this.name, this.stringQueues()));
this.job = null;
if (this.options.looping) {
this.poll();
}
}
async succeed(job, duration) {
await this.connection.redis.incr(this.connection.key("stat", "processed"));
await this.connection.redis.incr(this.connection.key("stat", "processed", this.name));
this.emit("success", this.queue, job, this.result, duration);
}
async fail(err, duration) {
await this.connection.redis.incr(this.connection.key("stat", "failed"));
await this.connection.redis.incr(this.connection.key("stat", "failed", this.name));
await this.connection.redis.rpush(this.connection.key("failed"), JSON.stringify(this.failurePayload(err, this.job)));
this.emit("failure", this.queue, this.job, err, duration);
}
async pause() {
this.emit("pause");
await new Promise((resolve) => {
setTimeout(() => {
this.poll();
resolve();
}, this.options.timeout);
});
}
async getJob() {
let currentJob = null;
const queueKey = this.connection.key("queue", this.queue);
const workerKey = this.connection.key("worker", this.name, this.stringQueues());
const encodedJob = await this.connection.redis["popAndStoreJob"](queueKey, workerKey, new Date().toString(), this.queue, this.name);
if (encodedJob)
currentJob = JSON.parse(encodedJob);
return currentJob;
}
async track() {
this.running = true;
return this.connection.redis.sadd(this.connection.key("workers"), this.name + ":" + this.stringQueues());
}
async ping() {
const name = this.name;
const nowSeconds = Math.round(new Date().getTime() / 1000);
this.emit("ping", nowSeconds);
const payload = JSON.stringify({
time: nowSeconds,
name: name,
queues: this.stringQueues(),
});
await this.connection.redis.set(this.connection.key("worker", "ping", name), payload);
}
async untrack() {
const name = this.name;
const queues = this.stringQueues();
if (!this.connection || !this.connection.redis) {
return;
}
await this.connection.redis.srem(this.connection.key("workers"), name + ":" + queues);
await this.connection.redis.del(this.connection.key("worker", "ping", name));
await this.connection.redis.del(this.connection.key("worker", name, queues));
await this.connection.redis.del(this.connection.key("worker", name, queues, "started"));
await this.connection.redis.del(this.connection.key("stat", "failed", name));
await this.connection.redis.del(this.connection.key("stat", "processed", name));
}
async checkQueues() {
if (Array.isArray(this.queues) && this.queues.length > 0) {
this.ready = true;
}
if ((this.queues[0] === "*" && this.queues.length === 1) ||
this.queues.length === 0 ||
this.originalQueue === "*") {
this.originalQueue = "*";
await this.untrack();
const response = await this.connection.redis.smembers(this.connection.key("queues"));
this.queues = response ? response.sort() : [];
await this.track();
}
}
failurePayload(err, job) {
return {
worker: this.name,
queue: this.queue,
payload: job,
exception: err.name,
error: err.message,
backtrace: err.stack ? err.stack.split("\n").slice(1) : null,
failed_at: new Date().toString(),
};
}
stringQueues() {
if (this.queues.length === 0) {
return ["*"].join(",");
}
else {
try {
return this.queues.join(",");
}
catch (e) {
return "";
}
}
}
}
exports.Worker = Worker;
exports.Worker = Worker;