> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# キュージョブの実行制御

> ShouldBeUnique・ShouldBeUniqueUntilProcessing・DebounceFor を使って、キュージョブの重複実行・連続ディスパッチを制御する方法を解説します。

## 概要

Laravel のキュー機能には、ジョブの\*\*重複排除（Unique）**と**デバウンス（Debounce）\*\*という2種類の実行制御が用意されています。どちらも「同じジョブを何度もディスパッチしたとき、無駄な実行を省く」ための仕組みですが、動作が異なります。

| 機能                      | インターフェース / アトリビュート              | 目的                           |
| ----------------------- | ------------------------------- | ---------------------------- |
| Unique Jobs             | `ShouldBeUnique`                | キューに同一ジョブが1つだけ存在する状態を保つ      |
| Unique Until Processing | `ShouldBeUniqueUntilProcessing` | 処理開始までのみユニーク制約を保持する          |
| Debounced Jobs          | `#[DebounceFor]`                | 短時間に連続ディスパッチされた場合、最新1件だけ実行する |

<Warning>
  Unique Jobs と Debounced Jobs は**排他的**です。`DebounceFor` アトリビュートを使用するジョブに `ShouldBeUnique` を実装しないでください。
</Warning>

***

## Unique Jobs — `ShouldBeUnique`

同一ジョブがキューに存在する間は、追加のディスパッチを無視します。

```php theme={null}
<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;

class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    // 追加メソッドは不要
}
```

`UpdateSearchIndex` がキューに積まれている（または処理中）の間、同じジョブをディスパッチしようとしても無視されます。

### キーでユニーク制約を絞り込む — `UniqueFor` + `uniqueId()`

同じジョブクラスでも「商品 A の更新」と「商品 B の更新」は別ジョブとして扱いたい場合、`uniqueId()` メソッドでキーを定義します。

```php theme={null}
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Queue\Attributes\UniqueFor;

#[UniqueFor(3600)] // 1時間後にロックを自動解放
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    public function __construct(public readonly int $productId)
    {
    }

    public function uniqueId(): string
    {
        return (string) $this->productId;
    }
}
```

* `uniqueId()` が返す値がキャッシュロックのキーになります。
* `#[UniqueFor(秒数)]` を指定すると、その秒数が経過した後にロックが自動解放されます（ジョブが処理されなかった場合のフェイルセーフ）。

### キャッシュドライバーを指定する — `uniqueVia()`

デフォルトのキャッシュドライバー以外を使用したい場合は `uniqueVia()` を実装します。

```php theme={null}
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

public function uniqueVia(): Repository
{
    return Cache::driver('redis');
}
```

<Info>
  Unique Jobs はアトミックロックをサポートするキャッシュドライバー（`redis`、`database`、`memcached`、`dynamodb`、`file`、`array`）が必要です。
</Info>

***

## `ShouldBeUnique` vs `ShouldBeUniqueUntilProcessing`

`ShouldBeUnique` のロックは**ジョブが完了またはリトライ上限に達するまで**保持されます。これが問題になるケースがあります。

**例：** キューに `UpdateSearchIndex(product_id: 42)` が1件あり、ワーカーが処理を開始した直後に同じジョブを再ディスパッチしたい場合。`ShouldBeUnique` では処理完了まで2件目がキューに入りません。

この場合は `ShouldBeUniqueUntilProcessing` を使います。ロックが**処理開始直前に解放**されるため、ワーカーがジョブを取り出した瞬間に次のディスパッチが可能になります。

```php theme={null}
<?php

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;

class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
    // ...
}
```

```mermaid theme={null}
sequenceDiagram
    participant D as ディスパッチャー
    participant Q as キュー
    participant W as ワーカー

    D->>Q: dispatch() — ロック取得
    D->>Q: dispatch() — ロックあり → 無視

    note over Q,W: ShouldBeUnique の場合
    W->>Q: ジョブ取り出し
    W->>W: 処理中（ロック保持）
    D->>Q: dispatch() — ロックあり → 無視
    W->>W: 処理完了 — ロック解放
    D->>Q: dispatch() — ここでやっとキュー可能

    note over Q,W: ShouldBeUniqueUntilProcessing の場合
    W->>Q: ジョブ取り出し — ロック解放
    D->>Q: dispatch() — ロックなし → キュー可能
    W->>W: 処理中
```

### 比較まとめ

|              | `ShouldBeUnique` | `ShouldBeUniqueUntilProcessing` |
| ------------ | ---------------- | ------------------------------- |
| ロック解放タイミング   | 処理完了 / 失敗後       | 処理開始直前                          |
| 処理中の重複ディスパッチ | 無視               | キュー可能                           |
| ユースケース       | 同時実行を完全に防ぎたい     | 処理後にすぐ次のジョブを入れたい                |

***

## Debounced Jobs — `#[DebounceFor]`

<Info>
  `DebounceFor` アトリビュートは Laravel 13 で追加された機能です。
</Info>

短時間に同じジョブが大量にディスパッチされた場合に、**最後にディスパッチされた1件だけ**を実行します。Web フロントエンドのデバウンスと同じ発想です。

