# 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` |