
# 工作进程

工作进程是长期运行的进程，它们会轮询数据库以查找待处理的作业、执行作业并推进工作流。`wp queuety work` 命令在 WP-CLI 中运行，而队列本身则通过可用的最佳 Queuety DB 驱动直接从 MySQL 领取。

该插件还会安装一个默认的每分钟运行一次的 WordPress cron 后备方案，供无法访问 shell 的环境使用。本页介绍专用的 WP-CLI 工作进程，这是吞吐量更高且推荐的部署模式。

## 启动工作进程

```bash
wp queuety work
```

这会在 `default` 队列上启动一个工作进程。它会循环运行：领取作业、执行作业，然后重复。当队列为空时，它会休眠 `QUEUETY_WORKER_SLEEP` 秒（默认值：1），然后再次轮询。

### 选项

| 选项 | 说明 |
|---|---|
| `--queue=<queue>` | 要处理的 Queue(s)，用逗号分隔以指定优先顺序（默认值：`default`） |
| `--once` | 最多处理一个作业后退出 |
| `--workers=<n>` | 派生 N 个工作进程（需要 `pcntl` 和 `posix` 扩展） |
| `--min-workers=<n>` | 自适应池的最小工作进程数。需要 `--max-workers` |
| `--max-workers=<n>` | 自适应池的最大工作进程数 |

```bash
# Process a specific queue
wp queuety work --queue=emails

# Process at most one job and exit
wp queuety work --once

# Run 4 concurrent workers
wp queuety work --workers=4

# Scale between 2 and 6 workers as backlog changes
wp queuety work --queue=providers --min-workers=2 --max-workers=6
```

## 多队列优先级

单个工作进程可以按优先级顺序处理多个队列。向 `--queue` 传入以逗号分隔的队列名称列表：

```bash
wp queuety work --queue=critical,default,low
```

工作进程会按列出的顺序尝试从每个队列领取作业。它会先检查 `critical`。如果 `critical` 中有作业，就会处理该作业。如果 `critical` 为空，则会继续检查 `default`，然后检查 `low`。这意味着高优先级队列总会先于低优先级队列清空。

### 优先级顺序的工作方式

每次迭代时，工作进程都会尝试从列表中的第一个队列领取作业。如果该队列为空或已暂停，它就会继续检查下一个队列，并依次检查，直到找到作业或检查完列表。如果所有队列都没有作业，工作进程就会休眠后重试。这是一种严格的优先级模式，因此持续不断的 `critical` 作业流会让 `default` 和 `low` 持续得不到处理。如果低优先级队列需要确保吞吐量，请改用专用工作进程。

### 在 PHP 中使用多队列

`Worker::run()` 和 `Worker::flush()` 方法接受字符串或数组：

```php
// Comma-separated string
Queuety::worker()->run( 'critical,default,low' );

// Array
Queuety::worker()->run( [ 'critical', 'default', 'low' ] );

// flush() also supports multi-queue
$count = Queuety::worker()->flush( 'critical,default,low' );
```

### 部署策略

**为每个队列配置专用工作进程**，可以让你完全控制并发和隔离。每个队列都有自己的工作进程 process(es)，不会因其他队列而得不到处理：

```ini
[program:queuety-critical]
command=wp queuety work --queue=critical
numprocs=4

[program:queuety-default]
command=wp queuety work --queue=default
numprocs=2

[program:queuety-low]
command=wp queuety work --queue=low
numprocs=1
```

**由单个工作进程按优先级顺序处理**，管理起来更简单，适用于低优先级队列能够容忍延迟的情况：

```ini
[program:queuety-worker]
command=wp queuety work --queue=critical,default,low
numprocs=2
```

你也可以结合这两种方式：为 `critical` 配置专用工作进程以确保吞吐量，并让另一个按优先级顺序运行的工作进程处理其他所有队列：

```ini
[program:queuety-critical]
command=wp queuety work --queue=critical
numprocs=2

[program:queuety-fallback]
command=wp queuety work --queue=default,low
numprocs=2
```

## 基于资源的准入控制