```php theme={null}
<?php

namespace App\Jobs;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\DebounceFor;

#[DebounceFor(30)] // 30秒以内の再ディスパッチは無視（最新1件のみ実行）
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;

    public function __construct(public readonly int $productId)
    {
    }

    public function debounceId(): string
    {
        return (string) $this->productId;
    }
}
```

* `debounceId()` が返す値でジョブを識別します（商品 ID ごとに独立したデバウンスが適用）。
* 30秒の間に同じ `productId` で10回ディスパッチされても、最後の1件だけが実行されます。

### `maxWait` — 最大待機時間の上限

頻繁に更新されるデータでは、デバウンスが延々と続いてジョブが永遠に実行されない可能性があります。`maxWait` で最大遅延時間を設定できます。

```php theme={null}
#[DebounceFor(30, maxWait: 120)]
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;
    // ...
}
```

この例では、初回ディスパッチから最大 120 秒後には必ず実行されます（30秒のデバウンスが続いても 120 秒でタイムアウト）。

### キャッシュドライバーを指定する — `debounceVia()`

```php theme={null}
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;

public function debounceVia(): Repository
{
    return Cache::driver('redis');
}
```

### `JobDebounced` イベント

後続のディスパッチによって上書きされたジョブは、`Illuminate\Queue\Events\JobDebounced` イベントを発行してキューから削除されます。このイベントをリッスンすることで、デバウンスされたジョブの追跡や監視が可能です。

***

## どれを使うべきか

```mermaid theme={null}
flowchart TD
    A["同じジョブを複数回ディスパッチする可能性がある"] --> B{"短時間に連続ディスパッチ<br>最新1件だけ実行したい?"}
    B -->|Yes| C["#[DebounceFor]"]
    B -->|No| D{"キューに1件だけ<br>存在させたい?"}
    D -->|Yes| E{"処理中も重複を防ぎたい?"}
    E -->|Yes| F["ShouldBeUnique"]
    E -->|No| G["ShouldBeUniqueUntilProcessing"]
    D -->|No| H["通常のジョブ"]
```

| ユースケース                       | 推奨                              |
| ---------------------------- | ------------------------------- |
| 同じ処理がキューに2件以上あっても意味がない       | `ShouldBeUnique`                |
| 処理中も含めて並列実行を防ぎたい             | `ShouldBeUnique`                |
| ワーカーが取り出したらすぐ次を入れたい          | `ShouldBeUniqueUntilProcessing` |
| ユーザーが保存ボタンを連打しても1回だけ実行したい    | `#[DebounceFor]`                |
| モデル更新のたびに検索インデックスを再構築（大量更新時） | `#[DebounceFor]` + `maxWait`    |

***

## 内部実装

### Unique Jobs のロック機構

`ShouldBeUnique` ジョブがディスパッチされると、Laravel は内部でキャッシュの[アトミックロック](/cache#atomic-locks)を取得します。ロックキーは以下のフォーマットです：

```
laravel_unique_job:{ジョブクラス名}:{uniqueId()}
```

ロックが取得できなかった場合（既に別のジョブが保持中）、ジョブはキューに追加されません。

### Debounced Jobs の実装

`DebounceFor` は内部的に「デバウンスウィンドウ」を管理するキャッシュエントリを使います。新しいディスパッチが来るたびに：

1. 既存のジョブをキューから削除（`JobDebounced` イベント発行）
2. 新しいジョブをキューに追加（デバウンス秒数の遅延付き）
3. キャッシュのタイマーをリセット

`maxWait` が指定されている場合、初回ディスパッチのタイムスタンプも記録し、その時刻から `maxWait` 秒を超えるデバウンスを防ぎます。

***

## 参考リンク

* [Laravel 公式ドキュメント — Unique Jobs](https://laravel.com/docs/queues#unique-jobs)
* [Laravel 公式ドキュメント — Debounced Jobs](https://laravel.com/docs/queues#debounced-jobs)
* [`Illuminate\Contracts\Queue\ShouldBeUnique`](https://github.com/laravel/framework/blob/main/src/Illuminate/Contracts/Queue/ShouldBeUnique.php)
* [`Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing`](https://github.com/laravel/framework/blob/main/src/Illuminate/Contracts/Queue/ShouldBeUniqueUntilProcessing.php)
* [`Illuminate\Queue\Attributes\DebounceFor`](https://github.com/laravel/framework/blob/main/src/Illuminate/Queue/Attributes/DebounceFor.php)
* [`Illuminate\Queue\Attributes\UniqueFor`](https://github.com/laravel/framework/blob/main/src/Illuminate/Queue/Attributes/UniqueFor.php)


## Related topics

- [Laravel Telescope 実践テクニック](/jp/blog/telescope-introduction.md)
- [タスクスケジューリング](/jp/scheduling.md)
- [キューとジョブ](/jp/queues.md)
- [Laravel Telescope](/jp/telescope.md)
- [Laravel Nightwatch 入門](/jp/blog/nightwatch-introduction.md)
