8

When running the code below it prints too many results.

My suspicion is that the completed event listens to all the previous jobs made in the current queue instance.

How can I manage the completed event to listen just to the current job completion?

producer.js

The producer creates a job with a default numerical id and listens to the global completion in order to return a response when the job is done.

const BullQ = require('bull');

let bullQ = BullQ('my-first-queue', {
  redis: {
    host: process.env.REDISHOST || 'localhost',
    password: process.env.REDISPASSWORD || ''
  }
});

app.get('/search/:term', async (req, res) => {
  const job = await bullQ.add({
    searchTerm: req.params.term
  });
  
  // Listen to the global completion of the queue in order to return result.
  bullQ.on('global:completed', (jobId, result) => {
    // Check if id is a number without any additions
    if (/^\d+$/.test(jobId) && !res.headersSent) {
      console.log(`Producer get: Job ${jobId} completed! Result: ${result}`);
      res.json(`Job is completed with result: ${result}`);
    }
  });
});

consumer.js

The consumer has 2 roles.

  1. To consume the jobs as it should be by the book
  2. To create new jobs based on the result of the last job.
const BullQ = require('bull');

let bullQ = BullQ('my-first-queue', {
  redis: {
    host: process.env.REDISHOST || 'localhost',
    password: process.env.REDISPASSWORD || ''
  }
});

bullQ.process((job, done) => {
  // Simulate asynchronous server request.
  setTimeout(async () => {
    // Done the first job and return an answer to the producer after the timeout.
    done(null, `Search result for ${job.data.searchTerm}`);
    // next job run
    if (counter < 10) {
      // For the first run the id is just a number if not changed via the jobId in JobOpts,
      // the next time the job id will be set to {{id}}_next_{{counter}} we need only the first number in order not to end with a long and not clear concatenated string.
      let jobID = (/^\d+$/.test(job.id)) ? job.id : job.id.replace(/[^\d].*/,'');
      await createNextJob(jobID, ++counter);
    }
  }, 100);
});

// Create next job and add it to the queue.
// Listen to the completed jobs (locally)
const createNextJob = async (id, counter) => {
  const nextJob = bullQ.add({
    searchTerm: "Next job"
  }, {
    jobId: `${id}_next_${counter}`
  });

  await bullQ.on('completed', (job, result) => {
    job.finished();
    console.log(`Consumer(next): Job ${job.id} completed! Result: ${result}`);
  });
};
ggorlen
  • 44,755
  • 7
  • 76
  • 106
Da-Vi
  • 141
  • 1
  • 1
  • 8
  • hey try adding a `bullQ.on('global:failed', (jobid,data){console.log(jobid,data)});` at the end because I found I had an error in my job function that wasn't being reported causing this problem – PHP_Developer Jul 31 '19 at 14:41

1 Answers1

8

You can await job.finished() to get your result specific to a job object returned by queue.add().

Here's a simple, runnable example without Express to illustrate:

const Queue = require("bull"); // "bull": "^4.8.5"

const sleep = (ms=1000) =>
  new Promise(resolve => setTimeout(resolve, ms))
;

const queue = new Queue("test", process.env.REDIS_URL);
queue.process(4, async job => {
  await sleep(job.data.seconds * 1000); // waste time
  return Promise.resolve(`job ${job.id} complete!`);
});

(async () => {
  const job = await queue.add({seconds: 5});
  const result = await job.finished();
  console.log(result); // => job 42 complete!
  await queue.close();
})();

With Express:

const Queue = require("bull");
const express = require("express");

const sleep = (ms=1000) =>
  new Promise(resolve => setTimeout(resolve, ms))
;

const queue = new Queue("test", process.env.REDIS_URL);
queue.process(4, async job => {
  await sleep(job.data.seconds * 1000);
  return Promise.resolve(`job ${job.id} complete!`);
});

const app = express();
app
  .set("port", process.env.PORT || 5000)
  .get("/", async (req, res) => {
    try {
      const job = await queue.add({
        seconds: Math.abs(+req.query.seconds) || 10,
      });
      const result = await job.finished();
      res.send(result);
    }
    catch (err) {
      res.status(500).send({error: err.message});
    }
  })
  .listen(app.get("port"), () => 
    console.log("app running on port", app.get("port"))
  )
;

Sample run:

$ curl localhost:5000?seconds=2
job 42 complete!
ggorlen
  • 44,755
  • 7
  • 76
  • 106
  • Thank you so much! Not sure why I haven't seen `job.finished()` adequately documented elsewhere. This is a much better approach for certain situations! – rinogo Nov 10 '21 at 19:08
  • 1
    Hey I'm confused, why would you want to await a background job? Isn't the point of a job to free up the node thread and shorten the response time? await-ing the job would result in the response time being the same as if it was not a background job. – Dashiell Rose Bark-Huss Aug 08 '22 at 03:36
  • Sure, the job completion time will be the same, but the thread will be free to work on other requests as the background task completes. That's why it's async. For some background jobs, it's acceptable to delay the HTTP response or send a socket message or [SSE](https://en.wikipedia.org/wiki/Server-sent_events) upon completion from the same Express handler scope. It totally depends on your use case; my example is contrived to illustrate the tool. – ggorlen Aug 08 '22 at 06:12