222 lines
9.3 KiB
Go
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.

// Package service 承载 room-service 的 Room Cell 管理、命令流水线、恢复、快照和 outbox 补偿。
package service
import (
"errors"
"sync"
"sync/atomic"
"time"
roomv1 "hyapp.local/api/proto/room/v1"
"hyapp/pkg/clock"
"hyapp/pkg/tencentim"
"hyapp/services/room-service/internal/integration"
"hyapp/services/room-service/internal/room/cell"
"hyapp/services/room-service/internal/room/outbox"
"hyapp/services/room-service/internal/router"
)
// Config 收敛 room-service 领域服务的运行参数。
type Config struct {
// NodeID 是当前 room-service 实例标识Redis lease 以它作为房间 owner。
NodeID string
// LeaseTTL 是房间 owner 租约有效期,过期后其他节点可以接管并恢复房间。
LeaseTTL time.Duration
// RankLimit 是房间本地礼物榜保留长度。
RankLimit int
// SnapshotEveryN 表示每 N 个房间版本保存一次快照。
SnapshotEveryN int64
// PresenceStaleAfter 控制业务 presence 多久未刷新后由后台 worker 清理。
PresenceStaleAfter time.Duration
// MicPublishTimeout 控制 MicUp 后等待 RTC 发流确认的最长时间。
MicPublishTimeout time.Duration
// RTCUserRemover 是平台级封禁后把用户从腾讯 RTC 房间移出的外部边界。
RTCUserRemover integration.RTCUserRemover
// RoomTreasureOpenScheduler 把倒计时开箱唤醒交给外部延迟消息,避免只靠已加载 Cell 扫描。
RoomTreasureOpenScheduler integration.RoomTreasureOpenScheduler
// RoomGiftLeaderboard 是跨房间金币榜读模型SendGift 提交后 best-effort 写入。
RoomGiftLeaderboard RoomGiftLeaderboardStore
// Clock 允许测试注入稳定时间,生产默认使用系统时钟。
Clock clock.Clock
}
// Service 承载 room-service 的核心房间领域逻辑。
type Service struct {
// nodeID 是当前实例 ID必须和 Redis lease 中的 owner 对齐。
nodeID string
// leaseTTL 控制当前节点持有房间执行权的时间窗口。
leaseTTL time.Duration
// rankLimit 控制每个 Room Cell 的 LocalRank top N。
rankLimit int
// snapshotEveryN 控制快照频率,值越小恢复越快但写放大越高。
snapshotEveryN int64
// presenceStaleAfter 是断线或无心跳用户从 room-service presence 中移除的阈值。
presenceStaleAfter time.Duration
// micPublishTimeout 是 pending_publish 自动下麦的确认窗口。
micPublishTimeout time.Duration
// clock 是房间命令、事件和快照的统一时间来源。
clock clock.Clock
// directory 负责 room_id -> node_id lease生产路径是 Redis。
directory router.Directory
// repository 是 MySQL 持久化边界,保存 meta、snapshot、command log 和 outbox。
repository Repository
// wallet 是 SendGift 的同步扣费依赖,扣费失败不能进入 Room Cell 状态变更。
wallet integration.WalletClient
// luckyGift 是幸运礼物同步检查和抽奖依赖;未配置时幸运礼物按未启用处理。
luckyGift integration.LuckyGiftClient
// syncPublisher 是 room-service 到腾讯云 IM 的低时延房间系统事件桥。
syncPublisher integration.RoomEventPublisher
// outboxPublisher 是补偿 worker 对外部消费者的异步投递抽象。
outboxPublisher integration.OutboxPublisher
// roomTreasureOpenScheduler 在 countdown outbox 投递成功后安排 open_at_ms 唤醒。
roomTreasureOpenScheduler integration.RoomTreasureOpenScheduler
// roomGiftLeaderboard 只保存房间维度金币消耗 zset不承载 Room Cell 核心状态。
roomGiftLeaderboard RoomGiftLeaderboardStore
// rtcUserRemover 在 Room Cell 提交驱逐后同步移除实时音频房连接。
rtcUserRemover integration.RTCUserRemover
// draining 表示当前节点正在下线,只允许已经进入执行链路的命令收尾。
draining atomic.Bool
// mu 保护本进程内 room_id -> RoomCell 注册表。
mu sync.Mutex
// cells 保存当前节点已经装载的单房间执行单元。
cells map[string]*cell.RoomCell
}
// mutationResult 是命令在 Room Cell 内执行后的统一结果包。
type mutationResult struct {
// applied 表示本次命令确实改变并提交了房间状态。
applied bool
// snapshot 是命令执行后对外返回的房间快照。
snapshot *roomv1.RoomSnapshot
// user 是 JoinRoom 等命令需要返回的用户投影。
user *roomv1.RoomUser
// seatNo 是麦位命令返回的目标或原麦位编号。
seatNo int32
// micSessionID 是 MicUp 创建的发流会话 ID客户端确认发流时必须带回。
micSessionID string
// publishDeadlineMS 是 pending_publish 自动释放的截止时间。
publishDeadlineMS int64
// billingReceiptID 是 SendGift 从 wallet-service 得到的账务回执。
billingReceiptID string
// roomHeat 是 SendGift 后的房间热度。
roomHeat int64
// giftRank 是 SendGift 后的本地礼物榜投影。
giftRank []*roomv1.RankItem
// luckyGift 是扣费成功后同步执行的幸运礼物抽奖结果;普通礼物保持 nil。
luckyGift *roomv1.LuckyGiftDrawResult
// commandPayload 允许少数命令把外部依赖的结算快照写入 command log用于恢复。
commandPayload []byte
// syncEvent 标记该命令存在腾讯云 IM 房间消息形态;实际投递只由 outbox worker 异步执行。
syncEvent *tencentim.RoomEvent
// outboxRecords 是本次命令已经持久化的房间事实,提交后可投给 activity 等外部消费者。
outboxRecords []outbox.Record
// roomStatus 非空时代表命令提交时需要同步更新 rooms 和列表投影生命周期状态。
roomStatus string
// roomSeatCount 非 nil 时代表命令提交时需要同步更新 rooms 元数据座位数。
roomSeatCount *int32
// roomPasswordHash 非 nil 时代表命令提交时需要同步更新 rooms 当前密码哈希。
roomPasswordHash *string
roomMode *string
visibleRegionID *int64
closeInfo *RoomCloseInfo
// walletDebitMS 记录 SendGift 同步扣费耗时;非钱包命令保持 0。
walletDebitMS int64
// roomGiftLeaderboard 是 SendGift 成功后写入跨房间金币榜的轻量增量。
roomGiftLeaderboard *RoomGiftLeaderboardIncrement
// roomUserGiftStats 是当前房间用户送礼价值统计,和命令日志同事务提交。
roomUserGiftStats []RoomUserGiftStatIncrement
}
// New 初始化 room-service 领域服务。
func New(cfg Config, directory router.Directory, repository Repository, wallet integration.WalletClient, syncPublisher integration.RoomEventPublisher, outboxPublisher integration.OutboxPublisher, luckyGift ...integration.LuckyGiftClient) *Service {
usedClock := cfg.Clock
if usedClock == nil {
// 生产默认系统时钟,测试可注入固定时钟保证断言稳定。
usedClock = clock.System{}
}
snapshotEveryN := cfg.SnapshotEveryN
if snapshotEveryN <= 0 {
// 默认每次状态变更都保存快照,优先保证首版恢复简单。
snapshotEveryN = 1
}
rankLimit := cfg.RankLimit
if rankLimit <= 0 {
// 默认只保留房间内前 20 名礼物榜,避免快照和广播载荷过大。
rankLimit = 20
}
presenceStaleAfter := cfg.PresenceStaleAfter
if presenceStaleAfter <= 0 {
// 默认 2 分钟清理无刷新 presence避免重连窗口过短导致误踢。
presenceStaleAfter = 2 * time.Minute
}
micPublishTimeout := cfg.MicPublishTimeout
if micPublishTimeout <= 0 {
// MicUp 只是业务占麦,客户端必须在短窗口内确认 RTC 发流成功。
micPublishTimeout = 15 * time.Second
}
var luckyGiftClient integration.LuckyGiftClient
if len(luckyGift) > 0 {
luckyGiftClient = luckyGift[0]
}
return &Service{
nodeID: cfg.NodeID,
leaseTTL: cfg.LeaseTTL,
rankLimit: rankLimit,
snapshotEveryN: snapshotEveryN,
presenceStaleAfter: presenceStaleAfter,
micPublishTimeout: micPublishTimeout,
clock: usedClock,
directory: directory,
repository: repository,
wallet: wallet,
luckyGift: luckyGiftClient,
syncPublisher: syncPublisher,
outboxPublisher: outboxPublisher,
roomTreasureOpenScheduler: cfg.RoomTreasureOpenScheduler,
roomGiftLeaderboard: cfg.RoomGiftLeaderboard,
rtcUserRemover: cfg.RTCUserRemover,
cells: make(map[string]*cell.RoomCell),
}
}
// HealthCheck 校验 room-service 核心状态 owner 和持久化依赖已经完成装配。
func (s *Service) HealthCheck() error {
if s == nil {
// nil service 说明 App 装配失败live/ready 都不能放行。
return errors.New("room service is not initialized")
}
if s.directory == nil {
// 没有 directory 就无法保证单房间单活 owner。
return errors.New("room directory is not initialized")
}
if s.repository == nil {
// 没有 repository 就无法保证 command log、snapshot 和 outbox 持久化。
return errors.New("room repository is not initialized")
}
if s.cells == nil {
// cells 注册表缺失会导致已恢复房间无法复用同一个 Room Cell。
return errors.New("room cell registry is not initialized")
}
return nil
}
// MarkDraining 让领域服务停止接收新的 Room Cell 命令和新 lease 接管。
func (s *Service) MarkDraining() {
if s == nil {
return
}
s.draining.Store(true)
}
func (s *Service) isDraining() bool {
return s != nil && s.draining.Load()
}