php elephant sticker
Photo by RealToughCandy.com on Pexels.com
目次

【完全保存版】Laravel スケジューラ&キュー連携で作る定期処理・バッチ運用ガイド――安定実行・監視・アクセシブルな進捗通知まで

この記事で学べること(要点)

  • Laravel のスケジューラ(app/Console/Kernel.php)で定期処理を安全に運用する設計
  • キュー(ShouldQueue/再試行/遅延)と組み合わせた大規模バッチの実装・監視
  • 冪等性・重複実行防止・タイムゾーン・メンテナンス時間帯の扱い
  • 成果物(レポート生成、インポート、通知)におけるアクセシビリティ配慮(読み上げ、フォーカス、色に依存しない状態表現)
  • 運用チェックリスト、失敗時のロールバック、CI/CD への組み込み

想定読者(だれが得をする?)

  • 実務で定期バッチ・夜間処理を動かしたい Laravel 初〜中級エンジニア
  • 受託や自社 SaaS のテックリード:キューとスケジューラで安定運用の型を作りたい方
  • CS/運用チーム:ユーザー通知や失敗検知を整備し、問い合わせを減らしたい方
  • アクセシビリティ担当・QA:進捗表示・完了報告・障害時の案内を「誰でも理解できる」形に整えたい方

1. はじめに:なぜ「スケジューラ×キュー」なのか

業務システムや SaaS では、夜間バッチ、毎時の集計、毎朝のメール配送など「決まったタイミングで確実に走る処理」が欠かせません。Laravel のスケジューラは OS の cron を一つだけ登録し、アプリ側で柔軟に発火条件を管理できる仕組みです。大量データを扱う処理や外部 API 連携には、ジョブキューと組み合わせるのが定石。これにより、以下を実現します。

  • アプリコードで時間・頻度・例外時の対応を一元管理
  • 各処理をジョブとして非同期化し、再試行やワーカー分散に対応
  • 失敗時のアラートやユーザー向けのお知らせを設計しやすい
  • 将来のスケールに備え、処理を細分化・直列/並列で構成可能

本記事は「安全に、静かに、確実に動く」定期処理の設計・実装・運用を、アクセシビリティ視点も含めて丁寧に解説します。


2. 基本の配線:cron は 1 行、スケジューラはアプリ側で管理

2.1 cron の設定(サーバー側)

* * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1
  • 1 分ごとに schedule:run を実行すると、app/Console/Kernel.php に定義したスケジュールが必要な時だけ走ります。
  • 複数サーバーで同じ cron を入れる場合は、ロック(後述)や「リーダーだけ実行」戦略を取りましょう。

2.2 app/Console/Kernel.php の基本

// app/Console/Kernel.php
namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    protected function schedule(Schedule $schedule): void
    {
        // 毎日 03:00 に売上レポートを生成
        $schedule->command('report:daily-sales')
            ->dailyAt('03:00')
            ->timezone('Asia/Tokyo')
            ->withoutOverlapping(30)        // 前回実行が残っていたらスキップ(30分ロック)
            ->onOneServer()                 // マルチサーバー時は単一実行
            ->emailOutputOnFailure('ops@example.com');
    }

    protected function commands(): void
    {
        $this->load(__DIR__.'/Commands');
    }
}

要点:

  • timezone('Asia/Tokyo') を明示し、時差や夏時間の影響を避けます。
  • withoutOverlapping()二重起動を防止。
  • onOneServer() は分散環境で単一ノードのみ実行するための保険。
  • emailOutputOnFailure() で失敗を検知し運用へ通知(他の通知チャネルも可)。

3. Artisan コマンドとキュー:重い処理は必ず非同期化

3.1 コマンドを作る(薄い orchestration)

php artisan make:command GenerateDailySalesReport
// app/Console/Commands/GenerateDailySalesReport.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Jobs\BuildSalesReport; // 後述

class GenerateDailySalesReport extends Command
{
    protected $signature = 'report:daily-sales {--date=}';
    protected $description = '日次売上レポートを生成';

    public function handle(): int
    {
        $date = $this->option('date') ?? now()->subDay()->toDateString();
        BuildSalesReport::dispatch($date)->onQueue('reports');

        $this->info("Dispatched BuildSalesReport for {$date}");
        return Command::SUCCESS;
    }
}
  • コマンドはオーケストレーションに徹し、重い処理はジョブへ
  • オプションで対象日を指定できるようにして、後から再実行しやすくします。