工作进程的工作不只是领取下一条待处理记录。在已领取的作业开始之前，Queuety 可以根据作业的并发组和对应处理程序近期的执行情况，执行粗略的准入检查。

关于更广泛的模型，包括它如何与作业元数据和工作流预算协同，请参阅 [资源管理](https://queuety.inline0.dev/zh/docs/features/resource-management)。

并发组为多个处理程序或作业类别设定共享上限。如果你为多个高开销提供方指定相同的组和上限，达到组上限后，工作进程就会释放新领取的作业，而不是开始处理。这样即使有多个空闲工作进程，彼此独立的队列也不会同时涌向同一个下游系统。

近期执行日志也会用于生成简单的准入配置文件。启用资源准入控制后，Queuety 会查看即将运行的处理程序近期的平均时长和峰值内存。如果当前工作进程已经接近其配置的内存上限、某个队列或提供方组已达到加权预算、当前容器的剩余内存似乎不足以满足该处理程序的已观察配置文件，或者 `--once` 式运行即将达到时间限制，Queuety 就会推迟该作业，而不是启动可能无法顺利完成的工作。

你可以在工作定义中选择启用该策略，而不是通过工作进程命令启用：

```php
class CallProviderStep implements \Queuety\Step {
    public function handle( array $state ): array {
        // ...
    }

    public function config(): array {
        return [
            'concurrency_group' => 'providers',
            'concurrency_limit' => 3,
            'cost_units' => 5,
        ];
    }
}
```

相同的元数据也可以通过公共属性附加到可分派的作业类，或通过 `PendingJob::concurrency_group()` 和 `PendingJob::cost_units()` 附加到单个分派任务。

工作进程侧的准入配置文件由以下常量控制：

| 常量 | 说明 |
|---|---|
| `QUEUETY_RESOURCE_ADMISSION` | 启用或禁用基于配置文件的准入检查 |
| `QUEUETY_RESOURCE_PROFILE_WINDOW_MINUTES` | 构建近期处理程序配置文件时回溯的时间范围 |
| `QUEUETY_RESOURCE_PROFILE_TTL` | 近期处理程序配置文件的缓存 TTL |
| `QUEUETY_RESOURCE_MEMORY_HEADROOM_MB` | 准入另一个作业前预留的内存余量 |
| `QUEUETY_RESOURCE_SYSTEM_MEMORY_AWARENESS` | 启用或禁用尽力而为的容器或主机内存检查 |
| `QUEUETY_RESOURCE_SYSTEM_MEMORY_HEADROOM_MB` | 准入另一个作业前预留的系统内存余量 |
| `QUEUETY_RESOURCE_QUEUE_COST_BUDGETS` | 按队列名称索引的加权成本预算 |
| `QUEUETY_RESOURCE_GROUP_COST_BUDGETS` | 按 `concurrency_group` 索引的加权成本预算 |
| `QUEUETY_RESOURCE_TIME_HEADROOM_MS` | 单次运行工作进程预留的时间余量 |

## 工作进程生命周期

满足以下任一条件时，工作进程会在完成当前作业后优雅停止：

| 条件 | 配置 |
|---|---|
| 已处理的作业数达到上限 | `QUEUETY_WORKER_MAX_JOBS`（默认值：1000） |
| 超出内存上限 | `QUEUETY_WORKER_MAX_MEMORY`（默认值：128 MB） |

停止后，工作进程会正常退出。在生产环境中，进程管理器会自动重启它。

目前尚未对信号进行优雅处理（[#4](https://github.com/inline0/queuety/issues/4)）。单个 `wp queuety work` 进程不会安装信号处理程序，因此 `SIGTERM` 或 `SIGINT`（Ctrl+C）会立即终止进程，即使作业正在执行也不例外。该作业会一直处于 `processing` 状态，直到过了 `QUEUETY_STALE_TIMEOUT` 秒后，过期作业恢复机制将其重置，因此请确保处理程序可以安全重试。工作进程池会将 `SIGTERM` 转发给子进程，但子进程不会处理它，因此池会继续运行，直到它们因上述某个原因停止。需要停止工作进程池时，请让进程管理器在停止超时后发送 `SIGKILL`。

## 多工作进程并发

`--workers=N` 标志会使用 `pcntl_fork()` 派生 N 个子进程。每个子进程都会创建自己的数据库连接并独立处理作业。父进程会监控子进程，并重启任何崩溃的进程。

```bash
wp queuety work --queue=default --workers=4
```

要求：
必须安装 `pcntl` 和 `posix` PHP 扩展（如果没有 `pcntl`，命令会失败并显示“pcntl extension is required for --workers=N.”），而且每个子进程都使用自己的数据库连接，因此你的 MySQL `max_connections` 设置必须能满足整个进程池的需求。自适应池（`--min-workers`/`--max-workers`）也有相同的要求。

## 自适应工作进程池

自适应池会保持一定数量的工作进程处于就绪状态，然后在配置的范围内扩容或缩容：

```bash
wp queuety work --queue=providers --min-workers=2 --max-workers=6
```

父进程会监控所选队列集合中可领取的积压作业，并根据积压情况调整子进程数量，但不会超过配置的最大值。队列安静下来后，进程池会等待空闲宽限时间再缩容，以免短暂的突发流量导致进程列表立即频繁变动。

扩容仍然采取保守策略。当 Queuety 能读取容器或主机内存遥测数据时，如果剩余内存不足以容纳另一个完整的工作进程资源配置，就不会增加子进程。

父进程侧的扩缩容循环由以下设置控制：

| 常量 | 说明 |
|---|---|
| `QUEUETY_WORKER_POOL_SCALE_INTERVAL` | 父进程调整进程池大小的频率 |
| `QUEUETY_WORKER_POOL_IDLE_GRACE` | 积压必须持续安静多久才会缩容 |

## 过期作业恢复

如果工作进程在处理作业时崩溃，该作业会留在 `processing` 状态。长期运行的工作进程大约每分钟检查一次，查找处于 `processing` 状态超过 `QUEUETY_STALE_TIMEOUT` 秒的作业。仍有尝试次数的过期作业会在退避延迟后重试；已用尽尝试次数的作业会被搁置，其工作流也会标记为失败。`--once` 工作进程和 WP-Cron 后备方案不会运行此项检查。

手动恢复过期作业：

```bash
wp queuety recover
```

## 清空模式

处理所有待处理作业后退出。适用于测试或一次性批处理：

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

## 生产环境部署

在生产环境中，使用进程管理器保持工作进程持续运行。以下是两种常见做法。

### Supervisord

```ini
[program:queuety-default]
command=wp queuety work --queue=default
directory=/var/www/html
autostart=true
autorestart=true
numprocs=2
process_name=%(program_name)s_%(process_num)02d
stdout_logfile=/var/log/queuety-default.log
stderr_logfile=/var/log/queuety-default-error.log

[program:queuety-emails]
command=wp queuety work --queue=emails
directory=/var/www/html
autostart=true
autorestart=true
numprocs=1
stdout_logfile=/var/log/queuety-emails.log
stderr_logfile=/var/log/queuety-emails-error.log
```

### Systemd

```ini
[Unit]
Description=Queuety Worker (default queue)
After=mysql.service

[Service]
Type=simple
User=www-data
WorkingDirectory=/var/www/html
ExecStart=/usr/local/bin/wp queuety work --queue=default
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

启用并启动：

```bash
sudo systemctl enable queuety-worker
sudo systemctl start queuety-worker
```

### 部署提示

如果需要更强的隔离，请为不同队列运行不同的工作进程。工作负载受 CPU 限制时使用 `--workers=N`；如果工作负载主要受 I/O 限制，则优先使用单独受监控的工作进程。如果希望定期重启以释放内存，请设置 `QUEUETY_WORKER_MAX_JOBS`，并通过 [网络钩子](https://queuety.inline0.dev/zh/docs/features/webhooks) 或 [指标](https://queuety.inline0.dev/zh/docs/features/metrics) 留意 `buried` 计数。
