
# Промежуточное ПО

Промежуточное ПО оборачивает выполнение задания в конвейер с последовательной обработкой. Каждый компонент может выполнять логику до и после задания, останавливать выполнение или перехватывать исключения. Queuety включает пять встроенных компонентов промежуточного ПО и поддерживает пользовательские компоненты через интерфейс `Contracts\Middleware`.

Промежуточное ПО доступно для [классов заданий с возможностью диспетчеризации](https://queuety.inline0.dev/ru/docs/jobs#dispatchable-job-classes) (классов, реализующих `Contracts\Job` с трейтом `Dispatchable`).

## Интерфейс `Contracts\Middleware`

```php
use Queuety\Contracts\Middleware;

interface Middleware {
    public function handle( object $job, \Closure $next ): void;
}
```

Параметр `$job` — экземпляр обрабатываемого задания. Вызовите `$next( $job )`, чтобы передать выполнение следующему компоненту в конвейере (или методу `handle()` задания, если это последний компонент).

## Добавление промежуточного ПО к заданию

Определите метод `middleware()` в классе задания, возвращающий массив экземпляров промежуточного ПО:

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;
use Queuety\Middleware\RateLimited;
use Queuety\Middleware\Timeout;

class SyncInventoryJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly string $sku,
    ) {}

    public function handle(): void {
        // Sync inventory for this SKU...
    }

    public function middleware(): array {
        return [
            new Timeout( seconds: 30 ),
            new RateLimited( max: 100, window: 60 ),
        ];
    }
}
```

## Порядок выполнения конвейера

Компоненты промежуточного ПО применяются в порядке массива; первый компонент образует внешний слой. Если метод `middleware()` возвращает `[A, B, C]`, порядок выполнения будет таким:

```
A::handle() -> B::handle() -> C::handle() -> $job->handle()
```

Каждый компонент оборачивает следующий. `A` выполняется первым перед заданием и последним после него, поэтому подходит для замеров времени или обёрток try/catch.

## Создание пользовательского промежуточного ПО

Реализуйте интерфейс `Contracts\Middleware`:

```php
use Queuety\Contracts\Middleware;

class LogExecution implements Middleware {
    public function handle( object $job, \Closure $next ): void {
        $class = get_class( $job );
        error_log( "[Queuety] Starting: {$class}" );

        $start = microtime( true );

        try {
            $next( $job );
        } finally {
            $elapsed = round( microtime( true ) - $start, 3 );
            error_log( "[Queuety] Finished: {$class} in {$elapsed}s" );
        }
    }
}
```

Используйте его в задании:

```php
public function middleware(): array {
    return [
        new LogExecution(),
    ];
}
```

### Прерывание выполнения

Компонент промежуточного ПО может полностью пропустить выполнение задания, если не вызвать `$next()`:

```php
class SkipOnMaintenanceMode implements Middleware {
    public function handle( object $job, \Closure $next ): void {
        if ( wp_is_maintenance_mode() ) {
            return; // Job silently skipped.
        }

        $next( $job );
    }
}
```

## Встроенное промежуточное ПО

### `ThrottlesExceptions`

Ограничивает исключения, возникающие в обработчике задания, чтобы предотвратить массовые сбои заданий. Если обработчик выбросил слишком много исключений за заданный период, последующие задания для этого обработчика отклоняются с `RuntimeException` вместо выполнения. Это не позволяет сбоям внешней службы израсходовать все попытки повтора сразу для множества заданий.

```php
use Queuety\Middleware\ThrottlesExceptions;

public function middleware(): array {
    return [
        new ThrottlesExceptions( max_attempts: 10, decay_minutes: 10 ),
    ];
}
```

| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
| `$max_attempts` | `int` | `10` | Максимальное число исключений в окне затухания |
| `$decay_minutes` | `int` | `10` | Длительность окна в минутах |

Счётчики исключений ведутся отдельно для каждого класса обработчика в таблице блокировок Queuety и доступны всем рабочим процессам. При достижении порога компонент выбрасывает `RuntimeException`, не вызывая задание, а рабочий процесс обрабатывает его как любую другую ошибку: попытка учитывается в `max_attempts`, задание повторяется с обычной задержкой и перемещается в архив после исчерпания попыток.

Пример: если внешняя служба API недоступна, первые 10 ошибок обрабатываются обычным образом (каждая увеличивает счётчик исключений). Последующие попытки быстро завершаются с ошибкой, не вызывая API, пока не пройдут 10 минут без нового исключения; при каждом зарегистрированном исключении отсчёт окна начинается заново.

### `RateLimited`

Ограничивает число выполнений за заданный период. Если ограничение превышено внутри промежуточного ПО, выбрасывается `RateLimitExceededException`; рабочий процесс обрабатывает его как обычную неудачную попытку, повторяет задание с обычной задержкой и учитывает попытку в `max_attempts`. После регистрации ограничения в ограничителе частоты рабочего процесса последующие задания этого обработчика также возвращаются в `pending` до запуска, пока ограничение действует (см. раздел [Ограничение частоты](https://queuety.inline0.dev/ru/docs/features/rate-limiting)).

```php
use Queuety\Middleware\RateLimited;

