
# 心跳

长时间运行的任务和流式步骤可以发送心跳，以证明它们仍在运行。如果没有心跳，过期任务检测器可能会回收仍在积极处理的任务，因为其 `reserved_at` 时间戳尚未更新。

## 工作原理

工作进程开始处理任务时，会调用 `Heartbeat::init()` 来设置当前任务上下文。在处理程序中调用 `Heartbeat::beat()` 会将任务的 `reserved_at` 列更新为 `NOW()`。过期检测器会使用 `reserved_at` 判断任务是否卡住，因此刷新该值可避免任务被回收。

任务完成时，工作进程会调用 `Heartbeat::clear()` 重置上下文。

## 发送心跳

在任意任务处理程序或流式步骤中调用 `Heartbeat::beat()`：

```php
use Queuety\Heartbeat;

class LongRunningStep implements \Queuety\Step {

    public function handle( array $state ): array {
        $items = $state['items'];

        foreach ( $items as $i => $item ) {
            $this->process( $item );
            Heartbeat::beat( [ 'processed' => $i + 1, 'total' => count( $items ) ] );
        }

        return [ 'done' => true ];
    }

    public function config(): array {
        return [];
    }

    private function process( $item ): void {
        // Expensive work here.
    }
}
```

## 进度数据

`$progress` 参数是一个可选的关联数组。提供该参数时，数组会经过 JSON 编码，并存储在任务记录的 `heartbeat_data` 列中。这样你就能从工作进程外部跟踪进度：

```php
Heartbeat::beat( [
    'processed' => 450,
    'total'     => 1000,
    'percent'   => 45,
] );
```

不带参数调用时，只会更新 `reserved_at`：

```php
Heartbeat::beat();
```

## 心跳 API

`Queuety\Heartbeat` 类完全采用静态方式。无需实例化。

| 方法 | 说明 |
|---|---|
| `Heartbeat::init( int $job_id, Connection $conn )` | 设置任务上下文。工作进程会在处理前自动调用。 |
| `Heartbeat::beat( array $progress = [] )` | 发送心跳。更新 `reserved_at`，并可选择存储进度数据。 |
| `Heartbeat::clear()` | 清除任务上下文。工作进程会在处理后自动调用。 |
| `Heartbeat::current_job_id()` | 获取当前任务 ID（适用于测试）。 |

## 在流式步骤中使用

工作进程会在每个数据块持久化后自动发送心跳，并将 `heartbeat_data` 设为 `{"streaming_chunks": n}`，因此只要数据流定期生成内容，状态就会保持最新。如果两次生成内容之间可能间隔很久，例如等待首个令牌时，请自行调用 `Heartbeat::beat()`。你传入的数据会被工作进程下一次按数据块发送的心跳覆盖：

```php
use Queuety\Contracts\StreamingStep;
use Queuety\Heartbeat;

class StreamAPIResponse implements StreamingStep {

    public function stream( array $state, array $existing_chunks = [] ): \Generator {
        $response = $this->client->stream( $state['prompt'] );

        foreach ( $response as $i => $chunk ) {
            Heartbeat::beat( [ 'chunks' => $i + 1 ] );
            yield $chunk;
        }
    }

    public function on_complete( array $chunks, array $state ): array {
        return [ 'response' => implode( '', $chunks ) ];
    }

    public function config(): array {
        return [];
    }
}
```

## 何时使用心跳

当单个任务或步骤运行时间超过 `QUEUETY_STALE_TIMEOUT` 秒时（默认值：600），请使用心跳。常见场景包括：

- LLM API 调用（支持流式响应）
- 大文件下载或上传
- 单个步骤内的批量处理重复操作
- 任何可能需要几分钟才能响应的外部 API 调用

如果你的步骤能在一分钟内完成，就不需要心跳。只有配置的超时时间已过后，过期检测器才会启动。
