
# Проміжне ПЗ

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

Проміжне ПЗ доступне для класів завдань, які можна запускати через [диспетчер](https://queuety.inline0.dev/uk/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/uk/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. Обмежуватиметься до 5 імпортів на хвилину для всіх постачальників (RateLimited)
