bull 源码阅读笔记

是一个基于Redis的强大的Nodejs队列框架
git地址:https://github.com/OptimalBits/bull
下图是其与其他队列库的对比图,可以看出Bull支持优先度,并发,延迟任务,全局事件,频率限制,重复任务,原子操作,可视化页面等功能。

image.png

三个角色

一个队列Queue有三个角色:任务生产者, 任务消费者, 事件监听者
一般生产者和消费者被分为不同的实例,生产者可以在没有可用消费者的情况下,依然可以往队列中加入任务,Queue提供了异步通信。
另外可以让一个或者多个消费者从队列中消费任务。
worker,可以在相同或者不同的进程,同一台或者集群中运行。
Redis作为一个中间者,只要生产者消费之可以连接到Redis,他们就可以分工合作处理任务

一个任务的生命周期

当调用队列的 add 的方法时,任务将会有一个生命周期,直到运行成功或者失败(失败的任务将会被重试,将会有一个新的生命周期),如下所示


image.png

当任务被加到队列后将会是 Wait(等待被处理)或者Delayed状态 (延迟被处理)。Delayed状态的任务不会直接被处理,而是到指定时间后,排到wait队列后,等到worker空闲后处理
接下来是 Active 状态,表示任务正在被处理。当处理完成任务就会进入 completed 状态,或者完成失败进入 Fail 状态。
任务在被Active激活的时候会获取锁(防止一个任务被多个消费者消费),completed时候会释放锁。但是锁有过期时间(lockDuration)

producer 任务生产者

生产者就是,往队列里面增加任务,应用示例如下:

// 1.创建队列
const myFirstQueue = new Bull('my-first-queue');
// 2.往队列增加任务
const job = await myFirstQueue.add({
  foo: 'bar'
});

底层代码内容包括以下几个步骤:
1 创建队列
2 增加任务

  • 先看 queue.add,不管是单个任务还是重复性任务,最终都都调用了Job.create
Queue.prototype.add = function(name, data, opts) {
 ...
  opts = _.cloneDeep(opts || {});
  _.defaults(opts, this.defaultJobOptions);

  if (opts.repeat) {
    return this.isReady().then(() => {
      return this.nextRepeatableJob(name, data, opts, true);  // 后面也是增加了一个延时的job, Job.create(....)
    });
  } else {
    return Job.create(this, name, data, opts);
  }
};
  • job.create 调用了 addJob
Job.create = function(queue, name, data, opts) {
  const job = new Job(queue, name, data, opts);

  return queue
    .isReady()
    .then(() => {
      return addJob(queue, queue.client, job);
    })
    .then(jobId => {
      job.id = jobId;
      debuglog('Job added', jobId);
      return job;
    });
};
  • addJob里面是调用了lua脚本, 增加wait 以及 delayed 队列,还 publish 进行发布事件

增加任务后,根据lua,redis会增加相应的值, 并发布消息。

  • 任务增加成功后,wait 队列以及 delayed 队列会增加数据

单项任务如下所示:
会增加一个redis值,类型为hash,表示一个任务
key = redis键前缀: 队列名字: 编号
value = 相应任务配置


image.png

重复任务如下所示,会修改redis两个值
1: repeat队列,类型为zset,表示可重复任务队列
key = redis键前缀: 队列名字:'repeat'
value = 各个任务,score为相应任务的下一次执行的时间戳


image.png

2 类型hash,表示单个任务的具体信息:
redis键前缀:队列名字:'repeat:' + md5(name + jobId + namespace) + ':' + nextMillis


image.png

consumer 任务消费者

是指处理队列中的定时任务,应用示例如下:

const myFirstQueue = new Bull('my-first-queue');

myFirstQueue.process(async (job) => {
  return doSomething(job.data);
});

process 方法会在有任务要执行且worker空闲的时候被调用

底层代码内容包括以下几个步骤:
1 绑定处理方法handler
2 增加任务

  • queue.process 先看process当中主要调用了 setHandler(绑定handler) _initProcess(注册监听) start
Queue.prototype.process = function(name, concurrency, handler) {
  switch (arguments.length) {
    case 1:
      handler = name;
      concurrency = 1;
      name = Job.DEFAULT_JOB_NAME;
      break;
    case 2: // (string, function) or (string, string) or (number, function) or (number, string)
      handler = concurrency;
      if (typeof name === 'string') {
        concurrency = 1;
      } else {
        concurrency = name;
        name = Job.DEFAULT_JOB_NAME;
      }
      break;
  }

  this.setHandler(name, handler);

  return this._initProcess().then(() => {
    return this.start(concurrency);
  });
};
  • 再看setHandler 将自定义的任务处理方法绑定至监听事件中
