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.loadCell(roomCtx, roomRef.RoomID) 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 }