queue:work 原理分析

Epona發表於2019-05-17

最近在專案中使用了佇列,因此研究一下相關的原始碼。本文只是粗略的進行分析,如果發現了錯誤歡迎大家討論交流。

基本實現

php artisan queue:work 的程式碼實現是在Illuminate\Queue\Console\WorkCommand中。那麼,讓我們看一下它是怎樣處理的:

public function handle()
{
    if ($this->downForMaintenance() && $this->option('once')) {
        return $this->worker->sleep($this->option('sleep'));
    }

    $this->listenForEvents();

    $connection = $this->argument('connection') ?: $this->laravel['config']['queue.default'];

    $queue = $this->getQueue($connection);

    $this->runWorker($connection, $queue);  
}

protected function runWorker($connection, $queue)
{
    // 設定快取
    $this->worker->setCache($this->laravel['cache']->driver());

    return $this->worker->{$this->option('once') ? 'runNextJob' : 'daemon'}(
        $connection, $queue, $this->gatherWorkerOptions()
    );

上面的程式碼邏輯很簡單,程式碼的核心邏輯在最後一行runWorker中,根據傳參的不同,執行方法可能為 runNextJob 或者 daemon,由於本文的目的為研究原理,故只分析 daemon 方法。

daemon方法

daemon 方法存在於Illuminate\Queue\Worker中,具體如下:

public function daemon($connectionName, $queue, WorkerOptions $options)
{
    $this->listenForSignals();

    $lastRestart = $this->getTimestampOfLastQueueRestart();

    while(true) {
        if(! $this->daemonShouldRun($option, $connectionName, $queue)) {
            $this->pauseWorker($options, $lastRestart);

            continue;
        }

        $job = $this->getNextJob(
            $this->manager->connection($connectionName), $queue
        );

        $this->registerTimeoutHandler($job, $options);

        if ($job) {
            $this->runJob($job, $connectionName, $options);
        } else {
            $this->sleep($options->sleep);
        }

        $this->stopIfNecessary($options, $lastRestart);
    }
}

下面讓我們一步一步來分析其中的程式碼邏輯。

監聽訊號

protected function listenForSignals()
{
    if ($this->supportsAsyncSignals()) {
        pcntl_async_signals(true);

        pcntl_signal(SIGTERM, function () {
            $this->shouldQuit = true;
        });

        pcntl_signal(SIGUSR2, function () {
            $this->paused = true;
        });

        pcntl_signal(SIGCONT, function () {
            $this->paused = false;
        });
    }
}

這段程式碼主要用到了 PHP 的 pcntl 擴充套件,具體的介紹和使用方法請看 官方文件 以及 這裡

此擴充套件的主要用於程式控制,根據相關的程式訊號值設定對應的引數。

獲取上一次重啟時間

   protected function getTimestampOfLastQueueRestart()                                                                                    
{                                                                                                                                      
     if ($this->cache) {                                                                                                                
         return $this->cache->get('illuminate:queue:restart');                                                                          
     }                                                                                                                                  
}

當上一步之行完之後,應用會獲取上一次的佇列重啟時間戳,即執行命令 php artisan queue:restart 時的時間戳。此時,如果快取使用Redis的話,使用 redis-cli 登入後並輸入 monitor 然後執行的話(monitor 命令用於實時監控 redis 的操作),會看到如下的顯示,

1434697488.632958 [0 127.0.0.1:60136] "GET" "laravel:illuminate:queue:restart"
1434697491.634111 [0 127.0.0.1:60136] "GET" "laravel:illuminate:queue:restart"
1434697494.635239 [0 127.0.0.1:60136] "GET" "laravel:illuminate:queue:restart"
1434697497.636391 [0 127.0.0.1:60136] "GET" "laravel:illuminate:queue:restart"
1434697500.637753 [0 127.0.0.1:60136] "GET" "laravel:illuminate:queue:restart"
1434697503.639073 [0 127.0.0.1:60136] "GET" "laravel:illuminate:queue:restart"
1434697506.640155 [0 127.0.0.1:60136] "GET" "laravel:illuminate:queue:restart"
1434697509.641288 [0 127.0.0.1:60136] "GET" "laravel:illuminate:queue:restart"
1434697512.642365 [0 127.0.0.1:60136] "GET" "laravel:illuminate:queue:restart"

這表示我們正在不斷的獲取時間戳。

判斷是否需要執行佇列

接著在一個無限的迴圈中,我們判斷是否可以執行佇列,如果答案為否,那麼我們將其進行休眠( PHP 中的 sleep 方法),如有必要也可以將對應的程式殺掉(stopIfNecessary 方法)。

執行佇列

如果佇列可以執行,那麼程式碼將會執行對應的佇列。

protected function runJob($job, $connectionName, WorkerOptions $options)
{
    try {
        return $this->process($connectionName, $job, $options);
    } catch (\Exception $e) {
        $this->exceptions->report($e);

        $this->stopWorkerIfLostConnection($e);
    } catch (Throwable $e) {
        $this->exceptions->report($e = new FatalThrowableError($e));

        $this->stopWorkerIfLostConnection($e);
    }
}

public function process($connectionName, $job, WorkerOptions $options)
{
    $this->raiseBeforeJobEvent($connectionName, $job);

    $this->markJobAsFailedIfAlreadyExceedsMaxAttempts(
        $connectionName, $job, (int) $options->maxTries
    );

    $job->fire();

    $this->raiseAfterJobEvent($connectionName, $job);

    // 省略了錯誤處理部分的程式碼
}

我們可以看到runJob的邏輯很簡單,程式碼進行佇列處理(process 方法),如果碰到錯誤或者異常將丟擲對應的異常。在 process 方法中首先觸發事件,然後判斷是否達到最大嘗試次數。如果沒有,則開始佇列處理,fire 方法會根據我們使用那種佇列(Redis,Beanstalkd等)來進行對應的實現;

佇列執行

// Illuminate\Queue\Jobs\Job;
public function fire()
{
    $payload = $this->payload();

    list($class, $method) = JobName::parse($payload['job]);

    ($this->instance = $this->resolve($class))->{$method}{$this, $payload['dada']};
}

在 這裡 我們通過 payload來獲取要執行的物件和方法,其中 payload 內容如下:

[2019-05-16 11:02:04] local.INFO: array (
  'displayName' => 'App\\Jobs\\TestJob',
  'job' => 'Illuminate\\Queue\\CallQueuedHandler@call',
  'maxTries' => NULL,
  'timeout' => NULL,
  'timeoutAt' => NULL,
  'data' => 
  array (
    'commandName' => 'App\\Jobs\\TestJob',
    'command' => 'O:16:"App\\Jobs\\TestJob":7:{s:6:"' . "\0" . '*' . "\0" . 'job";N;s:10:"connection";N;s:5:"queue";N;s:15:"chainConnection";N;s:10:"chainQueue";N;s:5:"delay";N;s:7:"chained";a:0:{}}',
  ),
)  

因此,fire 最後會使用 Illuminate\Queue\CallQueueHandler類中的call方法。程式碼如下:

public function call(Job $job, array $data)
{
    try {
        $command = $this->setJobInstanceIfNecessary(
            $job, unserialize($data['command'])
        );
    } catch (ModelNotFoundException $e) {
        return $this->handleModelNotFound($job, $e);
    }

    $this->dispatcher->dispatchNow(
        $command, $this->resolveHandler($job, $command)
    );

    // 其餘錯誤處理程式碼省略
}

在這裡我們首先設定好 Job 例項,然後處理對應的命令和錯誤,至此,queue:work 的基本原理已經分析完畢。

There's nothing wrong with having a little fun.

相關文章