
# 任务

任务是 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` | 用于加权队列或提供方预算、工作流预算和准入启发式的相对执行成本 |

这些资源键属于 Queuety 更广泛的[资源管理](https://queuety.inline0.dev/zh/docs/features/resource-management)模型，在该模型中，任务元数据、工作流预算和工作进程准入机制协同运作。

注册处理器：

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

也可以使用 [PHP 属性](https://queuety.inline0.dev/zh/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` | 先于 Low 和 Normal 处理。 |
| `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/zh/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/zh/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` trait

将 `Dispatchable` trait 添加到任务类，即可获得静态 `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` trait 会在内部调用 `Queuety::dispatch()`，因此所有 `PendingJob` 选项（queue、priority、delay、max_attempts、unique、after）的工作方式都完全相同。

### 序列化的工作方式

派发 `Job` 实例时，Queuety 的 `JobSerializer` 会提取完全限定类名作为处理器，并将所有公共属性作为载荷。在工作进程端，序列化器会根据载荷键与构造函数参数名的对应关系重建实例。带后备值的枚举会被序列化为其底层值，并自动还原。

### 作业属性

任务类可以声明公共属性，以控制重试和超时行为。工作进程会通过反射读取这些属性并自动应用。

```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/zh/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/zh/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` trait 提供 `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 );
```

条件为 `true` 时，`dispatch_if()`会派发任务并返回`PendingJob`。条件为`false`时，则返回`null`。

条件为 `false` 时，`dispatch_unless()`会派发任务并返回`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` trait 提供 `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/zh/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` | 所有重试次数均已用尽 |
