261 lines
10 KiB
Go
261 lines
10 KiB
Go
// 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)
|
||
}
|