
# Задачи

Задачи — основная единица работы в Queuety. Отправьте задачу с именем обработчика и данными, и воркер получит её и выполнит в фоновом режиме.

## Отправка задач

```php
use Queuety\Queuety;

Queuety::dispatch( 'send_email', [ 'to' => 'user@example.com' ] );
```

`dispatch()` возвращает построитель `PendingJob`. Перед автоматической отправкой можно последовательно добавить параметры:

```php
use Queuety\Enums\Priority;

Queuety::dispatch( 'send_email', [ 'to' => 'user@example.com' ] )
    ->on_queue( 'emails' )
    ->with_priority( Priority::High )
    ->delay( 300 )
    ->max_attempts( 5 );
```

Чтобы сразу получить ID задачи, вызовите `->id()`:

```php
$job_id = Queuety::dispatch( 'send_email', $payload )->id();
```

## Написание обработчиков

Обработчиком может быть любой класс, реализующий интерфейс `Queuety\Handler`:

```php
use Queuety\Handler;

class SendEmailHandler implements Handler {
    public function handle( array $payload ): void {
        wp_mail( $payload['to'], $payload['subject'], $payload['body'] );
    }

    public function config(): array {
        return [
            'queue'           => 'emails',
            'max_attempts'    => 5,
            'backoff'         => 'exponential',
        ];
    }
}
```

Метод `handle()` получает данные задачи и выполняет работу. Метод `config()` возвращает необязательные значения по умолчанию:

| Ключ | Тип | Описание |
|---|---|---|
| `queue` | `string` | Имя очереди по умолчанию |
| `max_attempts` | `int` | Максимальное число запусков, включая первый, после которого задача перемещается в архив |
| `backoff` | `string` | Стратегия задержки между повторами: `exponential`, `linear` или `fixed` |
| `rate_limit` | `array` | Ограничение частоты запросов в виде `[max_executions, window_seconds]` |
| `concurrency_group` | `string` | Общая группа допуска воркеров для ресурсоёмких обработчиков |
| `concurrency_limit` | `int` | Максимальное число одновременно обрабатываемых задач в этой группе |
| `cost_units` | `int` | Относительная стоимость выполнения, используемая при взвешивании очереди или бюджетов провайдера, бюджетов рабочего процесса и эвристик допуска |