public function middleware(): array {
    return [
        new RateLimited( max: 60, window: 60 ), // 60 per minute
    ];
}
```

| Параметр | Тип | Описание |
|---|---|---|
| `$max` | `int` | Максимальное число выполнений в окне |
| `$window` | `int` | Длительность окна в секундах |

Ограничитель частоты использует внутренний экземпляр Queuety `RateLimiter`, поэтому ограничения действуют для всех рабочих процессов.

### `Timeout`

Устанавливает тайм-аут выполнения отдельного задания с помощью `pcntl_alarm` и предназначен для выбрасывания `TimeoutException`, если задание выполняется слишком долго. Сейчас тайм-аут не применяется ([#10](https://github.com/inline0/queuety/issues/10)): Queuety не включает асинхронные сигналы, поэтому обработчик сигнала тревоги не запускается во время выполнения задания. Кроме того, по завершении компонент сбрасывает сигнал тревоги, отменяя собственный сигнал тайм-аута задания рабочего процесса.

```php
use Queuety\Middleware\Timeout;

public function middleware(): array {
    return [
        new Timeout( seconds: 30 ),
    ];
}
```

| Параметр | Тип | Описание |
|---|---|---|
| `$seconds` | `int` | Максимальное время выполнения в секундах |

Требуется расширение `pcntl` PHP. Если `pcntl` недоступно, промежуточное ПО молча продолжает выполнение (тайм-аут не применяется).

### `UniqueJob`

Запрещает одновременное выполнение заданий с одинаковым ключом. Использует таблицу блокировок базы данных, чтобы одновременно выполнялся только один экземпляр. Если блокировка уже установлена, задание молча пропускается (без повтора).

```php
use Queuety\Middleware\UniqueJob;

public function middleware(): array {
    return [
        new UniqueJob(),                         // Lock key defaults to the job class name
        new UniqueJob( key: 'sync_inventory' ),   // Custom lock key
    ];
}
```

| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
| `$key` | `string\|null` | `null` (используется имя класса) | Ключ уникальности блокировки |

### `WithoutOverlapping`

Не допускает одновременного выполнения заданий с одинаковым логическим ключом и автоматически снимает блокировку, чтобы избежать зависших блокировок при сбое рабочего процесса. Похоже на `UniqueJob`, но предназначено для длительных операций, где блокировка должна сниматься автоматически по тайм-ауту.

```php
use Queuety\Middleware\WithoutOverlapping;

public function middleware(): array {
    return [
        new WithoutOverlapping( key: 'import_products', release_after: 600 ),
    ];
}
```

| Параметр | Тип | По умолчанию | Описание |
|---|---|---|---|
| `$key` | `string` | (обязательно) | Ключ логической операции |
| `$release_after` | `int` | `300` | Максимальная длительность блокировки в секундах до автоматического снятия |

Если блокировка уже установлена, задание молча пропускается. Просроченные блокировки автоматически очищаются перед каждой попыткой установки блокировки.

## Пример: сочетание компонентов промежуточного ПО

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;
use Queuety\Middleware\RateLimited;
use Queuety\Middleware\Timeout;
use Queuety\Middleware\WithoutOverlapping;

class ImportProductsJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly int $supplier_id,
    ) {}

    public function handle(): void {
        // Fetch and import products from the supplier...
    }

    public function middleware(): array {
        return [
            new Timeout( seconds: 300 ),
            new WithoutOverlapping(
                key: "import_supplier_{$this->supplier_id}",
                release_after: 600,
            ),
            new RateLimited( max: 5, window: 60 ),
        ];
    }
}
```

Это задание будет:

1. Принудительно завершено, если работает дольше 5 минут (Timeout)
2. Пропущено, если уже выполняется другой импорт от того же поставщика (WithoutOverlapping)
3. Ограничено пятью импортами в минуту от всех поставщиков (RateLimited)
