hyapp-server/services/room-service/internal/room/service/robot_display_publisher.go

88 lines
3.0 KiB
Go

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)
}