3.2 ジョブを作る(ビジネス処理の本体)

php artisan make:job BuildSalesReport
// app/Jobs/BuildSalesReport.php
namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\SerializesModels;
use App\Services\Reports\SalesReportService;

class BuildSalesReport implements ShouldQueue
{
    use InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;               // 自動再試行回数
    public int $timeout = 3600;          // タイムアウト 1h

    public function __construct(public string $date) {}

    public function middleware(): array
    {
        return [
            new RateLimited('reports'),   // 外部 API 等への呼び出し制限名
        ];
    }

    public function handle(SalesReportService $svc): void
    {
        // 冪等チェック(既に生成済みならスキップ)
        if ($svc->exists($this->date)) {
            return;
        }

        // 大量データはチャンクで取得し、CSV/Excel/PDF を生成
        $svc->build($this->date);

        // 完了通知(メール/Web/Slack など任意)
        $svc->notifyCompleted($this->date);
    }

    public function failed(\Throwable $e): void
    {
        // 失敗時の通知・補助情報
        // 例:$svc->notifyFailed($this->date, $e->getMessage());
    }
}

設計ポイント:

  • ShouldQueue で非同期化。triestimeout を明示。
  • middleware()レート制限ユニーク制御を付与可能。
  • 冪等性(exists チェック等)を最初に置き、二重実行でも安全に。
  • 失敗時 failed()原因と対処を通知。

4. 重複実行と冪等性:事故を未然に防ぐ

4.1 スケジューラのロックとユニークジョブ

  • withoutOverlapping(minutes) をスケジュールに付与。
  • キュー側はユニークキーで重複投入を防止(例えば Redis ロック)。
// 簡易ユニーク投入例(サービス層などで)
$lockKey = "job:build-sales:{$date}";
if (cache()->lock($lockKey, 600)->get()) {
    BuildSalesReport::dispatch($date);
    // 後続で、ジョブ開始時/終了時に release() も可能
}

4.2 冪等な設計

  • 成果物(レポートファイル等)は決定的なパス/IDで保存し、存在すればスキップまたは上書き。
  • 外部 API はページング・カーソルで再取得しても同じ結果になるように。
  • DB 更新はトランザクションアップサートで途中失敗に強く。

5. スケーリング:大きな処理を小さく分ける

5.1 チャンクと逐次処理

// 大量行の集計
Order::whereDate('created_at', $this->date)
    ->orderBy('id')
    ->chunk(5_000, function ($chunk) use ($writer) {
        foreach ($chunk as $order) {
            $writer->appendRow($order);
        }
    });
  • メモリ使用量を抑え、長時間ジョブでも安定。
  • 集計結果の出力はストリーム(ファイルへ順次書き込み)を基本に。

5.2 並列化と依存関係

  • 大量ジョブを日単位/店舗単位/カテゴリ単位に分割してディスパッチ。
  • 依存関係がある場合は親→子の順序やキュー名でレーン分離。
  • レポート生成 → メール送信のように、キューを段階化すると監視しやすい。

6. 監視と可視化:失敗にすぐ気づき、静かに復旧

6.1 ジョブの状態監視

  • 失敗ジョブ(failed_jobs テーブル)を毎朝集計して通知
  • ワーカーは Supervisor などで自動再起動とログ保全。
  • キューの滞留(レイテンシ/件数)を指標化し、閾値でアラート。

6.2 スケジューラの健全性チェック

  • 毎日 00:05 に「スケジューラは動いている」ハートビートを送信。
  • 動いていない日はアラートで気付けるように。
  • schedule:listschedule:test を定期的に実行し、設定ドリフトを防止。

7. メンテナンス時間帯とタイムゾーン

  • 事業都合の静穏時間帯は「重い処理を避ける」。
  • skip(function() { ... })営業日/祝日等を条件分岐。
  • 取引先 API のレート制限窓口に合わせてスケジュールを設計。
  • ユーザー向け通知は受け手のタイムゾーンを考慮。
$schedule->command('notify:daily')
    ->dailyAt('08:00')
    ->timezone('Asia/Tokyo')
    ->skip(function () {
        return today('Asia/Tokyo')->isWeekend();
    });

