bull 源碼閱讀筆記

是一個(gè)基于Redis的強(qiáng)大的Nodejs隊(duì)列框架
git地址:https://github.com/OptimalBits/bull
下圖是其與其他隊(duì)列庫的對(duì)比圖涌韩,可以看出Bull支持優(yōu)先度,并發(fā)不瓶,延遲任務(wù)钠四,全局事件甘改,頻率限制,重復(fù)任務(wù)美尸,原子操作鞠柄,可視化頁面等功能。

image.png

三個(gè)角色

一個(gè)隊(duì)列Queue有三個(gè)角色:任務(wù)生產(chǎn)者械馆, 任務(wù)消費(fèi)者胖眷, 事件監(jiān)聽者
一般生產(chǎn)者和消費(fèi)者被分為不同的實(shí)例,生產(chǎn)者可以在沒有可用消費(fèi)者的情況下霹崎,依然可以往隊(duì)列中加入任務(wù)珊搀,Queue提供了異步通信。
另外可以讓一個(gè)或者多個(gè)消費(fèi)者從隊(duì)列中消費(fèi)任務(wù)尾菇。
worker,可以在相同或者不同的進(jìn)程境析,同一臺(tái)或者集群中運(yùn)行。
Redis作為一個(gè)中間者派诬,只要生產(chǎn)者消費(fèi)之可以連接到Redis劳淆,他們就可以分工合作處理任務(wù)

一個(gè)任務(wù)的生命周期

當(dāng)調(diào)用隊(duì)列的 add 的方法時(shí),任務(wù)將會(huì)有一個(gè)生命周期默赂,直到運(yùn)行成功或者失斉嫱摇(失敗的任務(wù)將會(huì)被重試,將會(huì)有一個(gè)新的生命周期)缆八,如下所示


image.png

當(dāng)任務(wù)被加到隊(duì)列后將會(huì)是 Wait(等待被處理)或者Delayed狀態(tài) (延遲被處理)曲掰。Delayed狀態(tài)的任務(wù)不會(huì)直接被處理,而是到指定時(shí)間后奈辰,排到wait隊(duì)列后栏妖,等到worker空閑后處理
接下來是 Active 狀態(tài),表示任務(wù)正在被處理奖恰。當(dāng)處理完成任務(wù)就會(huì)進(jìn)入 completed 狀態(tài)吊趾,或者完成失敗進(jìn)入 Fail 狀態(tài)。
任務(wù)在被Active激活的時(shí)候會(huì)獲取鎖(防止一個(gè)任務(wù)被多個(gè)消費(fèi)者消費(fèi)),completed時(shí)候會(huì)釋放鎖瑟啃。但是鎖有過期時(shí)間(lockDuration)

producer 任務(wù)生產(chǎn)者

生產(chǎn)者就是趾徽,往隊(duì)列里面增加任務(wù),應(yīng)用示例如下:

// 1.創(chuàng)建隊(duì)列
const myFirstQueue = new Bull('my-first-queue');
// 2.往隊(duì)列增加任務(wù)
const job = await myFirstQueue.add({
  foo: 'bar'
});

底層代碼內(nèi)容包括以下幾個(gè)步驟:
1 創(chuàng)建隊(duì)列
2 增加任務(wù)

  • 先看 queue.add翰守,不管是單個(gè)任務(wù)還是重復(fù)性任務(wù)孵奶,最終都都調(diào)用了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);  // 后面也是增加了一個(gè)延時(shí)的job, Job.create(....)
    });
  } else {
    return Job.create(this, name, data, opts);
  }
};
  • job.create 調(diào)用了 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里面是調(diào)用了lua腳本, 增加wait 以及 delayed 隊(duì)列蜡峰,還 publish 進(jìn)行發(fā)布事件

增加任務(wù)后了袁,根據(jù)lua,redis會(huì)增加相應(yīng)的值, 并發(fā)布消息朗恳。

  • 任務(wù)增加成功后,wait 隊(duì)列以及 delayed 隊(duì)列會(huì)增加數(shù)據(jù)

單項(xiàng)任務(wù)如下所示:
會(huì)增加一個(gè)redis值载绿,類型為hash粥诫,表示一個(gè)任務(wù)
key = redis鍵前綴: 隊(duì)列名字: 編號(hào)
value = 相應(yīng)任務(wù)配置


image.png

