
# 中间件

中间件会以洋葱式管道包装作业执行。每个中间件都可以在作业执行前后运行逻辑、短路执行或捕获异常。Queuety 内置了五种中间件，并支持通过 `Contracts\Middleware` 接口创建自定义中间件。

中间件适用于 [可分发的作业类](https://queuety.inline0.dev/zh/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/zh/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）
