啟動指令
php artisan queue:work
啟動檔案
namespace Illuminate\Queue\Console;
use Illuminate\Queue\Worker;
use Illuminate\Support\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Queue\Job;
use Illuminate\Queue\WorkerOptions;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
class WorkCommand extends Command
{
...
/**
* @var \Illuminate\Queue\Worker
*/
protected $worker;
public function __construct(Worker $worker)
{
parent::__construct();
$this->worker = $worker;
}
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
);
}
...
}
複製程式碼
我們先從建構函式和 handle()
方法開始分析,這是入口。
片段一:判斷是否維護模式或者
--force
強制啟動
if ($this->downForMaintenance() && $this->option('once')) {
return $this->worker->sleep($this->option('sleep'));
}
複製程式碼
片段二:通過事件繫結在控制檯輸出資訊
$this->listenForEvents();
protected function listenForEvents()
{
$this->laravel['events']->listen(JobProcessing::class, function ($event) {
$this->writeOutput($event->job, 'starting');
});
$this->laravel['events']->listen(JobProcessed::class, function ($event) {
$this->writeOutput($event->job, 'success');
});
$this->laravel['events']->listen(JobFailed::class, function ($event) {
$this->writeOutput($event->job, 'failed');
$this->logFailedJob($event);
});
}
複製程式碼
片段三:通過配置檔案中配置的驅動獲取對應驅動的佇列名,如果沒有則返回
default
$connection = $this->argument('connection') ?: $this->laravel['config']['queue.default'];
$queue = $this->getQueue($connection);
protected function getQueue($connection)
{
return $this->option('queue') ?: $this->laravel['config']->get(
"queue.connections.{$connection}.queue", 'default'
);
}
複製程式碼
片段四:傳入連線驅動和佇列名稱到
runWorker
方法執行任務。
$this->runWorker(
$connection, $queue
);
複製程式碼
這裡是啟動的重點,我們傳入的 $connection = 'redis' $queue = 'default'
,繼續分析
protected function runWorker($connection, $queue)
{
// "這裡的 $this->laravel['cache'] 是 Illuminate\Cache\CacheManager 類的例項。
(是在 app.providers.Illuminate\Cache\CacheServiceProvider::class 註冊的)
$this->laravel['cache']->driver() 返回 Illuminate\Cache\Repository 類的例項。"
// "框架通過 CacheManager 對很多儲存管理進行了統一。
可以通過修改 app.config.cache.default 和 `app.config.cache.stores 中的值來修改儲存驅動。"
// "將獲取的驅動賦值給 workder 的 cache成員"
$this->worker->setCache($this->laravel['cache']->driver());
// "當 worker 物件擁有了cache物件之後便擁有了操作對應資料的能力 !"
return $this->worker->{$this->option('once') ? 'runNextJob' : 'daemon'}(
$connection, $queue, $this->gatherWorkerOptions()
);
}
複製程式碼
繼續執行
return $this->worker->{$this->option('once') ? 'runNextJob' : 'daemon'}(
$connection, $queue, $this->gatherWorkerOptions()
);
複製程式碼
這裡傳入的引數分別是,可以看出都是對佇列消費的一些基本設定。
當執行模式非 --once
的情況下就會以 daemon
的方式執行。
我們看
\Illuminate\Queue\Worker
物件的 daemon
方法即可
守護程式模式
public function daemon($connectionName, $queue, WorkerOptions $options)
{
if ($this->supportsAsyncSignals()) {
$this->listenForSignals();
}
$lastRestart = $this->getTimestampOfLastQueueRestart();
while (true) {
if (! $this->daemonShouldRun($options, $connectionName, $queue)) {
$this->pauseWorker($options, $lastRestart);
continue;
}
$job = $this->getNextJob(
$this->manager->connection($connectionName), $queue
);
if ($this->supportsAsyncSignals()) {
$this->registerTimeoutHandler($job, $options);
}
if ($job) {
$this->runJob($job, $connectionName, $options);
} else {
$this->sleep($options->sleep);
}
$this->stopIfNecessary($options, $lastRestart, $job);
}
}
複製程式碼
程式引數設定
先設定程式的一些管理引數
if ($this->supportsAsyncSignals()) { // extension_loaded('pcntl'); 是否支援 'pcntl' 擴充,支援多程式的擴充。
$this->listenForSignals();
}
protected function listenForSignals()
{
// "PHP7.1訊號新特性 -- 開啟非同步訊號處理"
pcntl_async_signals(true);
// "安裝訊號處理器,後面可以傳入相應的訊號來終止或其他操作"
pcntl_signal(SIGTERM, function () {
// "SIGTERM 終止程式 軟體終止訊號"
$this->shouldQuit = true;
});
pcntl_signal(SIGUSR2, function () {
// "SIGUSR2 終止程式 使用者定義訊號2"
$this->paused = true;
});
pcntl_signal(SIGCONT, function () {
// "SIGCONT 忽略訊號 繼續執行一個停止的程式"
$this->paused = false;
});
}
複製程式碼
關於
pcntl
的用法可以參考 PCNTL
訊號可以參考對照表
接著看,從 cache
中獲取上一次重啟的時間戳
$lastRestart = $this->getTimestampOfLastQueueRestart();
複製程式碼
迴圈任務執行
判斷是否終止執行
if (! $this->daemonShouldRun($options, $connectionName,$queue)) {
// "$opions 就是 呼叫artisan 傳入的引數
$connectionName 我用了redis驅動,所有就是 'redis'
$queue 這裡沒有傳入佇列則是 'default'"
$this->pauseWorker($options, $lastRestart);
continue;
}
複製程式碼
下面程式碼一共三個判斷:
1.是否是關站模式並且非強制執行。
2.是否有外部傳入的暫停訊號
3.是否有繫結 Looping
事件執行並返回結果
如果符合條件則暫停或者傳送終止訊號。
主要功能是為了控制是否繼續執行任務。
protected function daemonShouldRun(WorkerOptions $options, $connectionName, $queue)
{
return ! (($this->manager->isDownForMaintenance() && ! $options->force) ||
$this->paused ||
$this->events->until(new Events\Looping($connectionName, $queue)) === false);
}
複製程式碼
獲取待執行的 Job
// "$this->manager->connection($connectionName) 是 Illuminate\Queue\RedisQueue 物件
$queue : 'default'"
$job = $this->getNextJob(
$this->manager->connection($connectionName), $queue
);
複製程式碼
繼續看 getNextJob
protected function getNextJob($connection, $queue)
{
try {
foreach (explode(',', $queue) as $queue) {
if (! is_null($job = $connection->pop($queue))) {
return $job;
}
}
} catch (Exception $e) {
// "異常處理主要是報告異常"
// "設定 '$this->shouldQuit = true;' 後續就會終止"
$this->exceptions->report($e);
$this->stopWorkerIfLostConnection($e);
$this->sleep(1);
} catch (Throwable $e) {
$this->exceptions->report($e = new FatalThrowableError($e));
$this->stopWorkerIfLostConnection($e);
$this->sleep(1);
}
}
複製程式碼
上面分析過了 $connection
是 RedisQueue
物件,所有展開 RedisQueue
的 pop
方法,獲取要執行的任務物件。
public function pop($queue = null)
{
$this->migrate($prefixed = $this->getQueue($queue));
if (empty($nextJob = $this->retrieveNextJob($prefixed))) {
return;
}
[$job, $reserved] = $nextJob;
if ($reserved) {
return new RedisJob(
$this->container, $this, $job,
$reserved, $this->connectionName, $queue ?: $this->default
);
}
}
複製程式碼
遷移延遲佇列
在 pop
的過程中首先遷移延遲佇列的相關資料
protected function migrate($queue)
{
// "這裡是不是很熟悉了,上一章儲存端分析的時候延遲"
// "佇列就是用的這個key來存的"
// "將延遲的佇列遷移到主佇列"
$this->migrateExpiredJobs($queue.':delayed', $queue);
// "將過期佇列遷移到主佇列"
if (! is_null($this->retryAfter)) {
$this->migrateExpiredJobs($queue.':reserved', $queue);
}
}
複製程式碼
繼續看如何遷移到主佇列的
public function migrateExpiredJobs($from, $to)
{
return $this->getConnection()->eval(
LuaScripts::migrateExpiredJobs(),
2,
$from,
$to,
$this->currentTime()
);
}
public static function migrateExpiredJobs()
{
return <<<'LUA'
if(next(val) ~= nil) then
redis.call('zremrangebyrank', KEYS[1], 0, #val - 1)
for i = 1, #val, 100 do
redis.call('rpush', KEYS[2], unpack(val, i, math.min(i+99, #val)))
end
end
return val
LUA;
}
複製程式碼
最終通過 eval
命令使用 Lua
直譯器執行指令碼。
請看 Redis Eval
真香,這僅僅是把延遲任務切回主佇列,繼續!
檢索資料
從佇列檢索下一個 Job
if (empty($nextJob = $this->retrieveNextJob($prefixed))) {
return; // 沒有資料就返回
}
複製程式碼
展開檢索程式碼
protected function retrieveNextJob($queue)
{
// "預設值是 null"
if (! is_null($this->blockFor)) {
return $this->blockingPop($queue);
}
// "這段是直接通過 lua 從 redis lpop出物件,"
// "在lua中完成封裝,執行邏輯和 blockingPop 相似"
return $this->getConnection()->eval(
LuaScripts::pop(), 2, $queue, $queue.':reserved',
$this->availableAt($this->retryAfter)
);
}
複製程式碼
我們主要看 blockingPop
的程式碼
protected function blockingPop($queue)
{
// "以阻塞的方式彈出佇列的第一個元素"
$rawBody = $this->getConnection()->blpop($queue, $this->blockFor);
// "解析獲取的資料,同時再封裝一個重試物件並寫入有序集合。"
if (! empty($rawBody)) {
$payload = json_decode($rawBody[1], true);
$payload['attempts']++;
$reserved = json_encode($payload);
$this->getConnection()->zadd($queue.':reserved', [
$reserved => $this->availableAt($this->retryAfter),
]);
return [$rawBody[1], $reserved];
}
return [null, null];
}
複製程式碼
檢索完成之後回到 pop
中繼續執行
public function pop($queue = null)
{
$this->migrate($prefixed = $this->getQueue($queue));
if (empty($nextJob = $this->retrieveNextJob($prefixed))) {
return;
}
// "到這裡了!"
[$job, $reserved] = $nextJob;
if ($reserved) {
return new RedisJob(
$this->container, $this, $job,
$reserved, $this->connectionName, $queue ?: $this->default
);
}
}
複製程式碼
我們來看看 $nextJob
是什麼
最後呼叫
return new RedisJob(
$this->container, $this, $job,
$reserved, $this->connectionName, $queue ?: $this->default
);
複製程式碼
看看 Illuminate\Queue\Jobs\RedisJob
的建構函式
public function __construct(Container $container, RedisQueue $redis, $job, $reserved, $connectionName, $queue)
{
$this->job = $job;
$this->redis = $redis;
$this->queue = $queue;
$this->reserved = $reserved;
$this->container = $container;
$this->connectionName = $connectionName;
$this->decoded = $this->payload();
}
複製程式碼
這應該是最後一層封裝,最後要返回給最外層的任務物件。
執行 Job
回到 Worker
物件中
...
$job = $this->getNextJob(
$this->manager->connection($connectionName), $queue
);
// "剛剛我們從 redis 中拿到了封裝好的 $job 物件,繼續執行"
// "$job 就是 Illuminate\Queue\Jobs\RedisJob 物件"
// "是否支援 pcntl 擴充,非同步模式傳遞訊號"
if ($this->supportsAsyncSignals()) {
// "設定超時訊號處理"
$this->registerTimeoutHandler($job, $options);
}
複製程式碼
繼續註冊超時訊號控制
protected function registerTimeoutHandler($job, WorkerOptions $options)
{
pcntl_signal(SIGALRM, function () {
$this->kill(1);
});
pcntl_alarm(
max($this->timeoutForJob($job, $options), 0)
);
}
複製程式碼
總算要到執行 Job
的部分了
if ($job) {
$this->runJob($job, $connectionName, $options);
} else {
// "不存在 $job 則睡眠,最低睡眠1秒"
$this->sleep($options->sleep);
}
複製程式碼
解析 runJob
到這一步我們已經拿到了所有的物件,接下來就是把 物件用起來!
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);
}
}
複製程式碼
展開
$this->process($connectionName, $job, $options);
複製程式碼
繼續展開
public function process($connectionName, $job, WorkerOptions $options)
{
try {
// "觸發任務執行前的繫結事件,從佇列刪除任務"
$this->raiseBeforeJobEvent($connectionName, $job);
// "標記超過最大重試次數的任務"
$this->markJobAsFailedIfAlreadyExceedsMaxAttempts(
$connectionName, $job, (int) $options->maxTries
);
$job->fire();
// "觸發任務執行後的繫結事件"
$this->raiseAfterJobEvent($connectionName, $job);
} catch (Exception $e) {
$this->handleJobException($connectionName, $job, $options, $e);
} catch (Throwable $e) {
$this->handleJobException(
$connectionName, $job, $options, new FatalThrowableError($e)
);
}
}
複製程式碼
$job->fire()
$job
=> Illuminate\Queue\Jobs\RedisJob
繼承了 Illuminate\Queue\Jobs\Job
所以呼叫了抽象父類的 fire()
方法
public function fire()
{
$payload = $this->payload();
[$class, $method] = JobName::parse($payload['job']);
($this->instance = $this->resolve($class))->{$method}($this, $payload['data']);
}
複製程式碼
我們看看 $payload
的結構實際就是 json_decode($job, true)
轉換後的[$class, $method]
分別是 Illuminate\Queue\CallQueuedHandler
和 call
最後就是從容器中解析出 Illuminate\Queue\CallQueuedHandler
物件並且呼叫 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)
);
if (! $job->hasFailed() && ! $job->isReleased()) {
$this->ensureNextJobInChainIsDispatched($command);
}
if (! $job->isDeletedOrReleased()) {
$job->delete();
}
}
複製程式碼
先看看 $command
獲取的是什麼
protected function setJobInstanceIfNecessary(Job $job, $instance)
{
if (in_array(InteractsWithQueue::class, class_uses_recursive($instance))) {
$instance->setJob($job);
}
return $instance;
}
複製程式碼
列印 class_uses_recursive($instance)
接著就呼叫了 $instance->setJob($job)
;
這裡的 $instance
就是對應我們自己編寫的任務物件。
執行完之後最終 $command
返回的就是自己編寫的類
將 RedisJob
和 $command
傳給 dispatchNow
方法
$this->dispatcher
是 Illuminate\Bus\Dispatcher
物件
$this->dispatcher->dispatchNow(
$command, $this->resolveHandler($job, $command)
);
複製程式碼
最後的真像
public function dispatchNow($command, $handler = null)
{
if ($handler || $handler = $this->getCommandHandler($command)) {
$callback = function ($command) use ($handler) {
// "劃重點,要考!"
return $handler->handle($command);
};
} else {
$callback = function ($command) {
return $this->container->call([$command, 'handle']);
};
}
return $this->pipeline->send($command)->through($this->pipes)->then($callback);
}
複製程式碼
其實費了那麼大的力氣,最後就是呼叫 $command->handle
回頭看看 job
的定義
就像煙火過後一樣,消失於無形。
最後
整體分析下來感覺使用 pcntl
擴充來做非同步訊號控制和程式中斷來實現終止迴圈是一個亮點!
至此完成了任務佇列消費端的分析,後續有機會分析 Horizon
是如何消費佇列的哈~