重復(fù)任務(wù)如下所示,會(huì)修改redis兩個(gè)值
1: repeat隊(duì)列,類型為zset崭庸,表示可重復(fù)任務(wù)隊(duì)列
key = redis鍵前綴: 隊(duì)列名字:'repeat'
value = 各個(gè)任務(wù)怀浆,score為相應(yīng)任務(wù)的下一次執(zhí)行的時(shí)間戳


image.png

2 類型hash,表示單個(gè)任務(wù)的具體信息:
redis鍵前綴:隊(duì)列名字:'repeat:' + md5(name + jobId + namespace) + ':' + nextMillis


image.png

consumer 任務(wù)消費(fèi)者

是指處理隊(duì)列中的定時(shí)任務(wù)怕享,應(yīng)用示例如下:

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

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

process 方法會(huì)在有任務(wù)要執(zhí)行且worker空閑的時(shí)候被調(diào)用

底層代碼內(nèi)容包括以下幾個(gè)步驟:
1 綁定處理方法handler
2 增加任務(wù)

  • queue.process 先看process當(dāng)中主要調(diào)用了 setHandler(綁定handler) _initProcess(注冊(cè)監(jiān)聽) 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 將自定義的任務(wù)處理方法綁定至監(jiān)聽事件中
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') {
    // 當(dāng)處理方法定義在另一個(gè)文件中
    ...
  } 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执赡, 為注冊(cè)監(jiān)聽
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方法調(diào)用了run,如下其實(shí)質(zhì)是調(diào)用了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 調(diào)用 _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 調(diào)用 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)                                        // 調(diào)用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來獲取消息函筋,超時(shí)時(shí)間為 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
    根據(jù)事件循環(huán)獲取事件沙合,并用綁定的handler去處理該任務(wù)
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);     // 調(diào)用綁定好的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();
      });
  }
};

在執(zhí)行完processJob后有重復(fù)執(zhí)行processJobs, 就是一個(gè)循環(huán),如下圖:


image.png

Listeners 監(jiān)聽者

應(yīng)用示例如下:

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

// Define a local completed event
myFirstQueue.on('completed', (job, result) => {
  console.log(`Job completed with result ${result}`);
})
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末跌帐,一起剝皮案震驚了整個(gè)濱河市首懈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌谨敛,老刑警劉巖究履,帶你破解...
    沈念sama閱讀 217,734評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異脸狸,居然都是意外死亡最仑,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門肥惭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人紊搪,你說我怎么就攤上這事蜜葱。” “怎么了耀石?”我有些...
    開封第一講書人閱讀 164,133評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵牵囤,是天一觀的道長。 經(jīng)常有香客問我滞伟,道長揭鳞,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評(píng)論 1 293
  • 正文 為了忘掉前任梆奈,我火速辦了婚禮野崇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘亩钟。我一直安慰自己乓梨,他們只是感情好鳖轰,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著扶镀,像睡著了一般蕴侣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上臭觉,一...
    開封第一講書人閱讀 51,462評(píng)論 1 302
  • 那天昆雀,我揣著相機(jī)與錄音,去河邊找鬼蝠筑。 笑死狞膘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的菱肖。 我是一名探鬼主播客冈,決...
    沈念sama閱讀 40,262評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼稳强!你這毒婦竟也來了场仲?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤退疫,失蹤者是張志新(化名)和其女友劉穎渠缕,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體褒繁,經(jīng)...
    沈念sama閱讀 45,587評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡亦鳞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了棒坏。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片燕差。...
    茶點(diǎn)故事閱讀 39,919評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖坝冕,靈堂內(nèi)的尸體忽然破棺而出徒探,到底是詐尸還是另有隱情,我是刑警寧澤喂窟,帶...
    沈念sama閱讀 35,635評(píng)論 5 345
  • 正文 年R本政府宣布测暗,位于F島的核電站,受9級(jí)特大地震影響磨澡,放射性物質(zhì)發(fā)生泄漏碗啄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評(píng)論 3 329
  • 文/蒙蒙 一稳摄、第九天 我趴在偏房一處隱蔽的房頂上張望稚字。 院中可真熱鬧,春花似錦厦酬、人聲如沸尉共。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽袄友。三九已至殿托,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間剧蚣,已是汗流浹背支竹。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鸠按,地道東北人礼搁。 一個(gè)月前我還...
    沈念sama閱讀 48,048評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像目尖,于是被迫代替她去往敵國和親馒吴。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評(píng)論 2 354