laravel-async-queueでLaravelの非同期キューを小さく使い始める

Laravelでキューを使うにはキューワーカーを立てる必要がありますが、キューワーカーのプロセス監視方法や、キューワーカーを使用しているアプリケーションのデプロイ方法を考える必要があり、結構面倒臭いです。
キューワーカーが不要な同期キュードライバもありますが、これだと非同期処理の旨味がありません。

## laravel-async-queue

https://github.com/barryvdh/laravel-async-queue

laravel-async-queueというパッケージが非同期キュードライバを提供しています。
この非同期キュードライバを使うと、ジョブをバックグラウンドプロセスで即時に実行できるようになり、キューワーカーを立てることなく非同期処理の旨味を得ることができます。

## インストール方法

なお、この記事では下記のバージョンのソフトウェアを使用しています。

| ソフトウェア | バージョン |
| --- | --- |
| PHP | 7.2.12 |
| Laravel | 5.5.44 | 
| laravel-async-queue | v0.7.3 |

laravel-async-queueをComposerでインストールします。

```
composer require barryvdh/laravel-async-queue
```

`config/app.php`にサービスプロバイダを追加します。

```php:config/app.php
Barryvdh\Queue\AsyncServiceProvider::class,
```

`config/queue.php`の`default`オプションを`async`に変更します。

```php:config/queue.php
'default' => 'async', 
```

さらに`connections`オプションに非同期キュードライバ用の設定を追加します。

```php:config/queue.php
'async' => [
    'driver' => 'async',
    'table' => 'jobs',
    'queue' => 'default',
    'expire' => 60,
],
```

## 動作テスト

動作テストをしてみます。

テスト用のコントローラとイベントリスナーとイベントを用意します。

```php:app/Http/Controllers/TestController.php
<?php

namespace App\Http\Controllers;

use App\Events\TestEvent;
use App\Http\Controllers\Controller;
use Log;

class TestController extends Controller
{
    public function test()
    {
        Log::debug('1');
        event(new TestEvent());
        Log::debug('3');
    }
}
```

```php:app/Listeners/TestListener.php
<?php

namespace App\Listeners;

use App\Events\TestEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Log;

class TestListener implements ShouldQueue
{
    public function handle(TestEvent $event)
    {
        Log::debug('2')
    }
}
```

```php:app/Events/TestEvent
<?php

namespace App\Events;

class TestEvent
{
}
```

```php:app/Providers/EventServiceProvider.php 
protected $listen = [
    'App\Events\TestEvent' => [
        'App\Listeners\TestListener',
    ],
];
```

コントローラのアクションを実行すると、下記のようなログが出力されます。

```
[2020-02-13 23:36:45] local.APP.DEBUG: 1 [] {"uid":"93eb38d","process_id":1017}
[2020-02-13 23:36:45] local.APP.DEBUG: 3 [] {"uid":"93eb38d","process_id":1017}
[2020-02-13 23:36:47] local.APP.DEBUG: 2 [] {"uid":"535fd9a","process_id":1046}
```

1→3→2となっており、非同期処理されていることがわかります。
プロセスIDとUIDもコントローラとイベントリスナーで別になっています。

ちなみに同期ドライバの場合です。

```
[2020-02-13 23:38:22] local.APP.DEBUG: 1 [] {"uid":"a9173e4","process_id":1020}
[2020-02-13 23:38:22] local.APP.DEBUG: 2 [] {"uid":"a9173e4","process_id":1020}
[2020-02-13 23:38:22] local.APP.DEBUG: 3 [] {"uid":"a9173e4","process_id":1020}
```

1→2→3となっており、同期処理です。
もちろんプロセスIDとUIDもコントローラとイベントリスナーで同じです。

## 失敗ジョブを扱えるようにする

非同期キュードライバは、キューワーカーを使っていないため、ジョブが1回失敗すると自動ではリトライされません。

また、ジョブ失敗ベントが発行されず、失敗したジョブは`jobs`テーブルに残ったままで、`failed_jobs`テーブルに入りません。

これだと運用がつらいので、ジョブ失敗をSlackに通知したりできるように、ジョブ失敗イベントが発行されるようにします。

イベントリスナーの`$tires`プロパティを`1`に設定すると、ジョブ失敗ベントが発行されるようになります[^1]。

[^1]: https://github.com/barryvdh/laravel-async-queue/issues/57


```php
class TestListener implements ShouldQueue
{
    public $tires = 1;
    ...
}
```

トレイトにしておくと便利かもしれません。

```php
trait AsyncQueueable
{
    public $tires = 1;
}
```
```php
class TestListener implements ShouldQueue
{
    use AsyncQueueable;
    ...
}
```

これでジョブ失敗イベントが発生するようになります。
同時に、失敗ジョブが`jobs`テーブルから消えるようになります。

ただ、このままだと失敗ジョブが`jobs`テーブルにも`failed_jobs`テーブルにも残らなくなってしまうので、失敗ジョブを`failed_jobs`テーブルに入れる処理をジョブ失敗イベントのコールバックとして書くことにします。
これは`AppServiceProvider`の`boot`メソッドに書くのがいいと思います。

```php:app/Providers/AppServiceProvider
\Illuminate\Support\Facades\Queue::failing(function (\Illuminate\Queue\Events\JobFailed $event) {
    if ($event->job->getConnectionName() == 'async') {
        // 失敗ジョブをfaled_jobsテーブルに保存する
        $id = $this->app['queue.failer']->log(
            $event->connectionName,
            $event->job->getQueue(),
            $event->job->getRawBody(),
            $event->exception
        );
    }
    // Slackへの通知など
    ...
});
```

ちなみに、`log`メソッドは`failed_jobs`テーブルのIDが返ってきます。
Slackに通知する時に使うと、後でジョブを手動でリトライする際に便利でいいと思います。

これで失敗ジョブが`failed_jobs`テーブルに入るようになります。

リトライが必要な場合は、下記のArtisanコマンドを実行します。

```
artisan queue:retry <failed_jobsテーブルのID>
```

これで`failed_jobs`テーブルの失敗ジョブが`jobs`テーブルに戻ります。

さらに下記のArtisanコマンドを実行するとジョブを実行できます。

```
artisan queue:async <jobsテーブルのID>
```