Эти ключи ресурсов входят в общую модель [Управления ресурсами](https://queuety.inline0.dev/ru/docs/features/resource-management) в Queuety, где метаданные задач, бюджеты рабочего процесса и допуск воркеров работают вместе.

Зарегистрируйте обработчик:

```php
Queuety::register( 'send_email', SendEmailHandler::class );
```

Или используйте атрибуты [PHP](https://queuety.inline0.dev/ru/docs/features/attributes) для автоматической регистрации.

## Приоритет

У задач есть четыре уровня приоритета. Воркеры в первую очередь получают задачи с более высоким приоритетом.

```php
use Queuety\Enums\Priority;

Queuety::dispatch( 'handler', $payload )->with_priority( Priority::Urgent );
```

| Уровень | Значение | Описание |
|---|---|---|
| `Priority::Low` | `0` | По умолчанию. Фоновая работа, которая может подождать. |
| `Priority::Normal` | `1` | Стандартный приоритет. |
| `Priority::High` | `2` | Обрабатывается раньше задач с низким и обычным приоритетом. |
| `Priority::Urgent` | `3` | Обрабатывается первой. Используйте умеренно. |

## Задержка

Задайте задержку задачи, чтобы она стала доступна только по истечении указанного числа секунд:

```php
Queuety::dispatch( 'send_reminder', $payload )->delay( 3600 ); // 1 hour
```

Задача добавляется сразу, но воркеры не получат её, пока не истечёт задержка.

## Уникальные задачи

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

```php
Queuety::dispatch( 'sync_inventory', [ 'sku' => 'ABC-123' ] )->unique();
```

## Зависимости задач

Задача может зависеть от другой задачи. Воркер не получит её, пока не завершится задача-зависимость.

```php
$first_id = Queuety::dispatch( 'import_data', $payload )->id();

Queuety::dispatch( 'send_notification', [ 'import_id' => $first_id ] )
    ->after( $first_id );
```

## Пакетная отправка

Отправляйте несколько задач одним многострочным запросом INSERT для максимальной пропускной способности:

```php
$job_ids = Queuety::batch( [
    [ 'handler' => 'send_email', 'payload' => [ 'to' => 'a@example.com' ] ],
    [ 'handler' => 'send_email', 'payload' => [ 'to' => 'b@example.com' ] ],
    [ 'handler' => 'send_email', 'payload' => [ 'to' => 'c@example.com' ] ],
] );
```

Каждый элемент может содержать необязательные ключи: `queue`, `priority`, `delay`, `max_attempts`.

## Повторные попытки и задержка

При возникновении исключения в задаче Queuety возвращает её в `pending`, откладывая `available_at` до тех пор, пока общее число запусков не достигнет `max_attempts` (первый запуск учитывается, поэтому `max_attempts( 3 )` означает не более двух повторных попыток). Задержка перед каждой повторной попыткой определяется стратегией задержки, где `attempt` — номер завершившегося с ошибкой запуска:

| Стратегия | Формула задержки | Пример (после запусков 1, 2, 3) |
|---|---|---|
| `exponential` | `2^attempt * 30` секунд, не более 3600 | 60s, 120s, 240s |
| `linear` | `attempt * 60` секунд | 60s, 120s, 180s |
| `fixed` | `60` секунд | 60s, 60s, 60s |

Задайте глобальное значение по умолчанию в `wp-config.php`:

```php
define( 'QUEUETY_RETRY_BACKOFF', 'linear' );
```

Или задайте его отдельно для каждого обработчика в `config()`:

```php
public function config(): array {
    return [ 'backoff' => 'exponential', 'max_attempts' => 5 ];
}
```

Или задайте его при отправке:

```php
Queuety::dispatch( 'handler', $payload )->max_attempts( 10 );
```

## Отложенные задания

После исчерпания всех попыток задача помечается как **архивная**. Архивные задачи остаются в базе данных для проверки, но не обрабатываются. Их можно повторить вручную. При ручном повторе задача снова `pending` без сброса счётчика попыток, поэтому архивная задача получает ещё один запуск и снова перемещается в архив, если этот запуск завершится с ошибкой:

```php
// Retry a specific buried job
Queuety::retry( $job_id );

// Retry all buried jobs
$count = Queuety::retry_buried();
```

Или через CLI:

```bash
wp queuety retry 42
wp queuety retry-buried
```

## Тайм-аут

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

```php
define( 'QUEUETY_MAX_EXECUTION_TIME', 300 ); // 5 minutes
```

Задачи, которые остаются в статусе `processing` дольше `QUEUETY_STALE_TIMEOUT` секунд, автоматически восстанавливаются:

```php
define( 'QUEUETY_STALE_TIMEOUT', 600 ); // 10 minutes
```

Восстановите устаревшие задачи вручную:

```bash
wp queuety recover
```

## Очереди

Задачи отправляются в именованные очереди. Каждый воркер обрабатывает одну очередь или упорядоченный список очередей (см. [Воркеры](https://queuety.inline0.dev/ru/docs/features/workers#multi-queue-priorities)):

```php
Queuety::dispatch( 'handler', $payload )->on_queue( 'emails' );
```

```bash
wp queuety work --queue=emails
```

Очередь по умолчанию — `default`. Для изоляции нагрузок можно запустить отдельные процессы воркеров для каждой очереди.

## Ограничение частоты запросов

Ограничьте частоту выполнения обработчика в заданном временном интервале:

```php
Queuety::dispatch( 'call_api', $payload )
    ->rate_limit( 10, 60 ); // max 10 executions per 60 seconds
```

Подробности см. в разделе [Ограничение частоты запросов](https://queuety.inline0.dev/ru/docs/features/rate-limiting).

## Классы задач с поддержкой отправки

Queuety v0.6.0 добавляет основанный на классах механизм отправки API задач. Вместо того чтобы разносить имя обработчика, данные и класс обработчика, вы создаёте один класс, содержащий всё необходимое: данные (в виде типизированных публичных свойств), логику выполнения и необязательное промежуточное ПО.

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

```php
use Queuety\Contracts\Job;

interface Job {
    public function handle(): void;
}
```

Класс `Job` хранит данные в виде публичных свойств и выполняет собственную логику в `handle()`. Отдельный метод `config()` или регистрация обработчика не требуются.

### Трейт `Dispatchable`

Добавьте трейт `Dispatchable`, чтобы получить статический метод `dispatch()` в классе задачи:

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;

class SendEmailJob implements Job {
    use Dispatchable;

    public function __construct(
        public readonly string $to,
        public readonly string $subject,
        public readonly string $body,
    ) {}

    public function handle(): void {
        wp_mail( $this->to, $this->subject, $this->body );
    }
}
```

Отправьте задачу, передав аргументы конструктора:

```php
SendEmailJob::dispatch( 'user@example.com', 'Welcome!', 'Hello there.' )
    ->on_queue( 'emails' )
    ->with_priority( Priority::High );
```

Трейт `Dispatchable` внутри вызывает `Queuety::dispatch()`, поэтому все параметры `PendingJob` (queue, priority, delay, max_attempts, unique, after) работают точно так же.

### Как работает сериализация

При отправке экземпляра `Job` метод `JobSerializer` в Queuety извлекает полное имя класса в качестве обработчика и все публичные свойства в качестве данных. На стороне воркера сериализатор восстанавливает экземпляр, сопоставляя ключи данных с именами параметров конструктора. Перечисления с базовым типом сериализуются в базовое значение и автоматически восстанавливаются.

### Свойства задания

Классы задач могут объявлять публичные свойства для настройки повторных попыток и тайм-аутов. Воркер считывает их с помощью рефлексии и применяет автоматически.

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;

class ImportProductsJob implements Job {
    use Dispatchable;

    public int $tries = 5;
    public int $timeout = 120;
    public array $backoff = [ 10, 30, 60, 120 ];
    public string $concurrency_group = 'imports';
    public int $concurrency_limit = 2;
    public int $cost_units = 4;

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

    public function handle(): void {
        // Import products...
    }
}
```

| Свойство | Тип | Описание |
|---|---|---|
| `$tries` | `int` | Максимальное число попыток до перемещения задачи в архив. Переопределяет значение `max_attempts`, заданное при отправке. |
| `$timeout` | `int` | Максимальное время выполнения в секундах. Переопределяет глобальную константу `QUEUETY_MAX_EXECUTION_TIME`. Требует `pcntl` и пока не применяется (см. [Тайм-аут](https://queuety.inline0.dev/ru/docs/jobs#timeout)). |
| `$backoff` | `array` | Массив значений задержки в секундах для каждой повторной попытки. Для последующих попыток используется последнее значение. |
| `$concurrency_group` | `string` | Общая группа допуска воркеров для ресурсоёмких задач. |
| `$concurrency_limit` | `int` | Максимальное число одновременно обрабатываемых задач `processing` в этой группе. Игнорируется без `$concurrency_group`. |
| `$cost_units` | `int` | Относительная стоимость выполнения, используемая бюджетами рабочих процессов и допуском воркеров. |

Массив `$backoff` задаёт увеличивающиеся задержки между повторными попытками. Например, `[10, 30, 60, 120]` означает, что первая повторная попытка начнётся через 10 секунд, вторая — через 30 секунд, третья — через 60 секунд, а все последующие — через 120 секунд.

Свойства, связанные с ресурсами, полезны, когда классы задач имеют разную стоимость выполнения. Обновление кэша и крупный вызов провайдера могут считаться «одной задачей», но их не всегда следует учитывать одинаково, если бюджет рабочего процесса или решение о допуске воркера должны удерживать процесс в безопасных пределах.

Полную модель, включая бюджеты рабочих процессов и допуск на стороне воркеров, см. в разделе [Управление ресурсами](https://queuety.inline0.dev/ru/docs/features/resource-management).

### Хук сбоя

Когда задача перемещается в архив после исчерпания всех попыток, Queuety вызывает метод `failed()` у экземпляра задачи, если он определён. Используйте его для очистки, отправки оповещений или ведения журнала.

```php
use Queuety\Contracts\Job;
use Queuety\Dispatchable;

class ProcessPaymentJob implements Job {
    use Dispatchable;

    public int $tries = 3;

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

    public function handle(): void {
        // Process the payment...
    }

    public function failed( \Throwable $exception ): void {
        // Notify the team that payment processing failed permanently.
        error_log( "Payment failed for order {$this->order_id}: {$exception->getMessage()}" );
    }
}
```

Метод `failed()` получает исключение, вызвавшее окончательный сбой. Экземпляр задачи десериализуется с исходными данными, поэтому доступны все публичные свойства.

### Условная отправка

Трейт `Dispatchable` предоставляет методы `dispatch_if()` и `dispatch_unless()` для условной отправки. Оба принимают логическое условие первым аргументом, а затем аргументы конструктора задачи.

```php
// Only dispatch if the user has opted in to notifications.
SendEmailJob::dispatch_if( $user->wants_notifications, $user->email, 'Update', $body );

// Dispatch unless the feature flag is disabled.
SyncInventoryJob::dispatch_unless( $maintenance_mode, $sku );
```

`dispatch_if()` отправляет задачу, если условие равно `true`, и возвращает `PendingJob`. Если условие равно `false`, возвращает `null`.

`dispatch_unless()` отправляет задачу, если условие равно `false`, и возвращает `PendingJob`. Если условие равно `true`, возвращает `null`.

Оба метода поддерживают добавление параметров `PendingJob` при отправке:

```php
SendEmailJob::dispatch_if( $should_send, $to, $subject, $body )
    ?->on_queue( 'emails' )
    ?->with_priority( Priority::High );
```

### Синхронная отправка

Используйте `Queuety::dispatch_sync()`, чтобы сразу выполнить задачу в текущем процессе, не добавляя её в очередь. Это полезно при тестировании и для задач, которые должны выполняться непосредственно.

```php
use Queuety\Queuety;

// With a Job instance
Queuety::dispatch_sync( new SendEmailJob( to: 'user@example.com', subject: 'Hello', body: 'Hi!' ) );

// With a handler name and payload
Queuety::dispatch_sync( 'send_email', [ 'to' => 'user@example.com' ] );
```

Метод `handle()` задачи вызывается напрямую. Строка в базе данных не создаётся, рабочий процесс не запускается, промежуточные обработчики не применяются.

### Цепочки задач

Цепочка задач последовательно выполняет задачи одну за другой. Каждая задача в цепочке зависит от успешного завершения предыдущей. Если задача завершается с ошибкой, последующие задачи в цепочке остаются в ожидании.

#### Использование `Queuety::chain()`

```php
use Queuety\Queuety;

$first_job_id = Queuety::chain( [
    new FetchDataJob( $url ),
    new ProcessDataJob(),
    new NotifyCompleteJob(),
] )
    ->on_queue( 'pipeline' )
    ->catch( ChainFailedHandler::class )
    ->dispatch();
```

`Queuety::chain()` возвращает `ChainBuilder` с цепочкой методов для настройки очереди и обработчика ошибок. Метод `dispatch()` создаёт все задачи с последовательными связями `depends_on` и возвращает ID первой задачи.

#### Использование `with_chain()`

Трейт `Dispatchable` предоставляет метод `with_chain()`. Сейчас он эквивалентен `Queuety::chain( $jobs )`: вызываемый класс не добавляется в цепочку ([#13](https://github.com/inline0/queuety/issues/13)), поэтому укажите в массиве и первую задачу:

```php
FetchDataJob::with_chain( [
    new FetchDataJob(),
    new ProcessDataJob(),
    new NotifyCompleteJob(),
] )
    ->on_queue( 'pipeline' )
    ->dispatch();
```

#### Обработка ошибок цепочки

Зарегистрируйте класс обработчика ошибок с помощью `catch()`. В классе обработчика должен быть метод `handle()`. Он вызывается, когда любая задача в цепочке окончательно перемещается в архив. Воркер поддерживает `handle()`, `handle( Job $job )` и `handle( Job $job, \Throwable $exception )`.

```php
class ChainFailedHandler {
    public function handle(): void {
        // Alert, clean up, etc.
    }
}
```

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

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

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

class CallOpenAIJob implements Job {
    use Dispatchable;

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

    public function handle(): void {
        // Call the API...
    }

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

Подробности о встроенном промежуточном ПО и создании собственного см. в разделе [Промежуточное ПО](https://queuety.inline0.dev/ru/docs/features/middleware).

### Сравнение с задачами на основе обработчиков

| | На основе обработчиков (v0.1+) | На основе классов (v0.6+) |
|---|---|---|
| Диспетчеризация | `Queuety::dispatch( 'send_email', $payload )` | `SendEmailJob::dispatch( $to, $subject )` |
| Обработчик | Отдельный класс, реализующий `Handler` | Тот же класс реализует `Contracts\Job` |
| Полезная нагрузка | Нетипизированная `array $payload` | Типизированные публичные свойства |
| Регистрация | `Queuety::register()` или `#[QueuetyHandler]` | Не требуется (используется FQCN) |
| Промежуточное ПО | Недоступно | Необязательный метод `middleware()` |
| Конфигурация | Метод `config()` | Свойства задания + цепочка `PendingJob` |

Оба подхода полностью поддерживаются. Существующие задания на основе обработчиков продолжают работать без изменений. В одном проекте можно сочетать оба стиля.

## Жизненный цикл задания

```
dispatch() -> pending -> processing -> completed
                  |           |
                  |           +-> exception -> pending (retry after backoff)
                  |                       |
                  |                       +-> buried (max attempts reached)
                  |
                  +-> delayed (available_at in the future)
```

| Статус | Описание |
|---|---|
| `pending` | Ожидает, пока его заберёт рабочий процесс |
| `processing` | Выполняется в данный момент |
| `completed` | Успешно завершено |
| `failed` | Зарезервировано; сейчас рабочий процесс не устанавливает этот статус. Неудачная попытка, которую будут повторять, возвращается в `pending` |
| `buried` | Все попытки повтора исчерпаны |