8. 典型ユースケースと実装サンプル

8.1 CSV インポート(夜間バッチ)

  • S3 へアップロードされたファイルを検出→分割してキューへ
  • 各ジョブで検証→DB 反映(バリデーションエラーは別テーブルへ蓄積)
  • 完了後に集計レポートエラーリストをメールで通知
// app/Jobs/ImportChunk.php
public function handle(ImportService $svc): void
{
    $rows = $svc->read($this->path, $this->offset, $this->limit);
    foreach ($rows as $row) {
        $dto = $svc->validate($row);   // 失敗は収集して後で通知
        $svc->upsert($dto);            // 冪等に
    }
}

8.2 外部 API 集計(毎時)

  • API レート制限に合わせてキューのスロットリングRateLimited ミドルウェア)
  • 途中失敗は自動再試行、一定回数失敗で保留キューへ退避し運用判断

9. ユーザー向け進捗・結果画面のアクセシビリティ

9.1 進捗ダッシュボード(Web)

{{-- 進捗サマリはライブリージョンで読み上げ --}}
<div id="status" aria-live="polite" role="status" class="mb-3">
  本日のレポート生成は 60% 完了(3/5 ステップ)
</div>

<ul class="space-y-2">
  <li>
    <span class="inline-flex items-center gap-2">
      <span aria-hidden="true">⏳</span>
      データ抽出
    </span>
    <span class="sr-only">進行中</span>
  </li>
  <li>
    <span class="inline-flex items-center gap-2 text-green-700">
      <span aria-hidden="true">✔</span>
      集計
    </span>
    <span class="sr-only">完了</span>
  </li>
  <li>
    <span class="inline-flex items-center gap-2 text-gray-700">
      <span aria-hidden="true">…</span>
      ファイル出力
    </span>
  </li>
</ul>
  • 視覚の色分けに加え、アイコン+テキストで状態を多重表現。
  • 進捗更新は aria-live="polite" で読み上げ通知。
  • キーボードでも操作できるよう、リンク/ボタンで操作を実装。

9.2 完了・失敗メールの文面

  • HTML とプレーンテキストの両方を用意。
  • 件名は「日時+内容+結果」の順で短く。
  • 失敗時は再試行の可否問い合わせ先エラー要点を明記。
  • 画像や色に依存せず、本文で意味が完結する構成に。

10. エラーハンドリングとロールバック

  • トランザクションで部分成功を避ける
  • 外部 API 連携はサーキットブレーカ指数バックオフを採用。
  • 失敗の種類を分類(データ不正/一時障害/恒久障害)し、再試行方針を分ける。
  • 連鎖ジョブは親の失敗時に子を中止し、通知を一本化。

11. セキュリティと権限

  • バッチ専用の環境変数/資格情報を最小権限で発行。
  • 署名付き URL や一時的な認証トークンで成果物の直リンク配布を安全に。
  • ログに個人情報や機微情報を残さない(マスキング)。

12. Horizon/Supervisor 運用(概要)

  • Redis キュー利用時は可視化とスロットリング設定が容易。
  • ワーカー数は処理時間×到達レイテンシで見積もり、段階的に増減。
  • ログにはジョブ ID/対象日/カテゴリなど検索キーを必ず記録。

13. 品質保証:テスト戦略

13.1 単体・機能テスト(同期実行)

public function test_report_command_dispatches_job(): void
{
    Queue::fake();
    $this->artisan('report:daily-sales --date=2025-08-31')
        ->assertExitCode(0);

    Queue::assertPushed(\App\Jobs\BuildSalesReport::class, function ($job) {
        return $job->date === '2025-08-31';
    });
}
  • Queue::fake() でジョブ投入を検証。
  • サービス層は純粋なメソッドにし、テーブル駆動テストで網羅。

13.2 E2E(小規模で本当の実行)

  • 小さな入力データで実際にファイル生成→整合性を検証。
  • 失敗系(API 403/429 やネットワーク障害)をモックして、再試行と通知を確認。

14. チェックリスト(配布用)

スケジュール設計

  • [ ] timezone() を明示し、withoutOverlapping()onOneServer() を付与
  • [ ] 営業日・静穏時間帯の skip() 条件
  • [ ] 失敗時通知・ハートビート

