hyapp-server/docs/房间Cell状态机.md
2026-05-12 21:51:39 +08:00

131 lines
6.3 KiB
Markdown
Raw Permalink 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 Cell State Machines
本文档定义 `room-service` 在微服务多机部署下必须遵守的 Room Cell 状态机。Room Cell 的目标是让同一个房间在同一时间只有一个执行节点提交状态并且在节点重启、发布、RTC 回调延迟和 outbox 重试时不丢状态、不双写、不重复扣费。
## Room
Room 是房间生命周期的主状态。只有 `active` 房间允许进房、麦位、管理和送礼命令。
```mermaid
stateDiagram-v2
[*] --> creating
creating --> active: CreateRoom 持久化成功
active --> closing: CloseRoom 开始收尾
closing --> closed: command log/outbox/meta 提交
active --> closed: CloseRoom 当前实现一次提交完成
closed --> [*]
```
状态语义:
| State | Meaning |
| ---------- | ------------------------------------------ |
| `creating` | 正在创建房间外部依赖,不对客户端开放 |
| `active` | 可进房、可管理、可上麦、可送礼 |
| `closing` | 正在关闭,不再接收新业务命令 |
| `closed` | 已关闭,`JoinRoom` 和 guard 查询都必须拒绝 |
`CloseRoom` 必须作为 Room Cell 命令提交,不能只从后台直接更新 `rooms.status`。关闭房间时必须清空业务 presence、释放麦位、写 command log、写 `RoomClosed` outbox、更新 `rooms``room_list_entries` 状态。
## Presence
Presence 是 room-service 自己维护的业务进房状态,不等同于腾讯云 IM 或 RTC 的真实连接。
```mermaid
stateDiagram-v2
[*] --> absent
absent --> present: JoinRoom
present --> present: JoinRoom 重试或 RoomHeartbeat 刷新 last_seen_at_ms
present --> left: LeaveRoom
present --> stale_left: stale worker 超时清理
present --> kicked: KickUser
present --> closed_left: CloseRoom
left --> present: JoinRoom
stale_left --> present: JoinRoom
kicked --> present: UnbanUser 后重新 JoinRoom
closed_left --> [*]
```
规则:
- `RoomHeartbeat` 只能刷新已有 presence不能创建 presence。
- `LeaveRoom` 要同步释放用户占用的麦位。
- RTC `room_exited` 只释放当前麦位,不清理业务 presence用户可能仍停留在房间页等待重连。
- `KickUser` 写入 ban 集合,用户必须先 `UnbanUser` 才能重新 `JoinRoom`
## Mic
Mic 是麦位和 RTC 发流确认状态。业务上麦不等于已经在 RTC 发流。
```mermaid
stateDiagram-v2
[*] --> idle
idle --> locked: SetMicSeatLock(true)
locked --> idle: SetMicSeatLock(false)
idle --> pending_publish: MicUp
pending_publish --> publishing: ConfirmMicPublishing 或 RTC audio_started
pending_publish --> idle: publish_timeout 或 MicDown 或 LeaveRoom
publishing --> idle: MicDown 或 RTC audio_stopped 或 LeaveRoom
publishing --> pending_publish: ChangeMicSeat 保留 mic_session
pending_publish --> pending_publish: ChangeMicSeat 保留 mic_session
```
规则:
- `mic_session_id` 是单次上麦会话 ID旧会话的确认、停止和超时任务不能清掉新会话。
- `mic_session_room_version``last_publish_event_time_ms` 用于丢弃旧 RTC 事件。
- `pending_publish` 超过 `publish_deadline_ms` 后由 worker 自动下麦。
- `LeaveRoom` 与 RTC `room_exited` 并发时以业务 presence 的 `last_seen_at_ms` 和当前 `mic_session_id` 为准;旧的 RTC 退房事件不能释放用户重进房后的新麦位会话。
- `CloseRoom` 必须释放全部麦位,但保留麦位编号和锁定结构。
## Lease
Lease 是多机部署下的房间执行权。Redis 只保存热路由不是恢复来源MySQL snapshot + command log 才是恢复来源。
```mermaid
stateDiagram-v2
[*] --> unowned
unowned --> owned_by_node: EnsureOwner
owned_by_node --> owned_by_node: 同节点续约并生成新 lease_token
owned_by_node --> expired: TTL 到期
expired --> owned_by_other_node: 新节点 EnsureOwner
owned_by_node --> fenced_out: 提交前 VerifyOwner 失败
fenced_out --> [*]
```
规则:
- Redis runtime route key 统一使用 `app_code + "\0" + room_id`,包括默认 App不能再使用裸 `room_id` 作为兼容 key。
- `room:node:{node_id}` 保存当前 room-service 实例的 `advertise_addr`,只用于 owner 转发;它不是服务发现的全局配置中心。
- gateway 可以连接 room-service 内网负载均衡;命中非 owner 节点时gRPC 边界必须按 Redis owner route 转发到当前 owner 实例。
- `advertise_addr` 必须是实例私网地址,不能是 room-service 内网 CLB 地址,否则转发会命中任意节点并触发递归保护。
- 命令开始前通过 `EnsureOwner` 获得执行权。
- 命令真正写 command log/outbox 前必须用 `lease_token` 再做一次 `VerifyOwner`
- 如果提交前 lease 已经过期或 token 已变化,旧节点必须返回 `UNAVAILABLE`,不能写房间状态。
- `room_command_log` 保存 `owner_node_id``lease_token`,用于排查谁在什么 lease 下提交了命令。
- stale presence worker 和 mic publish timeout worker 只能扫描本节点已加载 Cell清理前必须确认当前 Redis lease 仍属于本节点,并在读完快照后再次用相同 `lease_token` 校验,最后由命令提交链路再做 fencing。
- 节点进入 draining 后不能再接收新的 Room Cell 命令,也不能恢复或创建新的 CellgRPC drain 完成后应主动释放本节点已加载房间的 owner lease降低滚动发布等待 TTL 的窗口。
## Outbox
Outbox 是房间事件对外投递状态机。它允许重复投递,但多 worker 不应该同时抢同一批。
```mermaid
stateDiagram-v2
[*] --> pending
pending --> delivering: ClaimPendingOutbox
retryable --> delivering: ClaimPendingOutbox
delivering --> delivered: 投递成功并确认
delivering --> retryable: 投递失败或状态未知
delivering --> failed: 明确不可重试
retryable --> failed: 超过策略上限或人工判定
```
规则:
- `ClaimPendingOutbox` 必须把事件从 `pending/retryable` 原子改成 `delivering`,并写入 `worker_id``lock_until_ms`
- 其他 worker 不能抢到未过期的 `delivering` 事件。
- 投递成功后才能标记 `delivered`
- 投递失败必须保留 envelope 和 last_error转入 `retryable` 等待下一轮抢占。
- outbox 事件必须带 `app_code`,消费者按 App 隔离幂等。