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

# Queue Job 的執行控制

> 說明如何使用 ShouldBeUnique、ShouldBeUniqueUntilProcessing、DebounceFor 控制 Queue Job 的重複執行與連續 dispatch。

## 概觀

Laravel 的 Queue 功能提供了 **Unique（去重）** 與 **Debounce** 兩種 Job 執行控制。皆是為了「當同一 Job 被多次 dispatch 時，省去無謂執行」的機制，但運作有所不同。

| 功能                      | 介面 / Attribute                  | 目的                          |
| ----------------------- | ------------------------------- | --------------------------- |
| Unique Jobs             | `ShouldBeUnique`                | 保持 Queue 中同一 Job 僅存在 1 個的狀態 |
| Unique Until Processing | `ShouldBeUniqueUntilProcessing` | 僅在處理開始前保持唯一性約束              |
| Debounced Jobs          | `#[DebounceFor]`                | 若短時間內連續 dispatch，只執行最新 1 件  |

<Warning>
  Unique Jobs 與 Debounced Jobs 為**互斥**。請勿於使用 `DebounceFor` attribute 的 Job 中實作 `ShouldBeUnique`。
</Warning>

***

## Unique Jobs — `ShouldBeUnique`

當同一 Job 存在於 Queue 期間，將忽略追加的 dispatch。

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

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

class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    // 無需額外方法
}
```

當 `UpdateSearchIndex` 已在 Queue 中（或正在處理），再次 dispatch 同一 Job 會被忽略。

### 以 key 限縮唯一性約束 — `UniqueFor` + `uniqueId()`

若相同 Job 類別下希望將「商品 A 的更新」與「商品 B 的更新」視為不同 Job，可使用 `uniqueId()` 方法定義 key。

```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 小時後自動釋放 lock
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    public function __construct(public readonly int $productId)
    {
    }

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

* `uniqueId()` 回傳的值成為 cache lock 的 key。
* 若指定 `#[UniqueFor(秒數)]`，經過該秒數後 lock 會自動釋放（作為 Job 未被處理時的 fail-safe）。

### 指定 Cache Driver — `uniqueVia()`

若希望使用預設 cache driver 以外者，可實作 `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 需要支援 atomic lock 的 cache driver（`redis`、`database`、`memcached`、`dynamodb`、`file`、`array`）。
</Info>

***

## `ShouldBeUnique` vs `ShouldBeUniqueUntilProcessing`

`ShouldBeUnique` 的 lock **會保持至 Job 完成或達到重試上限**。這在某些情境下會有問題。

**例：** Queue 中有 1 件 `UpdateSearchIndex(product_id: 42)`，Worker 開始處理後想立即重新 dispatch 相同 Job。`ShouldBeUnique` 於處理完成前不會讓第 2 件進入 Queue。

此時可使用 `ShouldBeUniqueUntilProcessing`。因為 lock **於處理開始前釋放**，故當 Worker 取出 Job 的瞬間即可進行下次 dispatch。

```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 Dispatcher
    participant Q as Queue
    participant W as Worker

    D->>Q: dispatch() — 取得 lock
    D->>Q: dispatch() — 已有 lock → 忽略

    note over Q,W: ShouldBeUnique 時
    W->>Q: 取出 Job
    W->>W: 處理中（保持 lock）
    D->>Q: dispatch() — 已有 lock → 忽略
    W->>W: 處理完成 — 釋放 lock
    D->>Q: dispatch() — 此時才可進入 Queue

    note over Q,W: ShouldBeUniqueUntilProcessing 時
    W->>Q: 取出 Job — 釋放 lock
    D->>Q: dispatch() — 無 lock → 可進入 Queue
    W->>W: 處理中
```

### 比較彙整

|                 | `ShouldBeUnique` | `ShouldBeUniqueUntilProcessing` |
| --------------- | ---------------- | ------------------------------- |
| lock 釋放時機       | 處理完成 / 失敗後       | 處理開始前                           |
| 處理中的重複 dispatch | 忽略               | 可進入 Queue                       |
| 使用情境            | 希望完全防止併行執行       | 希望處理後立即放入下一個 Job                |

***

## Debounced Jobs — `#[DebounceFor]`