ジョブ設計

  • [ ] ShouldQueuetriestimeoutRateLimited 等を設定
  • [ ] 冪等性(存在チェック・決定的パス・アップサート)
  • [ ] チャンク/ストリームでメモリ上限に配慮
  • [ ] 失敗時の failed() で原因と次の手段を通知

スケール/分割

  • [ ] 日/店舗/カテゴリ単位に分割ディスパッチ
  • [ ] キュー名でレーン分離、依存順を制御

監視/運用

  • [ ] 失敗ジョブ集計・滞留アラート
  • [ ] Supervisor/Horizon でワーカー監視・自動再起動
  • [ ] ログ検索キー(ジョブID/日付/カテゴリ)

アクセシビリティ

  • [ ] 進捗/結果画面は role="status"aria-live で読み上げ
  • [ ] 成功/失敗を色以外(アイコン/文言)でも表現
  • [ ] メールはプレーンテキスト版を必ず用意

セキュリティ

  • [ ] 最小権限の資格情報
  • [ ] 成果物配布は署名付き URL
  • [ ] ログのマスキング

15. 具体的テンプレート(完成例・抜粋)

15.1 Console Kernel

protected function schedule(Schedule $schedule): void
{
    // ハートビート
    $schedule->call(fn() => \Log::info('scheduler:alive'))
        ->dailyAt('00:05')
        ->timezone('Asia/Tokyo')
        ->onOneServer();

    // 日次レポート
    $schedule->command('report:daily-sales')
        ->dailyAt('03:00')
        ->timezone('Asia/Tokyo')
        ->withoutOverlapping(60)
        ->onOneServer()
        ->emailOutputOnFailure('ops@example.com');
}

15.2 成果物の署名付きダウンロード

// routes/web.php
Route::get('/reports/{date}', [ReportController::class, 'show'])
    ->middleware('signed')
    ->name('reports.show');

// 生成
$url = URL::temporarySignedRoute('reports.show', now()->addHours(24), ['date' => $date]);

15.3 進捗更新のポーリング(簡易)

async function poll() {
  const res = await fetch('/api/report/progress?date=2025-08-31', {headers:{Accept:'application/json'}});
  if (res.ok) {
    const { percent, stepText } = await res.json();
    const el = document.getElementById('status');
    el.textContent = `進捗 ${percent}%:${stepText}`;
  }
  setTimeout(poll, 15000);
}
poll();

16. よくある落とし穴と回避策

  • 同期的に重い処理を command に直書き → 必ずジョブへ切り出し、再試行・監視を得る。
  • 何度走っても結果が変わる → 冪等性(存在チェック、決定的キー、アップサート)。
  • 「完了メールのみ」→失敗時に気づけない。失敗通知ハートビートを必ず。
  • 色だけで進捗表示 → 状態が判別しにくい。アイコン+文言を併用。
  • タイムゾーン未指定 → 想定外の時刻に実行。timezone() を明示。
  • 大きな CSV を一括読み → メモリ圧迫。チャンク処理ストリーム出力

17. チーム運用に馴染ませるコツ

  • コマンド・ジョブ・サービス層を定型化し、ひな形から追加できるようにする。
  • 命名規約(Build*, Import*, Notify*)で一覧性を上げる。
  • 成果物の出力先・命名reports/YYYYMM/DD/sales_{date}.csv)を統一。
  • 失敗通知は運用窓口を一本化し、個人宛メールにしない。
  • ダッシュボードに日別の完了/失敗数を集計し、異常傾向を早期発見。

18. まとめ:小さく確実に回し、静かにスケールする

  • スケジューラで時間管理をコード化し、withoutOverlapping()onOneServer() で安全に。
  • 仕事はジョブに任せ、再試行・レート制限・冪等性で壊れにくく。
  • 大量処理はチャンク並列化でスループットを稼ぎ、指標で滞留を可視化
  • 結果の提示はアクセシビリティを最初から考慮し、読み上げ・色以外の表現・テキスト版メールで誰でも理解できる形に。
  • 失敗は悪ではなく信号。失敗通知とハートビートで早く気づき、静かに復旧できる体制を。

本記事のテンプレートを土台に、あなたのチームでも「静かで、確実で、やさしい」定期処理を育ててください。わたしも応援しています。

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)