hyapp-server/docs/房间Outbox补偿开发.md
2026-05-12 20:07:49 +08:00

145 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Room Outbox Worker
本文档记录当前 `room-service` outbox 的实际语义。房间写命令只负责提交 Room Cell 状态、command log、snapshot 和 outbox腾讯云 IM、activity-service 等外部投递全部由 outbox worker 异步执行。
## 边界
| 项目 | 当前规则 |
| --- | --- |
| 主链路 | 不同步调用腾讯云 IM不同步投递 activity-service |
| 持久化 | 命令变更、command log、room_outbox 在同一提交链路内完成 |
| worker | 读取 `room_outbox`,投递到配置的 outbox publisher fanout |
| 腾讯云 IM | `RoomCreated` 由 worker 确保群组存在,其他房间事件由 worker 发送群系统消息 |
| activity-service | 由 room outbox worker 异步投递,失败进入 room_outbox 重试 |
| 幂等 | 房间命令靠 `command_id`outbox 投递靠 `event_id` |
不做的事:
| 不做 | 原因 |
| --- | --- |
| 在 HTTP/gRPC 请求里等待 IM REST | 外部慢调用会拖住房间串行命令队列 |
| 在 Room Cell 中保存投递状态 | 投递状态属于 outbox不属于房间权威状态 |
| 直接用 Redis 做 outbox | MySQL 是恢复和补偿的持久来源 |
| 精确一次投递 | 外部系统和进程崩溃窗口只能做到 at-least-once消费端必须按 `event_id` 去重 |
## 写命令语义
```text
gateway -> room-service command
-> idempotency check
-> ensure Redis lease and Room Cell
-> mutate cloned RoomState
-> optional wallet debit before room gift state is committed
-> SaveMutation(command log + room_outbox + room meta)
-> snapshot when version hits snapshot_every_n
-> update list/presence projections best effort
-> return response
outbox worker
-> claim pending/retryable rows
-> publish to composite outbox publisher
-> delivered / retryable / failed
```
主链路成功只表示房间状态已经落库;不表示腾讯云 IM 或 activity-service 已经完成投递。客户端实时表现应以房间响应、房间 snapshot 和 IM 系统消息共同补偿。
## 数据状态
`room_outbox.status` 使用:
| 状态 | 含义 |
| --- | --- |
| `pending` | 已落库,尚未被 worker 处理 |
| `delivering` | 已被 worker 抢占,`lock_until_ms` 前其他 worker 不抢 |
| `delivered` | 所有配置的 outbox publisher 已处理成功 |
| `retryable` | 上次投递失败,等待 `next_retry_at_ms` 后重试 |
| `failed` | 达到最大重试次数,进入死信,需要人工处理或后台工具重放 |
旧数据里可能存在历史 `published` 状态。当前 worker 不再写这个状态,新链路只写上表状态。
## 配置
`services/room-service/configs/*.yaml` 当前配置:
```yaml
snapshot_every_n: 10
outbox_worker:
enabled: true
poll_interval: "1s"
batch_size: 100
publish_timeout: "3s"
retry_strategy: "exponential_backoff"
max_retry_count: 10
initial_backoff: "5s"
max_backoff: "5m"
```
配置规则:
| 字段 | 规则 |
| --- | --- |
| `snapshot_every_n` | 开发和生产默认 `10`;命令日志仍保证恢复,不能依赖每条命令快照 |
| `poll_interval` | worker 空轮询间隔,必须大于 0避免 busy loop |
| `batch_size` | 每轮最大抢占条数,必须大于 0避免全表扫描 |
| `publish_timeout` | 单条 outbox 投递 deadline避免外部系统拖死 worker |
| `retry_strategy` | 当前为 `exponential_backoff` |
| `max_retry_count` | 达到次数后标记 `failed`,防止 poison event 永久污染日志 |
| `initial_backoff` | 第一次失败后的最小退避 |
| `max_backoff` | 指数退避上限 |
## 重试和死信
失败处理:
```text
publish failed
-> retry_count + 1
-> if retry_count >= max_retry_count: status=failed
-> else: status=retryable, next_retry_at_ms=now+backoff
```
`failed` 不再自动重试。后台应提供人工查看和重放工具,重放时必须保留原 `event_id` 或生成明确的新修复事件,避免消费端无法去重。
## 投递对象
当前 `CompositeOutboxPublisher` fanout 到多个 publisher
| Publisher | 行为 |
| --- | --- |
| Tencent IM | `RoomCreated` 确保群存在;房间系统事件发送腾讯云 IM 群自定义消息 |
| Activity | 将 room outbox 事件投递给 `activity-service` 的消费入口 |
fanout 要求所有 publisher 成功后才标记 `delivered`。任一失败都会进入重试,因此消费端必须幂等处理重复事件。
## 阶段耗时日志
每条房间写命令都会输出 `room_command_timing`
| 字段 | 含义 |
| --- | --- |
| `idempotency_ms` | command log 幂等检查耗时 |
| `lease_ms` | Redis lease / Room Cell 装载耗时 |
| `wallet_debit_ms` | 送礼扣费耗时;非送礼为 0 |
| `save_mutation_ms` | command log + outbox + 房间 meta 提交耗时 |
| `snapshot_ms` | 快照持久化耗时;未触发为 0 |
| `projection_ms` | 列表和 presence 投影耗时 |
| `activity_ms` | 主链路 activity 同步耗时;当前固定为 0 |
| `tencent_im_ms` | 主链路腾讯 IM 同步耗时;当前固定为 0 |
| `activity_async_outbox` | 本命令是否产生异步 activity/outbox 投递 |
| `tencent_im_async_outbox` | 本命令是否产生异步 IM/outbox 投递 |
排查接口卡顿时先看 `total_ms`,再看各阶段字段。送礼慢通常优先看 `wallet_debit_ms`,普通房间命令慢优先看 `save_mutation_ms``snapshot_ms``projection_ms`
## 验收
必须满足:
| 场景 | 验收 |
| --- | --- |
| IM 不可用 | 房间写命令仍成功outbox 进入 `retryable` 或最终 `failed` |
| activity 不可用 | 房间写命令仍成功outbox 进入重试 |
| poison event | 达到 `max_retry_count` 后进入 `failed`,不再每秒刷日志 |
| 正常投递 | 同房间事件最终全部 `delivered` |
| 真实链路 | create/join/mic/sendGift/leave 均有 `room_command_timing` |