2026-05-12 19:34:47 +08:00

261 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package config 定义 room-service 的启动配置和 YAML 加载入口。
package config
import (
"fmt"
"net/http"
"strings"
"time"
"hyapp/pkg/configx"
"hyapp/pkg/logx"
"hyapp/pkg/tencentim"
)
// 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"`
// LeaseTTL 是 room owner 租约有效期,节点故障后必须等它过期才能接管。
LeaseTTL time.Duration `yaml:"lease_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"`
// 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"`
// 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"`
// 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"`
}
// 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"`
}
// 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",
LeaseTTL: 10 * 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,
},
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,
OutboxWorker: defaultOutboxWorkerConfig(),
Log: logx.Config{
Level: "info",
Format: "json",
MaxPayloadBytes: 2048,
},
}
}
func defaultOutboxWorkerConfig() OutboxWorkerConfig {
// 默认启动补偿 worker使用退避和死信避免外部故障刷屏。
return OutboxWorkerConfig{
Enabled: true,
PollInterval: time.Second,
BatchSize: 100,
PublishTimeout: 3 * time.Second,
RetryStrategy: "exponential_backoff",
MaxRetryCount: 10,
InitialBackoff: 5 * time.Second,
MaxBackoff: 5 * time.Minute,
}
}
// 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.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"
}
outboxWorker, err := normalizeOutboxWorkerConfig(cfg.OutboxWorker)
if err != nil {
return Config{}, err
}
cfg.OutboxWorker = outboxWorker
return cfg, nil
}
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)
}
return cfg, nil
}
// 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)
}