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

# Contrôle de l'exécution des jobs de queue

> Découvrez comment contrôler les exécutions dupliquées ou les dispatches successifs de jobs de queue à l'aide de ShouldBeUnique, ShouldBeUniqueUntilProcessing et DebounceFor.

## Aperçu

La queue de Laravel propose deux mécanismes de contrôle d'exécution des jobs : la **déduplication (Unique)** et le **debounce**. Les deux visent à « éviter d'exécuter le même job plusieurs fois lorsqu'il est dispatché à répétition », mais leur comportement diffère.

| Fonctionnalité          | Interface / attribut            | Objectif                                                                  |
| ----------------------- | ------------------------------- | ------------------------------------------------------------------------- |
| Unique Jobs             | `ShouldBeUnique`                | Ne conserver qu'une seule instance du job en queue                        |
| Unique Until Processing | `ShouldBeUniqueUntilProcessing` | Maintenir la contrainte d'unicité uniquement jusqu'au début du traitement |
| Debounced Jobs          | `#[DebounceFor]`                | Lors de dispatches successifs en peu de temps, n'exécuter que le dernier  |

<Warning>
  Unique Jobs et Debounced Jobs sont **mutuellement exclusifs**. N'implémentez pas `ShouldBeUnique` sur un job qui utilise l'attribut `DebounceFor`.
</Warning>

***

## Unique Jobs — `ShouldBeUnique`

Tant qu'un même job est présent dans la queue, tout dispatch supplémentaire est ignoré.

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

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

class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    // Aucune méthode supplémentaire nécessaire
}
```

Tant que `UpdateSearchIndex` est en attente (ou en cours de traitement), toute tentative de dispatcher le même job est ignorée.

### Restreindre la contrainte d'unicité par clé — `UniqueFor` + `uniqueId()`

Si vous souhaitez que « la mise à jour du produit A » et « la mise à jour du produit B » soient traitées comme deux jobs distincts même si la classe est identique, définissez la clé via la méthode `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)] // Verrou libéré automatiquement au bout d'une heure
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
    public function __construct(public readonly int $productId)
    {
    }

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

* La valeur retournée par `uniqueId()` sert de clé au verrou de cache.
* Avec `#[UniqueFor(secondes)]`, le verrou est libéré automatiquement à l'expiration du délai indiqué (filet de sécurité au cas où le job ne serait pas traité).

### Choisir le driver de cache — `uniqueVia()`

Pour utiliser un autre driver de cache que celui par défaut, implémentez `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 requiert un driver de cache prenant en charge les verrous atomiques (`redis`, `database`, `memcached`, `dynamodb`, `file`, `array`).
</Info>

***

## `ShouldBeUnique` vs `ShouldBeUniqueUntilProcessing`

Le verrou de `ShouldBeUnique` est conservé **jusqu'à ce que le job soit terminé ou ait atteint son nombre maximal de tentatives**, ce qui peut poser problème.

**Exemple :** un seul `UpdateSearchIndex(product_id: 42)` est présent dans la queue et vous voulez pouvoir redispatcher le même job juste après que le worker a commencé son traitement. Avec `ShouldBeUnique`, le deuxième job ne peut pas rejoindre la queue avant la fin du traitement.

Dans ce cas, utilisez `ShouldBeUniqueUntilProcessing`. Le verrou est **libéré juste avant le début du traitement**, ce qui autorise un nouveau dispatch dès que le worker prend en charge le job.

```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() — acquisition du verrou
    D->>Q: dispatch() — verrou présent → ignoré

    note over Q,W: Cas ShouldBeUnique
    W->>Q: récupération du job
    W->>W: traitement en cours (verrou conservé)
    D->>Q: dispatch() — verrou présent → ignoré
    W->>W: traitement terminé — verrou libéré
    D->>Q: dispatch() — enfin possible de mettre en queue

    note over Q,W: Cas ShouldBeUniqueUntilProcessing
    W->>Q: récupération du job — verrou libéré
    D->>Q: dispatch() — plus de verrou → mise en queue possible
    W->>W: traitement en cours
```

### Récapitulatif comparatif

|                                         | `ShouldBeUnique`                                | `ShouldBeUniqueUntilProcessing`                     |
| --------------------------------------- | ----------------------------------------------- | --------------------------------------------------- |
| Libération du verrou                    | Après traitement / échec                        | Juste avant le début du traitement                  |
| Dispatch dupliqué pendant le traitement | Ignoré                                          | Mise en queue possible                              |
| Cas d'usage                             | Empêcher totalement toute exécution concurrente | Réinsérer un nouveau job dès le début du traitement |

***

## Debounced Jobs — `#[DebounceFor]`

