131 lines
6.3 KiB
Markdown
131 lines
6.3 KiB
Markdown
# 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 命令,也不能恢复或创建新的 Cell;gRPC 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 隔离幂等。
|