124 lines
3.6 KiB
Go
124 lines
3.6 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"time"
|
||
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/pkg/idgen"
|
||
"hyapp/services/room-service/internal/room/command"
|
||
"hyapp/services/room-service/internal/room/state"
|
||
)
|
||
|
||
const micDownReasonPublishTimeout = "publish_timeout"
|
||
|
||
// RunMicPublishTimeoutWorker 周期性释放超过 deadline 仍未确认 RTC 发流的麦位。
|
||
func (s *Service) RunMicPublishTimeoutWorker(ctx context.Context, interval time.Duration) {
|
||
if interval <= 0 || s.micPublishTimeout <= 0 {
|
||
// 非正数表示显式关闭,便于测试或临时止血。
|
||
return
|
||
}
|
||
|
||
ticker := time.NewTicker(interval)
|
||
defer ticker.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case <-ticker.C:
|
||
_ = s.SweepMicPublishTimeouts(ctx)
|
||
}
|
||
}
|
||
}
|
||
|
||
// SweepMicPublishTimeouts 扫描本节点已装载 Room Cell,自动下掉未确认发流的 pending_publish 麦位。
|
||
func (s *Service) SweepMicPublishTimeouts(ctx context.Context) error {
|
||
now := s.clock.Now()
|
||
nowMs := now.UnixMilli()
|
||
for _, roomRef := range s.loadedRoomRefs() {
|
||
// 后台扫描只处理本进程已装载的 Cell,未装载房间由下一次用户命令恢复后再处理。
|
||
roomCtx := appcode.WithContext(ctx, roomRef.AppCode)
|
||
lease, owned, err := s.loadedRoomLease(roomCtx, roomRef, now)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !owned {
|
||
// lease 已被其他节点持有时,旧 owner 的内存状态不能再驱动下麦命令。
|
||
continue
|
||
}
|
||
|
||
roomCell := s.loadCellForLease(roomCtx, roomRef.RoomID, lease)
|
||
if roomCell == nil {
|
||
// loadedRoomRefs 和 loadCell 之间允许并发变化,缺失时跳过即可。
|
||
continue
|
||
}
|
||
|
||
// 先在命令边界读取快照,再根据快照筛出候选超时麦位。
|
||
currentState, _, err := roomCell.Snapshot(roomCtx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
owned, err = s.verifyLoadedRoomLease(roomCtx, roomRef, lease, s.clock.Now())
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !owned {
|
||
continue
|
||
}
|
||
|
||
for _, seat := range pendingPublishTimedOutSeats(currentState, nowMs) {
|
||
// 每个候选执行前再次校验 lease,避免长批次期间 owner 已切换还继续写旧状态。
|
||
owned, err = s.verifyLoadedRoomLease(roomCtx, roomRef, lease, s.clock.Now())
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if !owned {
|
||
break
|
||
}
|
||
|
||
// 通过标准 MicDown 命令链路释放麦位,复用 command log、outbox、快照和幂等保护。
|
||
cmd := command.MicDown{
|
||
Base: command.Base{
|
||
RequestID: idgen.New("req_mic_publish_timeout"),
|
||
CommandID: idgen.New("cmd_mic_publish_timeout"),
|
||
ActorID: seat.UserID,
|
||
Room: roomRef.RoomID,
|
||
GatewayNodeID: s.nodeID,
|
||
SessionID: "mic-publish-timeout",
|
||
SentAtMS: nowMs,
|
||
},
|
||
TargetUserID: seat.UserID,
|
||
MicSessionID: seat.MicSessionID,
|
||
Reason: micDownReasonPublishTimeout,
|
||
}
|
||
if _, err := s.micDown(roomCtx, cmd); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// pendingPublishTimedOutSeats 返回已经超过确认 deadline 的 pending_publish 麦位快照。
|
||
func pendingPublishTimedOutSeats(current *state.RoomState, nowMs int64) []state.MicSeat {
|
||
if current == nil {
|
||
return nil
|
||
}
|
||
|
||
seats := make([]state.MicSeat, 0)
|
||
for _, seat := range current.MicSeats {
|
||
if seat.UserID <= 0 || seat.MicSessionID == "" {
|
||
// 空麦位或没有 mic_session 的历史状态不属于自动下麦目标。
|
||
continue
|
||
}
|
||
if seat.PublishState == state.MicPublishPending && seat.PublishDeadlineMS > 0 && seat.PublishDeadlineMS <= nowMs {
|
||
// 返回值拷贝,后续 MicDown 还会用 mic_session_id 在当前 Cell 状态中二次校验。
|
||
seats = append(seats, seat)
|
||
}
|
||
}
|
||
|
||
return seats
|
||
}
|