package service import ( "context" "log/slog" "time" "hyapp/pkg/appcode" "hyapp/pkg/logx" "hyapp/services/room-service/internal/room/outbox" ) const ( // robotDisplayPublishTimeout 是机器人展示直发 IM 的单条上限;超时或失败只记日志,也绝不能补写数据库或 MQ。 robotDisplayPublishTimeout = 3 * time.Second // robotDisplayMaxConcurrentPublishes 给高频机器人展示设置进程级硬上限,避免腾讯 IM 变慢时无界堆积 goroutine。 robotDisplayMaxConcurrentPublishes = 32 ) func (s *Service) publishRobotDisplayRecordsBestEffort(ctx context.Context, records []outbox.Record) { if s == nil || len(records) == 0 { return } if s.robotDisplayPublisher == nil { // 缺少腾讯 IM publisher 时只把整批纯展示事件视为 best-effort 投递失败,不创建持久化补偿事实。 logx.Warn(ctx, "robot_display_im_publish_skipped", slog.String("node_id", s.nodeID), slog.Int("record_count", len(records)), slog.String("reason", "publisher_not_configured"), ) return } scopedApp := appcode.FromContext(ctx) copied := append([]outbox.Record(nil), records...) for index := range copied { copied[index].AppCode = appcode.Normalize(firstNonEmpty(copied[index].AppCode, scopedApp)) if copied[index].Envelope != nil { copied[index].Envelope.AppCode = copied[index].AppCode } } sortOutboxRecords(copied) s.robotDisplayPublishOnce.Do(func() { s.robotDisplayPublishSlots = make(chan struct{}, robotDisplayMaxConcurrentPublishes) }) select { case s.robotDisplayPublishSlots <- struct{}{}: case <-ctx.Done(): return default: // 机器人消息没有业务事实语义;发布槽位耗尽时只丢当前展示,不能排队到 DB、MQ 或无界内存。 logx.Warn(ctx, "robot_display_im_publish_skipped", slog.String("node_id", s.nodeID), slog.Int("record_count", len(copied)), slog.String("reason", "publisher_busy"), ) return } go func(records []outbox.Record) { defer func() { <-s.robotDisplayPublishSlots }() for _, record := range records { if record.Envelope == nil { logx.Warn(ctx, "robot_display_im_publish_skipped", slog.String("node_id", s.nodeID), slog.String("event_id", record.EventID), slog.String("event_type", record.EventType), slog.String("room_id", record.RoomID), slog.String("reason", "nil_envelope"), ) continue } publishCtx, cancel := context.WithTimeout(appcode.WithContext(context.Background(), record.AppCode), robotDisplayPublishTimeout) err := s.robotDisplayPublisher.PublishOutboxEvent(publishCtx, record.Envelope) cancel() if err != nil { // 机器人展示事件允许极小概率丢失;这里只有日志且不补偿,房间快照只保证真实房态不被这次失败污染。 logx.Warn(ctx, "robot_display_im_publish_failed", slog.String("node_id", s.nodeID), slog.String("event_id", record.EventID), slog.String("event_type", record.EventType), slog.String("room_id", record.RoomID), slog.String("error", trimOutboxError(err.Error())), ) } } }(copied) }