<Info>
  L'attribut `DebounceFor` a été ajouté dans Laravel 13.
</Info>

Si un même job est dispatché en grande quantité sur une courte période, **seul le dernier job dispatché** est exécuté. C'est la même idée que le debounce côté frontend.

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

namespace App\Jobs;

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

#[DebounceFor(30)] // Les dispatches dans les 30 s sont ignorés (seul le dernier s'exécute)
class UpdateSearchIndex implements ShouldQueue
{
    use Queueable;

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

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

* La valeur retournée par `debounceId()` identifie le job (chaque `productId` applique un debounce indépendant).
* Même si 10 dispatches ont lieu en 30 secondes avec le même `productId`, seul le dernier sera exécuté.

### `maxWait` — délai d'attente maximum

Pour des données mises à jour très fréquemment, le debounce risque de se prolonger indéfiniment et le job de ne jamais être exécuté. L'option `maxWait` définit un délai maximal.

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

Dans cet exemple, le job sera nécessairement exécuté au plus tard 120 s après le premier dispatch (même si des debounces successifs de 30 s continuent, le délai est plafonné à 120 s).

### Choisir le driver de cache — `debounceVia()`

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

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

### L'événement `JobDebounced`

Un job écrasé par un dispatch ultérieur émet l'événement `Illuminate\Queue\Events\JobDebounced` et est retiré de la queue. En écoutant cet événement, vous pouvez tracer et monitorer les jobs mis en debounce.

***

## Lequel choisir ?

```mermaid theme={null}
flowchart TD
    A["Le même job peut être dispatché plusieurs fois"] --> B{"Souhaitez-vous n'exécuter que le dernier<br>en cas de dispatches successifs ?"}
    B -->|Oui| C["#[DebounceFor]"]
    B -->|Non| D{"Souhaitez-vous n'avoir qu'un<br>seul job dans la queue ?"}
    D -->|Oui| E{"Empêcher aussi les doublons<br>pendant le traitement ?"}
    E -->|Oui| F["ShouldBeUnique"]
    E -->|Non| G["ShouldBeUniqueUntilProcessing"]
    D -->|Non| H["Job classique"]
```

| Cas d'usage                                                                                  | Recommandation                  |
| -------------------------------------------------------------------------------------------- | ------------------------------- |
| Aucun intérêt d'avoir plusieurs fois le même job en queue                                    | `ShouldBeUnique`                |
| Empêcher l'exécution en parallèle, y compris pendant le traitement                           | `ShouldBeUnique`                |
| Autoriser un nouveau job dès que le worker prend le précédent                                | `ShouldBeUniqueUntilProcessing` |
| N'exécuter qu'une seule fois même si l'utilisateur clique frénétiquement sur « Enregistrer » | `#[DebounceFor]`                |
| Rebâtir l'index de recherche à chaque mise à jour de modèle (montée en charge)               | `#[DebounceFor]` + `maxWait`    |

***

## Implémentation interne

### Verrouillage des Unique Jobs

Lorsqu'un job `ShouldBeUnique` est dispatché, Laravel acquiert en interne un [verrou atomique](/fr/cache#operations-atomiques-verrous) du cache. La clé de verrou est de la forme :

```
laravel_unique_job:{nom de la classe de job}:{uniqueId()}
```

Si le verrou ne peut pas être obtenu (un autre job le détient déjà), le job n'est pas ajouté à la queue.

### Implémentation des Debounced Jobs

`DebounceFor` s'appuie en interne sur une entrée de cache qui gère la « fenêtre de debounce ». À chaque nouveau dispatch :

1. Le job existant est retiré de la queue (émission de l'événement `JobDebounced`).
2. Le nouveau job est ajouté à la queue (avec un délai égal au nombre de secondes du debounce).
3. Le minuteur en cache est réinitialisé.

Si `maxWait` est défini, l'horodatage du premier dispatch est également enregistré, ce qui empêche le debounce de dépasser `maxWait` secondes à partir de ce premier moment.

***

## Liens de référence

* [Documentation officielle Laravel — Unique Jobs](https://laravel.com/docs/queues#unique-jobs)
* [Documentation officielle 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

- [Context](/fr/context.md)
- [Techniques pratiques de Laravel Telescope](/fr/blog/telescope-introduction.md)
- [Queues et jobs](/fr/queues.md)
- [Tests HTTP](/fr/http-tests.md)
- [laravel/symfony-on-cloud — faire tourner une application Symfony sur Laravel Cloud](/fr/blog/symfony-on-cloud-introduction.md)