Queue.prototype.setHandler = function(name, handler) {
  if (!handler) {
    throw new Error('Cannot set an undefined handler');
  }
  if (this.handlers[name]) {
    throw new Error('Cannot define the same handler twice ' + name);
  }

  this.setWorkerName();

  if (typeof handler === 'string') {
    // 当处理方法定义在另一个文件中
    ...
  } else {
    handler = handler.bind(this);

    if (handler.length > 1) {
      this.handlers[name] = promisify(handler);
    } else {
      this.handlers[name] = function() {
        try {
          return Promise.resolve(handler.apply(null, arguments));
        } catch (err) {
          return Promise.reject(err);
        }
      };
    }
  }
};
  • 再看 _initProcess, 为注册监听
Queue.prototype._initProcess = function() {
  if (!this._initializingProcess) {
    this.delayedTimestamp = Number.MAX_VALUE;
    this._initializingProcess = this.isReady()  
      .then(() => {
        return this._registerEvent('delayed');
      })
      .then(() => {
        return this.updateDelayTimer();
      });

    this.errorRetryTimer = {};
  }

  return this._initializingProcess;
};
  • start方法调用了run,如下其实质是调用了processJobs
Queue.prototype.run = function(concurrency) {
  const promises = [];

  return this.isReady()
    .then(() => {
      return this.moveUnlockedJobsToWait();
    })
    .then(() => {
      return utils.isRedisReady(this.bclient);
    })
    .then(() => {
      while (concurrency--) {
        promises.push(
          new Promise(resolve => {
            this.processJobs(concurrency, resolve);
          })
        );
      }

      this.startMoveUnlockedJobsToWait();

      return Promise.all(promises);
    });
};
  • processJobs 调用 _processJobOnNextTick
Queue.prototype.processJobs = function(index, resolve, job) {
  const processJobs = this.processJobs.bind(this, index, resolve);
  process.nextTick(() => {
    this._processJobOnNextTick(processJobs, index, resolve, job);
  });
};
  • _processJobOnNextTick 调用 processJob
Queue.prototype._processJobOnNextTick = function(
  processJobs,
  index,
  resolve,
  job
) {
  if (!this.closing) {
    (this.paused || Promise.resolve())
      .then(() => {
        const gettingNextJob = job ? Promise.resolve(job) : this.getNextJob();  // 获取job

        return (this.processing[index] = gettingNextJob
          .then(this.processJob)                                        // 调用processJob
          .then(processJobs, err => {
            this.emit('error', err, 'Error processing job');
            clearTimeout(this.errorRetryTimer[index]);
            this.errorRetryTimer[index] = setTimeout(() => {
              processJobs();
            }, this.settings.retryProcessDelay);

            return null;
          }));
      })
      .catch(err => {
        this.emit('error', err, 'Error processing job');
      });
  } else {
    resolve(this.closing);
  }
};
  • 再看看getNextJob, 利用brpoplpush来获取消息,超时时间为 DrainDelay
Queue.prototype.getNextJob = function() {
  if (this.closing) {
    return Promise.resolve();
  }

  if (this.drained) {
    //
    // Waiting for new jobs to arrive
    //
    return this.bclient
      .brpoplpush(this.keys.wait, this.keys.active, this.settings.drainDelay)
      .then(
        jobId => {
          if (jobId) {
            return this.moveToActive(jobId);
          }
        },
        err => {
          // Swallow error
          if (err.message !== 'Connection is closed.') {
            console.error('BRPOPLPUSH', err);
          }
        }
      );
  } else {
    return this.moveToActive();
  }
};
  • 再看 processJob
    根据事件循环获取事件,并用绑定的handler去处理该任务
Queue.prototype.processJob = function(job, notFetch = false) {
  let lockRenewId;
  let timerStopped = false;

  if (!job) {
    return Promise.resolve();
  }
  ...
  const handleCompleted = result => {
    return job.moveToCompleted(result, undefined, notFetch).then(jobData => {
      this.emit('completed', job, result, 'active');
      return jobData ? this.nextJobFromJobData(jobData[0], jobData[1]) : null;
    });
  };

  const handleFailed = err => {
    const error = err;

    return job.moveToFailed(err).then(jobData => {
      this.emit('failed', job, error, 'active');
      return jobData ? this.nextJobFromJobData(jobData[0], jobData[1]) : null;
    });
  };

  lockExtender();
  const handler = this.handlers[job.name] || this.handlers['*'];

  if (!handler) {
    return handleFailed(
      new Error('Missing process handler for job type ' + job.name)
    );
  } else {
    let jobPromise = handler(job);     // 调用绑定好的handler

    if (timeoutMs) {
      jobPromise = pTimeout(jobPromise, timeoutMs);
    }

    // Local event with jobPromise so that we can cancel job.
    this.emit('active', job, jobPromise, 'waiting');

    return jobPromise
      .then(handleCompleted)
      .catch(handleFailed)
      .finally(() => {
        stopTimer();
      });
  }
};

在执行完processJob后有重复执行processJobs, 就是一个循环,如下图:


image.png

Listeners 监听者

应用示例如下:

const myFirstQueue = new Bull('my-first-queue');

// Define a local completed event
myFirstQueue.on('completed', (job, result) => {
  console.log(`Job completed with result ${result}`);
})

你可能感兴趣的:(bull 源码阅读笔记)