532 lines
21 KiB
Go
532 lines
21 KiB
Go
// Package config 定义 room-service 的启动配置和 YAML 加载入口。
|
||
package config
|
||
|
||
import (
|
||
"fmt"
|
||
"net"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"hyapp/pkg/configx"
|
||
"hyapp/pkg/logx"
|
||
"hyapp/pkg/tencentim"
|
||
"hyapp/pkg/tencentrtc"
|
||
)
|
||
|
||
const (
|
||
OutboxPublishModeDirect = "direct"
|
||
OutboxPublishModeMQ = "mq"
|
||
OutboxPublishModeDual = "dual"
|
||
)
|
||
|
||
// Config 描述 room-service 进程启动所需的最小配置。
|
||
type Config struct {
|
||
// ServiceName 是日志和部署识别使用的稳定服务名。
|
||
ServiceName string `yaml:"service_name"`
|
||
// Environment 是 local/dev/staging/prod 等运行环境标签。
|
||
Environment string `yaml:"environment"`
|
||
// NodeID 是当前 room-service 实例标识,Redis lease owner 使用它。
|
||
NodeID string `yaml:"node_id"`
|
||
// GRPCAddr 是内部 gRPC 监听地址,gateway-service 通过它访问 room-service。
|
||
GRPCAddr string `yaml:"grpc_addr"`
|
||
// AdvertiseAddr 是其他 room-service 实例转发到本节点时使用的私网地址,不能写负载均衡地址。
|
||
AdvertiseAddr string `yaml:"advertise_addr"`
|
||
// HealthHTTPAddr 是给 CLB/发布脚本探活使用的 HTTP 监听地址,不承载业务流量。
|
||
HealthHTTPAddr string `yaml:"health_http_addr"`
|
||
// NodeRegistryTTL 是 node_id -> advertise_addr 注册的有效期。
|
||
NodeRegistryTTL time.Duration `yaml:"node_registry_ttl"`
|
||
// NodeRegistryHeartbeatInterval 是刷新节点注册的周期。
|
||
NodeRegistryHeartbeatInterval time.Duration `yaml:"node_registry_heartbeat_interval"`
|
||
// OwnerForwardTimeout 是非 owner 节点转发到当前 owner 的单次 RPC 超时。
|
||
OwnerForwardTimeout time.Duration `yaml:"owner_forward_timeout"`
|
||
// LeaseTTL 是 room owner 租约有效期,节点故障后必须等它过期才能接管。
|
||
LeaseTTL time.Duration `yaml:"lease_ttl"`
|
||
// LuckyGiftSendLockTTL 是同一用户幸运礼物扣费、抽奖、同步返奖链路的 Redis 短锁 TTL。
|
||
LuckyGiftSendLockTTL time.Duration `yaml:"lucky_gift_send_lock_ttl"`
|
||
// RankLimit 是房间本地礼物榜 top N。
|
||
RankLimit int `yaml:"rank_limit"`
|
||
// SnapshotEveryN 控制按房间版本保存快照的频率。
|
||
SnapshotEveryN int64 `yaml:"snapshot_every_n"`
|
||
// WalletServiceAddr 是 SendGift 同步扣费 gRPC 地址。
|
||
WalletServiceAddr string `yaml:"wallet_service_addr"`
|
||
// ActivityServiceAddr 是房间事实事件投给 activity-service 的内部 gRPC 地址。
|
||
ActivityServiceAddr string `yaml:"activity_service_addr"`
|
||
// TencentIM 是腾讯云 IM 群组和房间系统消息的外部集成配置。
|
||
TencentIM TencentIMConfig `yaml:"tencent_im"`
|
||
// TencentRTC 是平台级封禁后移出实时音频房的外部集成配置。
|
||
TencentRTC TencentRTCConfig `yaml:"tencent_rtc"`
|
||
// MySQLDSN 是 rooms、snapshot、command log 和 outbox 的持久化连接串。
|
||
MySQLDSN string `yaml:"mysql_dsn"`
|
||
// MySQLMaxOpenConns 控制 MySQL 最大打开连接数。
|
||
MySQLMaxOpenConns int `yaml:"mysql_max_open_conns"`
|
||
// MySQLMaxIdleConns 控制 MySQL 空闲连接数。
|
||
MySQLMaxIdleConns int `yaml:"mysql_max_idle_conns"`
|
||
// MySQLAutoMigrate 控制启动时是否自动建表。
|
||
MySQLAutoMigrate bool `yaml:"mysql_auto_migrate"`
|
||
// RedisAddr 是 room owner lease 目录地址。
|
||
RedisAddr string `yaml:"redis_addr"`
|
||
// RedisPassword 是 Redis 鉴权密码,本地默认留空。
|
||
RedisPassword string `yaml:"redis_password"`
|
||
// RedisDB 是 Redis logical DB。
|
||
RedisDB int `yaml:"redis_db"`
|
||
// PresenceStaleAfter 是业务 presence 超过多久未刷新后被视为断线离房。
|
||
PresenceStaleAfter time.Duration `yaml:"presence_stale_after"`
|
||
// PresenceStaleScanInterval 是本节点扫描已装载 Room Cell 的周期。
|
||
PresenceStaleScanInterval time.Duration `yaml:"presence_stale_scan_interval"`
|
||
// MicPublishTimeout 是 MicUp 后等待客户端/RTC 发流确认的最长时间。
|
||
MicPublishTimeout time.Duration `yaml:"mic_publish_timeout"`
|
||
// MicPublishScanInterval 是本节点扫描 pending_publish 超时麦位的周期。
|
||
MicPublishScanInterval time.Duration `yaml:"mic_publish_scan_interval"`
|
||
// RoomRocketLaunchScanInterval 是本节点扫描到点发射火箭的周期。
|
||
RoomRocketLaunchScanInterval time.Duration `yaml:"room_rocket_launch_scan_interval"`
|
||
// RocketMQ 承载 room_outbox 事件分发和语音房火箭延迟发射唤醒。
|
||
RocketMQ RocketMQConfig `yaml:"rocketmq"`
|
||
// OutboxWorker 控制 room_outbox 到腾讯云 IM 补偿投递 worker。
|
||
OutboxWorker OutboxWorkerConfig `yaml:"outbox_worker"`
|
||
// Log 控制 stdout 结构化日志格式、等级和 access body 策略。
|
||
Log logx.Config `yaml:"log"`
|
||
}
|
||
|
||
// OutboxWorkerConfig 控制 room_outbox pending 事件的补偿投递循环。
|
||
type OutboxWorkerConfig struct {
|
||
// Enabled 为 false 时不启动补偿 worker,测试或临时止血可用。
|
||
Enabled bool `yaml:"enabled"`
|
||
// PublishMode 控制 room_outbox 发往 direct、mq 或 dual,避免 MQ 消费者与直接投递重复发同一条 IM。
|
||
PublishMode string `yaml:"publish_mode"`
|
||
// PollInterval 是两轮 pending outbox 扫描之间的固定间隔。
|
||
PollInterval time.Duration `yaml:"poll_interval"`
|
||
// BatchSize 是单轮最多处理的 pending outbox 数,防止全表扫描。
|
||
BatchSize int `yaml:"batch_size"`
|
||
// PublishTimeout 是单条 outbox 投递腾讯云 IM 的最大耗时。
|
||
PublishTimeout time.Duration `yaml:"publish_timeout"`
|
||
// RetryStrategy 当前只接受 exponential_backoff,避免配置错误静默改变投递语义。
|
||
RetryStrategy string `yaml:"retry_strategy"`
|
||
// MaxRetryCount 是转入 failed 前允许的最大失败次数。
|
||
MaxRetryCount int `yaml:"max_retry_count"`
|
||
// InitialBackoff 是首次失败后的等待时间。
|
||
InitialBackoff time.Duration `yaml:"initial_backoff"`
|
||
// MaxBackoff 是指数退避的最大等待时间。
|
||
MaxBackoff time.Duration `yaml:"max_backoff"`
|
||
}
|
||
|
||
// RocketMQConfig 描述 room-service 使用的 RocketMQ 集群和业务 topic。
|
||
type RocketMQConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
NameServers []string `yaml:"name_servers"`
|
||
NameServerDomain string `yaml:"name_server_domain"`
|
||
AccessKey string `yaml:"access_key"`
|
||
SecretKey string `yaml:"secret_key"`
|
||
SecurityToken string `yaml:"security_token"`
|
||
Namespace string `yaml:"namespace"`
|
||
VIPChannel bool `yaml:"vip_channel"`
|
||
SendTimeout time.Duration `yaml:"send_timeout"`
|
||
Retry int `yaml:"retry"`
|
||
RoomOutbox RoomOutboxMQConfig `yaml:"room_outbox"`
|
||
RocketLaunch RocketLaunchMQConfig `yaml:"rocket_launch"`
|
||
}
|
||
|
||
// RoomOutboxMQConfig 控制 room_outbox fanout topic。
|
||
type RoomOutboxMQConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
Topic string `yaml:"topic"`
|
||
ProducerGroup string `yaml:"producer_group"`
|
||
TencentIMConsumerEnabled bool `yaml:"tencent_im_consumer_enabled"`
|
||
TencentIMConsumerGroup string `yaml:"tencent_im_consumer_group"`
|
||
ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"`
|
||
}
|
||
|
||
// RocketLaunchMQConfig 控制火箭延迟发射唤醒 topic。
|
||
type RocketLaunchMQConfig struct {
|
||
Enabled bool `yaml:"enabled"`
|
||
Topic string `yaml:"topic"`
|
||
ProducerGroup string `yaml:"producer_group"`
|
||
ConsumerGroup string `yaml:"consumer_group"`
|
||
ConsumerMaxReconsumeTimes int32 `yaml:"consumer_max_reconsume_times"`
|
||
}
|
||
|
||
// TencentIMConfig 描述 room-service 调用腾讯云 IM REST API 的配置。
|
||
type TencentIMConfig struct {
|
||
// Enabled 控制是否启用腾讯云 IM REST 桥接;关闭时只保留 Room Cell 内部提交。
|
||
Enabled bool `yaml:"enabled"`
|
||
// SDKAppID 是腾讯云 IM 控制台分配的应用 ID。
|
||
SDKAppID int64 `yaml:"sdk_app_id"`
|
||
// SecretKey 用于生成 App 管理员 UserSig,不能写入公开配置。
|
||
SecretKey string `yaml:"secret_key"`
|
||
// AdminIdentifier 是腾讯云 IM App 管理员账号。
|
||
AdminIdentifier string `yaml:"admin_identifier"`
|
||
// AdminUserSigTTL 是 REST API 管理员票据有效期。
|
||
AdminUserSigTTL time.Duration `yaml:"admin_user_sig_ttl"`
|
||
// Endpoint 是腾讯云 IM REST API 区域域名。
|
||
Endpoint string `yaml:"endpoint"`
|
||
// GroupType 是语音房对应的腾讯云 IM 群组类型。
|
||
GroupType string `yaml:"group_type"`
|
||
// RequestTimeout 是单次 REST API 调用超时。
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
// TencentRTCConfig 描述 room-service 调用腾讯云 TRTC 房间管理 API 的配置。
|
||
type TencentRTCConfig struct {
|
||
// Enabled 控制是否启用服务端 RTC 踢人;关闭时仍会提交 Room Cell 驱逐。
|
||
Enabled bool `yaml:"enabled"`
|
||
// SDKAppID 是 TRTC 控制台分配的应用 ID,必须和 gateway 签发 RTC UserSig 的应用一致。
|
||
SDKAppID int64 `yaml:"sdk_app_id"`
|
||
// SecretID/SecretKey 是腾讯云 API 3.0 访问密钥,只能写在线上密钥系统或私有配置中。
|
||
SecretID string `yaml:"secret_id"`
|
||
SecretKey string `yaml:"secret_key"`
|
||
// Region 是腾讯云 API 公共参数;TRTC 房间管理接口当前使用 ap-guangzhou/ap-beijing。
|
||
Region string `yaml:"region"`
|
||
// Endpoint 是腾讯云 API 域名,默认 trtc.tencentcloudapi.com。
|
||
Endpoint string `yaml:"endpoint"`
|
||
// RequestTimeout 是单次公网调用超时。
|
||
RequestTimeout time.Duration `yaml:"request_timeout"`
|
||
}
|
||
|
||
// RESTConfig 把 YAML 配置转换成 pkg/tencentrtc 的 REST 客户端配置。
|
||
func (c TencentRTCConfig) RESTConfig() tencentrtc.RESTConfig {
|
||
timeout := c.RequestTimeout
|
||
if timeout <= 0 {
|
||
timeout = 5 * time.Second
|
||
}
|
||
return tencentrtc.RESTConfig{
|
||
Enabled: c.Enabled,
|
||
SDKAppID: c.SDKAppID,
|
||
SecretID: c.SecretID,
|
||
SecretKey: c.SecretKey,
|
||
Region: c.Region,
|
||
Endpoint: c.Endpoint,
|
||
HTTPClient: &http.Client{Timeout: timeout},
|
||
}
|
||
}
|
||
|
||
// RESTConfig 把 YAML 配置转换成 pkg/tencentim 的 REST 客户端配置。
|
||
func (c TencentIMConfig) RESTConfig() tencentim.RESTConfig {
|
||
timeout := c.RequestTimeout
|
||
if timeout <= 0 {
|
||
// 腾讯云 REST 后台超时约 3 秒,本地客户端默认给 5 秒预算。
|
||
timeout = 5 * time.Second
|
||
}
|
||
|
||
return tencentim.RESTConfig{
|
||
SDKAppID: c.SDKAppID,
|
||
SecretKey: c.SecretKey,
|
||
AdminIdentifier: c.AdminIdentifier,
|
||
AdminUserSigTTL: c.AdminUserSigTTL,
|
||
Endpoint: c.Endpoint,
|
||
GroupType: c.GroupType,
|
||
HTTPClient: &http.Client{Timeout: timeout},
|
||
}
|
||
}
|
||
|
||
// Default 返回没有外部私有密钥时可启动的基础配置。
|
||
func Default() Config {
|
||
// 默认端口遵守项目 13xxx 约束,MySQL/Redis 指向本地 Docker 暴露端口。
|
||
return Config{
|
||
ServiceName: "room-service",
|
||
Environment: "local",
|
||
NodeID: "room-node-local",
|
||
GRPCAddr: ":13001",
|
||
AdvertiseAddr: "127.0.0.1:13001",
|
||
HealthHTTPAddr: ":13101",
|
||
NodeRegistryTTL: 30 * time.Second,
|
||
OwnerForwardTimeout: 2 * time.Second,
|
||
LeaseTTL: 10 * time.Second,
|
||
LuckyGiftSendLockTTL: 5 * time.Second,
|
||
RankLimit: 20,
|
||
SnapshotEveryN: 10,
|
||
WalletServiceAddr: "127.0.0.1:13004",
|
||
ActivityServiceAddr: "127.0.0.1:13006",
|
||
TencentIM: TencentIMConfig{
|
||
Enabled: false,
|
||
AdminIdentifier: "administrator",
|
||
AdminUserSigTTL: 24 * time.Hour,
|
||
Endpoint: tencentim.DefaultEndpoint,
|
||
GroupType: tencentim.DefaultGroupType,
|
||
RequestTimeout: 5 * time.Second,
|
||
},
|
||
TencentRTC: TencentRTCConfig{
|
||
Enabled: false,
|
||
Region: tencentrtc.DefaultRESTRegion,
|
||
Endpoint: tencentrtc.DefaultRESTEndpoint,
|
||
RequestTimeout: 5 * time.Second,
|
||
},
|
||
MySQLDSN: "hyapp:hyapp@tcp(127.0.0.1:23306)/hyapp_room?parseTime=true&charset=utf8mb4&loc=UTC",
|
||
MySQLMaxOpenConns: 20,
|
||
MySQLMaxIdleConns: 10,
|
||
MySQLAutoMigrate: true,
|
||
RedisAddr: "127.0.0.1:13379",
|
||
RedisDB: 0,
|
||
PresenceStaleAfter: 2 * time.Minute,
|
||
PresenceStaleScanInterval: 30 * time.Second,
|
||
MicPublishTimeout: 15 * time.Second,
|
||
MicPublishScanInterval: time.Second,
|
||
RoomRocketLaunchScanInterval: time.Second,
|
||
RocketMQ: defaultRocketMQConfig(),
|
||
OutboxWorker: defaultOutboxWorkerConfig(),
|
||
Log: logx.Config{
|
||
Level: "info",
|
||
Format: "json",
|
||
MaxPayloadBytes: 2048,
|
||
},
|
||
}
|
||
}
|
||
|
||
func defaultOutboxWorkerConfig() OutboxWorkerConfig {
|
||
// 默认启动补偿 worker,使用退避和死信避免外部故障刷屏。
|
||
return OutboxWorkerConfig{
|
||
Enabled: true,
|
||
PublishMode: OutboxPublishModeDirect,
|
||
PollInterval: time.Second,
|
||
BatchSize: 100,
|
||
PublishTimeout: 3 * time.Second,
|
||
RetryStrategy: "exponential_backoff",
|
||
MaxRetryCount: 10,
|
||
InitialBackoff: 5 * time.Second,
|
||
MaxBackoff: 5 * time.Minute,
|
||
}
|
||
}
|
||
|
||
func defaultRocketMQConfig() RocketMQConfig {
|
||
return RocketMQConfig{
|
||
Enabled: false,
|
||
SendTimeout: 3 * time.Second,
|
||
Retry: 2,
|
||
RoomOutbox: RoomOutboxMQConfig{
|
||
Enabled: false,
|
||
Topic: "hyapp_room_outbox",
|
||
ProducerGroup: "hyapp-room-outbox-producer",
|
||
TencentIMConsumerEnabled: false,
|
||
TencentIMConsumerGroup: "hyapp-room-im-bridge",
|
||
ConsumerMaxReconsumeTimes: 16,
|
||
},
|
||
RocketLaunch: RocketLaunchMQConfig{
|
||
Enabled: false,
|
||
Topic: "hyapp_room_rocket_launch",
|
||
ProducerGroup: "hyapp-room-rocket-launch-producer",
|
||
ConsumerGroup: "hyapp-room-rocket-launch",
|
||
ConsumerMaxReconsumeTimes: 16,
|
||
},
|
||
}
|
||
}
|
||
|
||
// Normalize 补齐动态配置默认值,并拒绝会改变补偿语义的未知策略。
|
||
func Normalize(cfg Config) (Config, error) {
|
||
// 配置归一化只处理需要跨环境保持语义的字段,其他默认值由 Default 提供。
|
||
cfg.ServiceName = strings.TrimSpace(cfg.ServiceName)
|
||
if cfg.ServiceName == "" {
|
||
cfg.ServiceName = "room-service"
|
||
}
|
||
cfg.Environment = strings.ToLower(strings.TrimSpace(cfg.Environment))
|
||
if cfg.Environment == "" {
|
||
cfg.Environment = "local"
|
||
}
|
||
cfg.NodeID = strings.TrimSpace(cfg.NodeID)
|
||
if cfg.NodeID == "" {
|
||
cfg.NodeID = cfg.ServiceName
|
||
}
|
||
cfg.GRPCAddr = strings.TrimSpace(cfg.GRPCAddr)
|
||
if cfg.GRPCAddr == "" {
|
||
cfg.GRPCAddr = ":13001"
|
||
}
|
||
cfg.AdvertiseAddr = strings.TrimSpace(cfg.AdvertiseAddr)
|
||
if cfg.AdvertiseAddr == "" {
|
||
cfg.AdvertiseAddr = defaultAdvertiseAddr(cfg.GRPCAddr)
|
||
}
|
||
cfg.HealthHTTPAddr = strings.TrimSpace(cfg.HealthHTTPAddr)
|
||
if cfg.HealthHTTPAddr == "" {
|
||
cfg.HealthHTTPAddr = ":13101"
|
||
}
|
||
if cfg.NodeRegistryTTL <= 0 {
|
||
cfg.NodeRegistryTTL = 30 * time.Second
|
||
}
|
||
if cfg.NodeRegistryHeartbeatInterval <= 0 {
|
||
cfg.NodeRegistryHeartbeatInterval = cfg.NodeRegistryTTL / 3
|
||
}
|
||
if cfg.NodeRegistryHeartbeatInterval <= 0 {
|
||
cfg.NodeRegistryHeartbeatInterval = 10 * time.Second
|
||
}
|
||
if cfg.NodeRegistryHeartbeatInterval >= cfg.NodeRegistryTTL {
|
||
cfg.NodeRegistryHeartbeatInterval = cfg.NodeRegistryTTL / 3
|
||
}
|
||
if cfg.OwnerForwardTimeout <= 0 {
|
||
cfg.OwnerForwardTimeout = 2 * time.Second
|
||
}
|
||
if cfg.LuckyGiftSendLockTTL <= 0 {
|
||
cfg.LuckyGiftSendLockTTL = 5 * time.Second
|
||
}
|
||
cfg.WalletServiceAddr = strings.TrimSpace(cfg.WalletServiceAddr)
|
||
if cfg.WalletServiceAddr == "" {
|
||
cfg.WalletServiceAddr = "127.0.0.1:13004"
|
||
}
|
||
cfg.ActivityServiceAddr = strings.TrimSpace(cfg.ActivityServiceAddr)
|
||
if cfg.ActivityServiceAddr == "" {
|
||
cfg.ActivityServiceAddr = "127.0.0.1:13006"
|
||
}
|
||
cfg.TencentRTC.SecretID = strings.TrimSpace(cfg.TencentRTC.SecretID)
|
||
cfg.TencentRTC.SecretKey = strings.TrimSpace(cfg.TencentRTC.SecretKey)
|
||
cfg.TencentRTC.Region = strings.TrimSpace(cfg.TencentRTC.Region)
|
||
if cfg.TencentRTC.Region == "" {
|
||
cfg.TencentRTC.Region = tencentrtc.DefaultRESTRegion
|
||
}
|
||
cfg.TencentRTC.Endpoint = strings.TrimSpace(cfg.TencentRTC.Endpoint)
|
||
if cfg.TencentRTC.Endpoint == "" {
|
||
cfg.TencentRTC.Endpoint = tencentrtc.DefaultRESTEndpoint
|
||
}
|
||
if cfg.TencentRTC.RequestTimeout <= 0 {
|
||
cfg.TencentRTC.RequestTimeout = 5 * time.Second
|
||
}
|
||
outboxWorker, err := normalizeOutboxWorkerConfig(cfg.OutboxWorker)
|
||
if err != nil {
|
||
return Config{}, err
|
||
}
|
||
cfg.OutboxWorker = outboxWorker
|
||
rocketMQ, err := normalizeRocketMQConfig(cfg.RocketMQ, cfg.OutboxWorker.PublishMode)
|
||
if err != nil {
|
||
return Config{}, err
|
||
}
|
||
cfg.RocketMQ = rocketMQ
|
||
|
||
return cfg, nil
|
||
}
|
||
|
||
func defaultAdvertiseAddr(grpcAddr string) string {
|
||
host, port, err := net.SplitHostPort(grpcAddr)
|
||
if err != nil {
|
||
// 非 host:port 形态保持原值,由启动拨测暴露错误。
|
||
return grpcAddr
|
||
}
|
||
if host == "" || host == "0.0.0.0" || host == "::" {
|
||
// 本地默认值只能服务单机开发;多机部署必须显式配置真实私网 IP 或主机名。
|
||
host = "127.0.0.1"
|
||
}
|
||
return net.JoinHostPort(host, port)
|
||
}
|
||
|
||
func normalizeOutboxWorkerConfig(cfg OutboxWorkerConfig) (OutboxWorkerConfig, error) {
|
||
// Outbox worker 的所有时间和批量参数都必须有安全下限,避免配置缺省导致 busy loop。
|
||
defaults := defaultOutboxWorkerConfig()
|
||
if cfg.PollInterval <= 0 {
|
||
cfg.PollInterval = defaults.PollInterval
|
||
}
|
||
if cfg.BatchSize <= 0 {
|
||
cfg.BatchSize = defaults.BatchSize
|
||
}
|
||
if cfg.PublishTimeout <= 0 {
|
||
cfg.PublishTimeout = defaults.PublishTimeout
|
||
}
|
||
if cfg.MaxRetryCount <= 0 {
|
||
cfg.MaxRetryCount = defaults.MaxRetryCount
|
||
}
|
||
if cfg.InitialBackoff <= 0 {
|
||
cfg.InitialBackoff = defaults.InitialBackoff
|
||
}
|
||
if cfg.MaxBackoff <= 0 {
|
||
cfg.MaxBackoff = defaults.MaxBackoff
|
||
}
|
||
if cfg.MaxBackoff < cfg.InitialBackoff {
|
||
cfg.MaxBackoff = cfg.InitialBackoff
|
||
}
|
||
cfg.RetryStrategy = strings.TrimSpace(cfg.RetryStrategy)
|
||
if cfg.RetryStrategy == "" {
|
||
cfg.RetryStrategy = defaults.RetryStrategy
|
||
}
|
||
if cfg.RetryStrategy != defaults.RetryStrategy {
|
||
// 当前只实现指数退避;未知策略直接启动失败,避免误以为退避或死信已生效。
|
||
return OutboxWorkerConfig{}, fmt.Errorf("unsupported outbox_worker retry_strategy %q", cfg.RetryStrategy)
|
||
}
|
||
cfg.PublishMode = strings.ToLower(strings.TrimSpace(cfg.PublishMode))
|
||
if cfg.PublishMode == "" {
|
||
cfg.PublishMode = defaults.PublishMode
|
||
}
|
||
switch cfg.PublishMode {
|
||
case OutboxPublishModeDirect, OutboxPublishModeMQ, OutboxPublishModeDual:
|
||
default:
|
||
return OutboxWorkerConfig{}, fmt.Errorf("unsupported outbox_worker publish_mode %q", cfg.PublishMode)
|
||
}
|
||
|
||
return cfg, nil
|
||
}
|
||
|
||
func normalizeRocketMQConfig(cfg RocketMQConfig, publishMode string) (RocketMQConfig, error) {
|
||
defaults := defaultRocketMQConfig()
|
||
cfg.NameServers = normalizeStringSlice(cfg.NameServers)
|
||
cfg.NameServerDomain = strings.TrimSpace(cfg.NameServerDomain)
|
||
cfg.AccessKey = strings.TrimSpace(cfg.AccessKey)
|
||
cfg.SecretKey = strings.TrimSpace(cfg.SecretKey)
|
||
cfg.SecurityToken = strings.TrimSpace(cfg.SecurityToken)
|
||
cfg.Namespace = strings.TrimSpace(cfg.Namespace)
|
||
if cfg.SendTimeout <= 0 {
|
||
cfg.SendTimeout = defaults.SendTimeout
|
||
}
|
||
if cfg.Retry < 0 {
|
||
cfg.Retry = defaults.Retry
|
||
}
|
||
if cfg.RoomOutbox.Topic = strings.TrimSpace(cfg.RoomOutbox.Topic); cfg.RoomOutbox.Topic == "" {
|
||
cfg.RoomOutbox.Topic = defaults.RoomOutbox.Topic
|
||
}
|
||
if cfg.RoomOutbox.ProducerGroup = strings.TrimSpace(cfg.RoomOutbox.ProducerGroup); cfg.RoomOutbox.ProducerGroup == "" {
|
||
cfg.RoomOutbox.ProducerGroup = defaults.RoomOutbox.ProducerGroup
|
||
}
|
||
if cfg.RoomOutbox.TencentIMConsumerGroup = strings.TrimSpace(cfg.RoomOutbox.TencentIMConsumerGroup); cfg.RoomOutbox.TencentIMConsumerGroup == "" {
|
||
cfg.RoomOutbox.TencentIMConsumerGroup = defaults.RoomOutbox.TencentIMConsumerGroup
|
||
}
|
||
if cfg.RoomOutbox.ConsumerMaxReconsumeTimes <= 0 {
|
||
cfg.RoomOutbox.ConsumerMaxReconsumeTimes = defaults.RoomOutbox.ConsumerMaxReconsumeTimes
|
||
}
|
||
if cfg.RocketLaunch.Topic = strings.TrimSpace(cfg.RocketLaunch.Topic); cfg.RocketLaunch.Topic == "" {
|
||
cfg.RocketLaunch.Topic = defaults.RocketLaunch.Topic
|
||
}
|
||
if cfg.RocketLaunch.ProducerGroup = strings.TrimSpace(cfg.RocketLaunch.ProducerGroup); cfg.RocketLaunch.ProducerGroup == "" {
|
||
cfg.RocketLaunch.ProducerGroup = defaults.RocketLaunch.ProducerGroup
|
||
}
|
||
if cfg.RocketLaunch.ConsumerGroup = strings.TrimSpace(cfg.RocketLaunch.ConsumerGroup); cfg.RocketLaunch.ConsumerGroup == "" {
|
||
cfg.RocketLaunch.ConsumerGroup = defaults.RocketLaunch.ConsumerGroup
|
||
}
|
||
if cfg.RocketLaunch.ConsumerMaxReconsumeTimes <= 0 {
|
||
cfg.RocketLaunch.ConsumerMaxReconsumeTimes = defaults.RocketLaunch.ConsumerMaxReconsumeTimes
|
||
}
|
||
if cfg.RoomOutbox.Enabled || cfg.RocketLaunch.Enabled || cfg.RoomOutbox.TencentIMConsumerEnabled {
|
||
cfg.Enabled = true
|
||
}
|
||
if publishMode == OutboxPublishModeMQ || publishMode == OutboxPublishModeDual {
|
||
if !cfg.RoomOutbox.Enabled {
|
||
return RocketMQConfig{}, fmt.Errorf("outbox_worker publish_mode %q requires rocketmq.room_outbox.enabled", publishMode)
|
||
}
|
||
}
|
||
if cfg.Enabled {
|
||
if len(cfg.NameServers) == 0 && cfg.NameServerDomain == "" {
|
||
return RocketMQConfig{}, fmt.Errorf("rocketmq name_servers or name_server_domain is required")
|
||
}
|
||
if (cfg.AccessKey == "") != (cfg.SecretKey == "") {
|
||
return RocketMQConfig{}, fmt.Errorf("rocketmq access_key and secret_key must be configured together")
|
||
}
|
||
}
|
||
return cfg, nil
|
||
}
|
||
|
||
func normalizeStringSlice(input []string) []string {
|
||
out := make([]string, 0, len(input))
|
||
for _, value := range input {
|
||
if value = strings.TrimSpace(value); value != "" {
|
||
out = append(out, value)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// Load 从独立 config.yaml 读取 room-service 配置。
|
||
func Load(path string) (Config, error) {
|
||
cfg := Default()
|
||
if path == "" {
|
||
// 空路径代表显式使用默认配置,便于单元测试和最小启动。
|
||
return Normalize(cfg)
|
||
}
|
||
|
||
if err := configx.LoadYAML(path, &cfg); err != nil {
|
||
// YAML 解析失败返回空 Config,避免调用方误用部分默认值启动。
|
||
return Config{}, err
|
||
}
|
||
|
||
return Normalize(cfg)
|
||
}
|