<Info>
  `DebounceFor` attribute 為 Laravel 13 新增的功能。
</Info>

當短時間內大量 dispatch 同一 Job 時，只執行**最後 dispatch 的 1 件**。與 Web 前端的 debounce 為同樣思路。

```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 秒內的再次 dispatch（僅執行最新 1 件）
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;

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

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

* 以 `debounceId()` 回傳的值辨識 Job（每個 product ID 有獨立的 debounce）。
* 即便 30 秒內以相同 `productId` dispatch 10 次，也只執行最後 1 件。

### `maxWait` — 最大等待時間上限

於頻繁更新的資料，debounce 可能持續而使 Job 永遠不執行。可透過 `maxWait` 設定最大延遲時間。

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

此例中，自初次 dispatch 起最多 120 秒後必定會執行（即便 30 秒 debounce 持續，也會於 120 秒 timeout）。

### 指定 Cache Driver — `debounceVia()`

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

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

### `JobDebounced` 事件

被後續 dispatch 覆蓋的 Job 會發出 `Illuminate\Queue\Events\JobDebounced` 事件後從 Queue 中被刪除。透過監聽此事件可以追蹤及監視被 debounce 的 Job。

***

## 應該使用哪一個

```mermaid theme={null}
flowchart TD
    A["可能多次 dispatch 相同 Job"] --> B{"短時間內連續 dispatch<br>希望僅執行最新 1 件?"}
    B -->|Yes| C["#[DebounceFor]"]
    B -->|No| D{"希望 Queue 中<br>僅存在 1 件?"}
    D -->|Yes| E{"希望連處理中也防止重複?"}
    E -->|Yes| F["ShouldBeUnique"]
    E -->|No| G["ShouldBeUniqueUntilProcessing"]
    D -->|No| H["一般 Job"]
```

| 使用情境                      | 建議                              |
| ------------------------- | ------------------------------- |
| Queue 中同一處理即便存在 2 件以上也無意義 | `ShouldBeUnique`                |
| 連處理中也希望防止並行執行             | `ShouldBeUnique`                |
| Worker 取出後想立即放入下一個        | `ShouldBeUniqueUntilProcessing` |
| 即便使用者連按儲存按鈕也希望只執行 1 次     | `#[DebounceFor]`                |
| 每次模型更新都重建搜尋索引（大量更新時）      | `#[DebounceFor]` + `maxWait`    |

***

## 內部實作

### Unique Jobs 的 lock 機制

當 `ShouldBeUnique` Job 被 dispatch 時，Laravel 內部會取得 cache 的 [Atomic Lock](/zh-TW/cache#atomic-操作lock)。lock key 格式如下：

```
laravel_unique_job:{Job 類別名}:{uniqueId()}
```

若未能取得 lock（已被其他 Job 持有），Job 便不會加入 Queue。

### Debounced Jobs 的實作

`DebounceFor` 內部使用管理「Debounce 視窗」的 cache 項目。每次有新的 dispatch 進來時：

1. 從 Queue 移除既有 Job（發出 `JobDebounced` 事件）
2. 將新 Job 加入 Queue（附帶 Debounce 秒數的延遲）
3. 重置 cache 的計時器

若有指定 `maxWait`，也會記錄初次 dispatch 的時間戳記，防止超過該時刻起 `maxWait` 秒的 Debounce。

***

## 參考連結

* [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/13.x/src/Illuminate/Contracts/Queue/ShouldBeUnique.php)
* [`Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Contracts/Queue/ShouldBeUniqueUntilProcessing.php)
* [`Illuminate\Queue\Attributes\DebounceFor`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Queue/Attributes/DebounceFor.php)
* [`Illuminate\Queue\Attributes\UniqueFor`](https://github.com/laravel/framework/blob/13.x/src/Illuminate/Queue/Attributes/UniqueFor.php)


## Related topics

- [任務排程](/zh-TW/scheduling.md)
- [Queue 與 Job](/zh-TW/queues.md)
- [Laravel Nightwatch 入門](/zh-TW/blog/nightwatch-introduction.md)
- [Session Hook](/zh-TW/packages/laravel-copilot-sdk/hooks.md)
- [Laravel Package Skeleton — 官方套件用起始模板](/zh-TW/blog/package-skeleton-introduction.md)
