はじめに
Redis・Queues・Laravel Horizon。
1つは「使用しているプロジェクトに関わったことがある」、もしくは「ドキュメントを眺めたことがある」方は多くいらっしゃると思います。
逆に「1から自分で組み立てる必要がなかった」方も多いのではないでしょうか?
この記事では簡易メルマガ配信を題材に、3つを一から組み合わせて動きを体感してみます。
セットアップが終われば、以下を自分の目で確認できます。
- コマンドを叩いた瞬間に処理が返り、バックグラウンドでジョブが流れ始める
- Horizonダッシュボードでジョブが処理・失敗・リトライされる様子をリアルタイムに見られる
- ワーカーを強制終了しても、アプリ側のstatus管理により重複送信を防げる
さっそく始めましょう。
前提条件
- PHP 8.x + Composer
- Docker Desktop
- phpredis — PHPからRedisに接続するためのPHP拡張。Laravel Herd使用中であれば導入済みのためスキップ
- 未導入の方は
pecl install redisでインストール(PHPのバージョンによっては設定が手間とのことなので、筆者はLaravel Herd(無料) を使用しました)
- 未導入の方は
セットアップ
この記事で作るもの
題材は簡易メルマガ配信です。コマンドを1つ叩くと100件のメール送信ジョブがキューに積まれ、Horizonのワーカーがバックグラウンドで順番に処理します。
処理の流れと登場人物は以下のとおりです:
[SendTestMailsCommand]
│ mail:send-test を実行
│ 100件ぶん MailLog レコードを作成(status: pending)
│ SendFakeMailJob::dispatch() × 100 → Redis に積む
▼
[Redis キュー]
│ Horizon ワーカーが1件ずつ取り出す
▼
[SendFakeMailJob::handle()]
│ status を pending → sending に更新(重複防止)
│ Mailpit にメールを送信
│ status を sending → sent に更新
│
│ ※ id が 10 の倍数(id=10, 20, 30...)は意図的に失敗させる
│ → status を pending に戻して throw → Horizon がリトライ
│ → 3回失敗したら failed() が呼ばれ status を failed に更新
▼
[Mailpit] http://localhost:8025 で受信確認
まず3つのツールの役割を整理しておきます。
| ツール | 役割 |
|---|---|
| Laravel Queue | ジョブを「抽象的なキューAPI」として扱う仕組みで、ジョブ投入・取得・再試行などを統一インターフェースで提供する |
| Redis | Queue Driver の1つで、キューに積まれたジョブデータを高速にメモリに保持する |
| Laravel Horizon | Redisキューのワーカーを管理し、処理状況を可視化する |
3つがそろってはじめて「キューが動いている様子を目で見る」環境になります。順番にセットアップしていきます。
1. Laravelプロジェクトを作成する
composer create-project laravel/laravel test-redis-mail
cd test-redis-mail
以降のコマンドはすべてこの test-redis-mail/ ディレクトリで実行します。
2. Laravel Queue — 非同期処理の仕組み
Laravel Queueは「重い処理を今すぐやらず、あとでやっといて」と指示する仕組みです。
[Artisanコマンド / Webリクエスト]
│
│ SendFakeMailJob::dispatch() を呼ぶ
│ → ジョブがRedisに積まれ、呼び出し元にすぐ処理が返る
▼
[Redis キュー] ← ジョブが積まれて待機
│
│ Horizon ワーカーがバックグラウンドで取り出す
▼
[SendFakeMailJob::handle() が実行される] ← ここで実際にメールが送られる
通常のWebアプリはメール送信のような重い処理が入ると、終わるまでレスポンスが遅延します。Queueを使うとジョブをキューに積んで即座に返し、ワーカーがバックグラウンドで処理します。
.env を開き、QUEUE_CONNECTION の値を redis に変更します。
QUEUE_CONNECTION=redis
redis に変えることでRedisキューが有効になります。Horizon は Redis Queue 専用のため、QUEUE_CONNECTION=redis が必須です。
3. Redis と Mailpit を Docker で起動する
Redisはキューの「実体」です。ジョブの情報をメモリ上に保持し、ワーカーが順番に取り出して処理します。
Mailpitはローカルのメール受信サーバーです。実際には外部に送信されず、http://localhost:8025 でキャプチャして確認できます。
docker-compose.yml をプロジェクトルート(test-redis-mail/ 直下)にエディタで新規作成します:
services:
redis:
image: redis:latest
container_name: laravel-redis
ports:
- "6379:6379"
mailpit:
image: axllent/mailpit
container_name: laravel-mailpit
ports:
- "1025:1025" # SMTP
- "8025:8025" # Web UI
docker compose up -d # -d はバックグラウンドで起動するオプション
docker ps # STATUSが "Up" になっていれば起動成功
(※エラーが出る場合は他の起動中のコンテナと競合しているかもしれません)
続けて .env をエディタで開き、以下の設定を反映してください。DBはデフォルトのSQLiteがそのまま使えますが、別DBの用意がある場合は任意で変更してください。
-
CACHE_STOREはredisに書き換えてください -
REDIS_CLIENT・REDIS_HOST・REDIS_PORTはデフォルト値がすでに正しいはずですが、念のため確認してください -
MAIL_MAILER・MAIL_HOST・MAIL_PORT・MAIL_FROM_ADDRESS・MAIL_FROM_NAMEは以下の値に書き換えてください
CACHE_STORE=redis
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025
MAIL_FROM_ADDRESS=test@example.com
MAIL_FROM_NAME="Laravel Queue Test"
Redisに繋がっているか確認します。tinker はLaravelのREPL(対話的にPHPコードを実行できるツール)です:
php artisan tinker
$redis = new \Redis();
$redis->connect('127.0.0.1', 6379);
$redis->set('test', 'hello');
$redis->get('test'); // => "hello" と表示されれば疎通OK
$redis->del('test'); // => 1(削除した件数)
exit; // tinkerを終了してターミナルに戻る
phpredis 拡張のインスタンスを直接使って接続確認します。get('test') で "hello" が返れば疎通OKです。
4. Horizon — ワーカーの管理と可視化
HorizonはRedisキューのワーカーを管理し、処理状況をリアルタイムでダッシュボードに表示します。
composer require laravel/horizon # Horizonパッケージをインストール
php artisan horizon:install # config/horizon.php と管理画面のアセットを生成
この記事では .onQueue('emails') でジョブを emails キューに積むため、Horizonの監視対象を emails に変える必要があります。インストールで生成される config/horizon.php をエディタで開き、defaults → supervisor-1 の中にある queue の値を ['emails'] に変更して保存します:
// config/horizon.php
'defaults' => [
'supervisor-1' => [
'connection' => 'redis',
'queue' => ['emails'], // ← 'default' から変更
// ...
],
],
defaults はすべての環境に共通する設定テンプレートです。queue のようにどの環境でも同じ値を使うものはここに書きます。maxProcesses のように本番環境だけ増やしたい設定は、environments.production に上書きします。
5. mail_logs テーブルを作成する
メールの送信状態を管理するテーブルを作ります。1レコードが1件のメール送信に対応し、ジョブはこのレコードのIDを受け取って処理します。
status カラムは pending(投入直後)→ sending(ワーカーが処理中)→ sent(送信完了)と遷移し、リトライ上限に達した場合は failed になります。sending は単なる状態表示ではなく「このメールはいま誰かが処理中——他のワーカーは手を出すな」というアプリケーション側のロックとしても機能します。これが重複防止・クラッシュリカバリの核になります(後の実験2で詳しく見ます)。
php artisan make:migration create_mail_logs_table
生成されたマイグレーションファイルをエディタで開き、中身をすべて以下の内容に書き換えて保存します:
(※database/migrations/ 以下に xxxx_xx_xx_xxxxxx_create_mail_logs_table.php という名前で作成されます。xxxx は生成日時です)
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('mail_logs', function (Blueprint $table) {
$table->id();
$table->string('email');
$table->enum('status', [
'pending', // 処理待ち
'sending', // ワーカーが処理中(重複防止のマーカー)
'sent', // 送信完了
'failed', // リトライ上限に達した
])->default('pending');
$table->timestamp('sent_at')->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('mail_logs');
}
};
php artisan migrate
migrateコマンドで以下のテーブルをまとめて作成:
| テーブル | 作成元 | 用途 |
|---|---|---|
mail_logs |
セットアップ5で作成したマイグレーション | この記事専用。メール送信状態の管理 |
jobs |
php artisan queue:table を実行した場合に生成 |
Databaseキュー用(この記事ではRedisを使うため未使用) |
cache |
Laravelデフォルト | キャッシュのDB保存用(この記事ではRedisを使うため未使用) |
users など |
Laravelデフォルト | この記事では使用しない |
horizon:installはDBマイグレーションを追加しません。 ただし、Laravelのデフォルト設定(QUEUE_FAILED_DRIVER=database-uuids)では、失敗ジョブはDBのfailed_jobsテーブルにも書き込まれます。Horizonダッシュボードはそれとは別にRedisで独自管理しています。
6. ジョブ・モデル・メールを実装する
必要なクラスを生成します(マイグレーションはセットアップ5で作成済みなので、ここではモデルのみ生成します):
php artisan make:model MailLog
php artisan make:job SendFakeMailJob
php artisan make:mail FakeMail --markdown=emails.fake-mail
php artisan make:command SendTestMailsCommand
🚧生成された各ファイルをエディタで開き、中身をすべて以下の内容に書き換えて保存してください:
app/Models/MailLog.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class MailLog extends Model
{
protected $fillable = ['email', 'status', 'sent_at'];
}
app/Mail/FakeMail.php
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class FakeMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public readonly string $recipient_email, // 宛先メールアドレス
public readonly int $mail_log_id, // メール件名の番号表示に使用
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: "テストメール #{$this->mail_log_id}",
);
}
public function content(): Content
{
return new Content(
markdown: 'emails.fake-mail', // resources/views/emails/fake-mail.blade.php
);
}
public function attachments(): array
{
return []; // 添付ファイルなし
}
}
resources/views/emails/fake-mail.blade.php(make:mail で自動生成済み。ファイルの中身をすべて以下に書き換えて保存します)
<x-mail::message>
# テストメール
**宛先:** {{ $recipient_email }}
**Mail Log ID:** {{ $mail_log_id }}
Redisキューから非同期で送信されたテストメールです。
Thanks,<br>
{{ config('app.name') }}
</x-mail::message>
app/Jobs/SendFakeMailJob.php
ShouldQueue インターフェースを実装するだけでLaravelがジョブをキュー経由にしてくれます。handle() 冒頭の重複防止ロジックは実験2で詳しく見ます。
Queueableトレイトについて:SendFakeMailJobではIlluminate\Foundation\Queue\Queueable、FakeMailではIlluminate\Bus\Queueableを使います。名前が同じですが別物です——コピペミスではありません。
<?php
namespace App\Jobs;
use App\Mail\FakeMail;
use App\Models\MailLog;
use Illuminate\Contracts\Queue\ShouldQueue; // このインターフェースを実装するだけでキュー経由になる
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Mail;
class SendFakeMailJob implements ShouldQueue
{
use Queueable;
public int $tries = 3; // 失敗しても最大3回まで試行する(初回 + リトライ2回)
public function __construct(
public int $mail_log_id // Redisに保存されるため、モデルではなくIDだけを渡す
) {}
// Horizonの Failed Jobs・Completed Jobs 一覧に "Tags: mail_log:10" のように表示される
// どのレコードのジョブかを一覧画面で即座に特定できる
public function tags(): array
{
return ['mail_log:' . $this->mail_log_id];
}
public function handle(): void
{
// pending のレコードだけを sending に変更する
// 複数ワーカーが同時に同じジョブを処理しようとしても、
// DBのUPDATE文は先着1件だけが updated=1 を受け取る(重複防止の核心)
$updated = MailLog::where('id', $this->mail_log_id)
->where('status', 'pending')
->update(['status' => 'sending']);
if ($updated === 0) {
return; // 別のワーカーがすでに処理中 or 送信済み → スキップ
}
$mail_log = MailLog::find($this->mail_log_id);
if (! $mail_log) {
return; // レコードが削除済みの場合のガード(通常は発生しない)
}
sleep(1); // 実際のメール送信の遅さをシミュレート(Horizonで処理の流れを観察しやすくする)
if ($this->shouldFail($mail_log->id)) {
$mail_log->update(['status' => 'pending']); // リトライ時に冒頭のUPDATE条件を通過できるよう pending に戻す
throw new \Exception("Mail #{$mail_log->id}: simulated failure");
}
// Mailpit(http://localhost:8025)にメールを送信
Mail::to($mail_log->email)->send(new FakeMail(
recipient_email: $mail_log->email,
mail_log_id: $mail_log->id,
));
// 送信完了後すぐに sent に更新する
// クラッシュ後に再キューされたとき「送信済み」と判別するために必要
$mail_log->update([
'status' => 'sent',
'sent_at' => now(),
]);
}
// id=10, 20, 30... を意図的に失敗させる(Horizonのリトライ動作を観察するため)
protected function shouldFail(int $id): bool
{
return $id % 10 === 0;
}
// $tries 回すべて失敗したときに Horizon が自動で呼び出す
public function failed(\Throwable $e): void
{
MailLog::where('id', $this->mail_log_id)
->update(['status' => 'failed']);
}
}
app/Console/Commands/SendTestMailsCommand.php
<?php
namespace App\Console\Commands;
use App\Jobs\SendFakeMailJob;
use App\Models\MailLog;
use Illuminate\Console\Command;
class SendTestMailsCommand extends Command
{
protected $signature = 'mail:send-test {--count=100 : キューに投入するメール件数}';
protected $description = 'テストメールをキューに投入する — Horizon と Mailpit で処理を観察できる';
public function handle(): void
{
$count = max(0, (int) $this->option('count'));
$this->newLine();
$this->line(" <info>{$count}</info> 件のテストメールをキューに投入中...");
$this->newLine();
// withProgressBar はプログレスバーを表示しながらループするヘルパー
// dispatch() はジョブをRedisに積むだけ — この時点でメールはまだ送られない
$this->withProgressBar($count > 0 ? range(1, $count) : [], function ($i) {
$mail_log = MailLog::create([
'email' => "user{$i}@example.com",
]);
SendFakeMailJob::dispatch($mail_log->id)
->onQueue('emails'); // config/horizon.php で監視しているキュー名と合わせる
});
$this->newLine(2);
$this->info(" {$count} 件のキューへの投入が完了しました。");
$this->newLine();
$this->line(' <comment>Horizon ダッシュボード:</comment> http://localhost:8000/horizon');
$this->line(' <comment>Mailpit 受信ボックス:</comment> http://localhost:8025');
$this->newLine();
}
}
7. 起動する
ターミナルのタブを3つ開いてください(Mac: Cmd+T、Windows: Ctrl+Shift+T で新規タブ)。すべてセットアップ1で作成したプロジェクトルート(test-redis-mail/ ディレクトリ)で実行します。
ターミナル1: Laravelサーバーを起動
php artisan serve
ターミナル2: Horizonを起動(ワーカーが動き始めます)
php artisan horizon
ターミナル3: 💠以降の実験コマンドはここで打ちます。
ターミナル1と2は起動したまま閉じないでください。実験中もバックグラウンドで動き続ける必要があります。
Laravel Herdの場合 はターミナル1の
php artisan serveは不要です。http://test-redis-mail.testでアクセスできます。その場合は.envのAPP_URLも合わせて変更してください。APP_URL=http://test-redis-mail.test
ターミナル2でHorizonを起動したら、ブラウザでHorizonダッシュボードを開いておきましょう:http://localhost:8000/horizon(php artisan serve のデフォルトポートは 8000 です。ポートが違う場合は適宜書き換えてください)
準備完了です。
実験1:非同期処理を体感する
コード確認
ジョブを投入するArtisanコマンド内(app/Console/Commands/SendTestMailsCommand.php):
$this->withProgressBar($count > 0 ? range(1, $count) : [], function ($i) {
// MailLogレコードを作成してジョブをキューに積む(メールはまだ送られない)
$mail_log = MailLog::create(['email' => "user{$i}@example.com"]);
SendFakeMailJob::dispatch($mail_log->id)->onQueue('emails');
});
dispatch() した時点ではメールはまだ1通も送られていません。ジョブをRedisに積んだだけです。
sleep(1) を入れている理由: 実際のメール送信は数百ms〜数秒かかります。これがないと100件が一瞬で処理されてHorizonで流れを観察できません。
なお SendFakeMailJob には id % 10 === 0 のレコードを意図的に失敗させるロジックが入っています。mail_logs テーブルが空の状態から実行すれば id=10, 20, 30... が失敗対象になります。最終的に届くメールは100件中90件です。
動かしてみる
ターミナル3(何も実行していない)で実行します:
php artisan mail:send-test
プログレスバーが一瞬で完了します。100件のメールはまだ1通も送られていません。ジョブがRedisに積まれただけです。
Horizonダッシュボード(http://localhost:8000/horizon)に切り替えてください。 sleep(1) のおかげでゆっくりと処理が進み、左サイドバーの各ページでリアルタイムに確認できます:
- Pending Jobs → 処理待ちのジョブ件数が減っていく
-
Completed Jobs → 処理が完了するたびに増えていく。一覧には
Tags: mail_log:1のように表示され、どのレコードが送信済みか確認できる -
Failed Jobs → 3回すべて失敗したジョブが移動してくる。ジョブをクリックすると
Attempts: 3(3回試みた)とエラーメッセージが確認できます——これがリトライの証拠です。右上の「Retry」ボタンで手動再投入もできます
処理完了の確認はDashboardの Current Workload の Jobs が 0 になったタイミングです。 Completed Jobs に90件・Failed Jobs に10件(id=10, 20, ..., 100)になっていれば完了です。Mailpit(http://localhost:8025)も確認してみてください。90件のメールが届いています。
「コマンドが即座に返る」「バックグラウンドで処理が流れる」「失敗したジョブが自動リトライされる」——これが非同期処理の体感です。
実験2:クラッシュリカバリ
「ワーカーが処理中にクラッシュしたら、そのジョブはどうなるのか?」
実験前にDBとHorizonをリセットしてください。
php artisan migrate:fresh
php artisan horizon:clear
仕組みを理解する
ワーカーがRedisからジョブを取り出すとき、Laravelはそのジョブをすぐに削除せず、Redis内部で「処理中」として別管理します。ワーカーが処理を正常完了したときに初めてその管理が外れます。一定時間(デフォルト90秒)応答のなかったジョブは、Laravel Queue によって再取得可能状態へ戻されます(この秒数は .env の REDIS_QUEUE_RETRY_AFTER で変更できます)。
ただしこの仕組みが保証するのは「ジョブが消えないこと」だけです。Redisがジョブを再キューするとき、そのジョブが「すでに実行済みかどうか」はRedisには分かりません。再キューされたジョブは新しいジョブと同じように handle() を呼び出します。mail_logs.status による確認がなければ、Mail::send() がそのまま再実行されて重複送信が起きます。
mail_logs.status はセットアップ5で作成したカラムで、pending(処理待ち)・sending(処理中)・sent(送信済み)・failed(失敗)の4つの値を持ちます。ワーカーが handle() を実行した時点で pending → sending に更新され、送信完了後に sent に更新されます。再キュー後に handle() が再実行されても、すでに sent(送信済み)または sending(クラッシュ直前まで処理中だった)になっているレコードは冒頭のUPDATE条件(WHERE status = 'pending')を通過できないため、メール送信はスキップされます。
handle() 冒頭のこのコードがそれを担います(app/Jobs/SendFakeMailJob.php):
$updated = MailLog::where('id', $this->mail_log_id)
->where('status', 'pending')
->update(['status' => 'sending']);
if ($updated === 0) {
return; // 別のワーカーがすでに処理済み
}
動かしてみる
ターミナル3(何も実行していない)でジョブを投入します:
php artisan mail:send-test
ターミナル2(php artisan horizon が動いている)を見てください。Horizonダッシュボード(http://localhost:8000/horizon)の左サイドバー「Completed Jobs」が 20 件前後になったタイミングで、ターミナル3で以下を実行してHorizonを強制終了してください:
kill -9 $(pgrep -f "artisan horizon")
CTRL+Cではなく
kill -9を使う理由: CTRL+C はSIGINTシグナルを送り、Horizonはグレースフルシャットダウン(処理中のジョブを完了させてから終了)を行います。そのため CTRL+C では処理中のジョブが完了してしまい、sendingのまま止まったレコードが残りません。kill -9(SIGKILL)はプロセスを即座に強制終了するため、処理中のジョブが中断されてsendingのままのレコードが残ります。
ターミナル3でDBを確認します:
php artisan tinker
MailLog::where('status', 'sending')->count();
// → 1以上であれば成功(0の場合は kill -9 のタイミングが早すぎたか遅すぎた)
exit; // tinkerを終了してターミナルに戻る
sending のまま止まったレコードが確認できます。ワーカーが落ちた瞬間にちょうど処理中だったジョブです。0件だった場合は kill -9 のタイミングが早すぎたか遅すぎたので、以下でリセットしてから再試してください:
php artisan migrate:fresh
php artisan horizon:clear
ターミナル2でHorizonを再起動してから、ターミナル3で mail:send-test を再実行してください:
php artisan horizon
Horizonを再起動すると、Redisにまだ残っている pending のジョブはすぐに処理が再開されます。ターミナル2のログに Processed が流れ始めます。
kill -9 で止まったとき、Redis上には2種類のジョブが残っています:
-
まだキューに積まれていただけのジョブ(
pending)→ Horizonが普通に処理を再開する -
処理中だったジョブ(Redisが「処理中」として別管理していたもの)→ 最大90秒待つと(デフォルト設定の場合。
REDIS_QUEUE_RETRY_AFTERを変更している場合はその秒数)、Laravel Queue が「応答なし」と判断して再キューに戻す
再キューされたジョブが handle() を再実行したとき、mail_logs.status の判定が働きます:
| status | 再キュー後の動作 |
|---|---|
pending |
WHERE status = 'pending' 条件を通過 → 処理が再開される
|
sent |
条件を通過できず → スキップ(重複送信を防ぐ) |
sending |
条件を通過できず → スキップ(クラッシュしたメールは送信されないまま停止) |
sending のスキップは意図どおりの動作です——この記事の実装はシンプルにするため sending のままクラッシュしたジョブの再処理は行いません。
待ち時間を短縮したい場合 は、実験2を始める前(冒頭のリセット時)に
.envへREDIS_QUEUE_RETRY_AFTER=60を追加しておいてください。Horizonを起動する前に設定しておけば、90秒ではなく60秒後に再キューされます。
retry_afterとtimeoutの関係:retry_afterは「ジョブを再取得可能にするまでの時間」、queue:work --timeoutは「ワーカープロセスを強制終了するまでの時間」です。通常はtimeout < retry_afterに設定します。逆転すると、タイムアウトで終了したワーカーがジョブを手放す前に別のワーカーが同じジョブを拾い、並列実行が起きる可能性があります。
Horizon停止前にすでに届いていたメールが2通になっていないか、Mailpit(http://localhost:8025)で確認してください。
ワーカーが落ちてもRedisにジョブが残る。アプリ側のstatus管理により送信済みジョブの重複を防げる。これがキューの耐障害性です。
まとめ
2つの実験を通して体感できたことを振り返ります。
| 実験 | 体感できること |
|---|---|
| 実験1:非同期処理・失敗・リトライ |
dispatch() した瞬間に処理が返る。失敗したジョブはHorizonが自動リトライし、上限に達したら failed() で failed に更新 |
| 実験2:クラッシュリカバリ | Redisにジョブが残る。アプリ側のstatus管理により送信済みジョブの重複を防げる |
キューを「なんとなく知っている」から「動いている様子を目で見た」状態に変わったでしょうか。
Laravel Queue がジョブを定義して dispatch() し、Redis がそのジョブをインメモリで保持し、Horizon がワーカーを管理してリアルタイムに可視化する——この3つが組み合わさってはじめてキューの全挙動を体感できる環境になります。
ご拝読いただきありがとうございました!