578 lines
20 KiB
Go
578 lines
20 KiB
Go
// Package app 负责把 room-service 的领域服务、存储、路由目录和 gRPC 入口装配成进程。
|
||
package app
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log/slog"
|
||
"net"
|
||
"sync"
|
||
"time"
|
||
|
||
"google.golang.org/grpc"
|
||
"google.golang.org/grpc/credentials/insecure"
|
||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||
roomv1 "hyapp.local/api/proto/room/v1"
|
||
"hyapp/pkg/healthhttp"
|
||
"hyapp/pkg/logx"
|
||
"hyapp/pkg/rocketmqx"
|
||
"hyapp/pkg/roommq"
|
||
serviceapp "hyapp/pkg/servicekit/app"
|
||
servicegrpc "hyapp/pkg/servicekit/grpcserver"
|
||
servicehealth "hyapp/pkg/servicekit/health"
|
||
servicemq "hyapp/pkg/servicekit/mq"
|
||
"hyapp/pkg/tencentim"
|
||
"hyapp/pkg/tencentrtc"
|
||
"hyapp/pkg/usermq"
|
||
"hyapp/services/room-service/internal/config"
|
||
"hyapp/services/room-service/internal/healthcheck"
|
||
"hyapp/services/room-service/internal/integration"
|
||
roomservice "hyapp/services/room-service/internal/room/service"
|
||
"hyapp/services/room-service/internal/router"
|
||
mysqlstorage "hyapp/services/room-service/internal/storage/mysql"
|
||
grpcserver "hyapp/services/room-service/internal/transport/grpc"
|
||
)
|
||
|
||
// App 负责装配 room-service 运行时依赖。
|
||
type App struct {
|
||
// cfg 保留启动配置,便于后续扩展后台 worker 时复用。
|
||
cfg config.Config
|
||
// grpcServer 承载 RoomCommandService、RoomGuardService 和标准 gRPC health。
|
||
grpcServer *grpc.Server
|
||
// service 是 Room Cell 领域入口,后台 presence worker 复用它执行清理命令。
|
||
service *roomservice.Service
|
||
// listener 是 room-service 的唯一网络入口。
|
||
listener net.Listener
|
||
// walletConn 是 SendGift 同步扣费依赖。
|
||
walletConn *grpc.ClientConn
|
||
// activityConn 是 room outbox 事实投递到 activity-service 的 best-effort 通道。
|
||
activityConn *grpc.ClientConn
|
||
// robotConn 是真人房间机器人运行时读取全站机器人池的依赖。
|
||
robotConn *grpc.ClientConn
|
||
// userConn 是真人房间机器人运行时读取机器人国家资料的依赖。
|
||
userConn *grpc.ClientConn
|
||
// repository 是 MySQL 持久化实现,保存 meta、command log、snapshot 和 outbox。
|
||
repository *mysqlstorage.Repository
|
||
// redisClose 关闭 Redis client,Redis directory 本身不拥有额外生命周期。
|
||
redisClose func() error
|
||
// nodeRegistry 保存当前节点可被 owner 转发直连的实例地址。
|
||
nodeRegistry router.NodeRegistry
|
||
// health 汇总 gRPC、runtime、MySQL 和 Redis lease 探测状态。
|
||
health *healthcheck.State
|
||
// healthHTTP 给 CLB 和发布脚本提供 HTTP readiness,不承载业务请求。
|
||
healthHTTP *healthhttp.Server
|
||
// mqProducers 在 outbox worker 启动前打开,承载 fanout 和延迟发射唤醒。
|
||
mqProducers []*rocketmqx.Producer
|
||
// mqConsumers 在 gRPC 服务启动前订阅,承接 room_outbox fanout 和 delayed open。
|
||
mqConsumers []*rocketmqx.Consumer
|
||
// workers 统一控制本节点后台 worker,关闭时必须先停止 worker 再释放 MySQL/Redis。
|
||
workers *serviceapp.BackgroundGroup
|
||
// closeOnce 防止信号退出和 Serve 错误同时触发重复关闭。
|
||
closeOnce sync.Once
|
||
}
|
||
|
||
// New 初始化 room-service 应用。
|
||
func New(cfg config.Config) (*App, error) {
|
||
normalized, err := config.Normalize(cfg)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
cfg = normalized
|
||
|
||
// wallet-service 是 SendGift 主链路依赖,连接对象创建失败时不能启动。
|
||
walletConn, err := grpc.Dial(cfg.WalletServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var activityConn *grpc.ClientConn
|
||
if cfg.ActivityServiceAddr != "" {
|
||
activityConn, err = grpc.Dial(cfg.ActivityServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||
if err != nil {
|
||
_ = walletConn.Close()
|
||
return nil, err
|
||
}
|
||
}
|
||
robotConn, err := grpc.Dial(cfg.RobotServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||
if err != nil {
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
return nil, err
|
||
}
|
||
userConn, err := grpc.Dial(cfg.UserServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||
if err != nil {
|
||
closeClientConn(robotConn)
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
return nil, err
|
||
}
|
||
closeRobotDepsOnError := true
|
||
defer func() {
|
||
if closeRobotDepsOnError {
|
||
closeClientConn(userConn)
|
||
closeClientConn(robotConn)
|
||
}
|
||
}()
|
||
|
||
startupCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||
defer cancel()
|
||
|
||
// MySQL 是房间恢复的持久来源,启动阶段必须确认连接池可用。
|
||
repository, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN, cfg.MySQLMaxOpenConns, cfg.MySQLMaxIdleConns)
|
||
if err != nil {
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
return nil, err
|
||
}
|
||
|
||
if cfg.MySQLAutoMigrate {
|
||
// 本地和测试环境可自动建表,线上应通过显式迁移控制结构变更。
|
||
if err := repository.Migrate(startupCtx); err != nil {
|
||
_ = repository.Close()
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
// Redis directory 保存 room owner lease,是单房间单活的运行时仲裁来源。
|
||
redisClient, err := router.NewRedisClient(startupCtx, cfg.RedisAddr, cfg.RedisPassword, cfg.RedisDB)
|
||
if err != nil {
|
||
_ = repository.Close()
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
return nil, err
|
||
}
|
||
|
||
directory := router.NewRedisDirectory(redisClient)
|
||
roomPublisher := integration.NewNoopRoomEventPublisher()
|
||
outboxPublishers := make([]integration.OutboxPublisher, 0, 3)
|
||
var rtcUserRemover integration.RTCUserRemover
|
||
var tencentPublisher *integration.TencentIMPublisher
|
||
if cfg.TencentIM.Enabled {
|
||
// 腾讯云 IM 替代自研 IM;room-service 只负责服务端 REST 群消息桥接。
|
||
tencentClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
|
||
if err != nil {
|
||
_ = repository.Close()
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
_ = redisClient.Close()
|
||
return nil, err
|
||
}
|
||
tencentPublisher = integration.NewTencentIMPublisher(tencentClient)
|
||
roomPublisher = tencentPublisher
|
||
}
|
||
if cfg.TencentRTC.Enabled {
|
||
rtcClient, err := tencentrtc.NewRESTClient(cfg.TencentRTC.RESTConfig())
|
||
if err != nil {
|
||
_ = repository.Close()
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
_ = redisClient.Close()
|
||
return nil, err
|
||
}
|
||
rtcUserRemover = rtcClient
|
||
}
|
||
|
||
mqProducers := make([]*rocketmqx.Producer, 0, 2)
|
||
mqConsumers := make([]*rocketmqx.Consumer, 0, 3)
|
||
var rocketLaunchScheduler integration.RoomRocketLaunchScheduler
|
||
if cfg.OutboxWorker.PublishMode == config.OutboxPublishModeDirect || cfg.OutboxWorker.PublishMode == config.OutboxPublishModeDual {
|
||
if activityConn != nil {
|
||
activityPublisher := integration.NewActivityOutboxPublisher(activityv1.NewRoomEventConsumerServiceClient(activityConn), time.Second)
|
||
outboxPublishers = append(outboxPublishers, activityPublisher)
|
||
}
|
||
}
|
||
if cfg.OutboxWorker.PublishMode == config.OutboxPublishModeMQ || cfg.OutboxWorker.PublishMode == config.OutboxPublishModeDual {
|
||
outboxProducer, err := rocketmqx.NewProducer(rocketMQProducerConfig(cfg.RocketMQ, cfg.RocketMQ.RoomOutbox.ProducerGroup))
|
||
if err != nil {
|
||
_ = repository.Close()
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
_ = redisClient.Close()
|
||
return nil, err
|
||
}
|
||
mqProducers = append(mqProducers, outboxProducer)
|
||
outboxPublishers = append(outboxPublishers, integration.NewRocketMQOutboxPublisher(outboxProducer, cfg.RocketMQ.RoomOutbox.Topic))
|
||
}
|
||
if cfg.RocketMQ.RocketLaunch.Enabled {
|
||
rocketProducer, err := rocketmqx.NewProducer(rocketMQProducerConfig(cfg.RocketMQ, cfg.RocketMQ.RocketLaunch.ProducerGroup))
|
||
if err != nil {
|
||
_ = repository.Close()
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
_ = redisClient.Close()
|
||
return nil, err
|
||
}
|
||
mqProducers = append(mqProducers, rocketProducer)
|
||
rocketLaunchScheduler = integration.NewRocketMQRocketLaunchScheduler(rocketProducer, cfg.RocketMQ.RocketLaunch.Topic)
|
||
}
|
||
outboxPublisher := integration.NewCompositeOutboxPublisher(outboxPublishers...)
|
||
var luckyGiftClient integration.LuckyGiftClient
|
||
if activityConn != nil {
|
||
luckyGiftClient = integration.NewGRPCLuckyGiftClient(activityConn)
|
||
}
|
||
|
||
// 领域服务只依赖接口,App 层负责选择 MySQL/Redis/gRPC 具体实现。
|
||
svc := roomservice.New(roomservice.Config{
|
||
NodeID: cfg.NodeID,
|
||
LeaseTTL: cfg.LeaseTTL,
|
||
RankLimit: cfg.RankLimit,
|
||
SnapshotEveryN: cfg.SnapshotEveryN,
|
||
PresenceStaleAfter: cfg.PresenceStaleAfter,
|
||
MicPublishTimeout: cfg.MicPublishTimeout,
|
||
RTCUserRemover: rtcUserRemover,
|
||
RoomRocketLaunchScheduler: rocketLaunchScheduler,
|
||
RobotDisplayPublisher: tencentPublisher,
|
||
RoomDisplayPublisher: tencentPublisher,
|
||
HumanRobotPoolProvider: integration.NewGRPCHumanRoomRobotPoolProvider(robotConn, userConn),
|
||
RoomGiftLeaderboard: roomservice.NewRedisRoomGiftLeaderboardStore(redisClient),
|
||
RoomListCache: roomservice.NewRedisRoomListCacheStore(redisClient),
|
||
LuckyGiftSendLocker: roomservice.NewRedisLuckyGiftSendLocker(redisClient),
|
||
LuckyGiftSendLockTTL: cfg.LuckyGiftSendLockTTL,
|
||
}, directory, repository, integration.NewGRPCWalletClient(walletConn), roomPublisher, outboxPublisher, luckyGiftClient)
|
||
if cfg.RocketMQ.RocketLaunch.Enabled {
|
||
rocketConsumer, err := rocketmqx.NewConsumer(rocketMQConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.RocketLaunch.ConsumerGroup, cfg.RocketMQ.RocketLaunch.ConsumerMaxReconsumeTimes, 0))
|
||
if err != nil {
|
||
_ = repository.Close()
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
_ = redisClient.Close()
|
||
return nil, err
|
||
}
|
||
if err := rocketConsumer.Subscribe(cfg.RocketMQ.RocketLaunch.Topic, roommq.TagRoomRocketLaunchDue, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
|
||
due, err := roommq.DecodeRoomRocketLaunchDueMessage(message.Body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return svc.HandleRoomRocketLaunchDue(ctx, due)
|
||
}); err != nil {
|
||
_ = repository.Close()
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
_ = redisClient.Close()
|
||
return nil, err
|
||
}
|
||
mqConsumers = append(mqConsumers, rocketConsumer)
|
||
}
|
||
if cfg.RocketMQ.UserOutbox.Enabled {
|
||
userConsumer, err := rocketmqx.NewConsumer(rocketMQConsumerConfig(cfg.RocketMQ, cfg.RocketMQ.UserOutbox.ConsumerGroup, cfg.RocketMQ.UserOutbox.ConsumerMaxReconsumeTimes, 0))
|
||
if err != nil {
|
||
_ = repository.Close()
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
_ = redisClient.Close()
|
||
return nil, err
|
||
}
|
||
if err := userConsumer.Subscribe(cfg.RocketMQ.UserOutbox.Topic, usermq.TagUserOutboxEvent, func(ctx context.Context, message rocketmqx.ConsumedMessage) error {
|
||
outboxMessage, err := usermq.DecodeUserOutboxMessage(message.Body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return svc.HandleUserOutboxMessage(ctx, outboxMessage)
|
||
}); err != nil {
|
||
_ = repository.Close()
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
_ = redisClient.Close()
|
||
return nil, err
|
||
}
|
||
mqConsumers = append(mqConsumers, userConsumer)
|
||
}
|
||
|
||
server := servicegrpc.New("room-service")
|
||
roomServer := grpcserver.NewServer(svc, grpcserver.WithOwnerForwarding(cfg.NodeID, directory, directory, cfg.OwnerForwardTimeout))
|
||
// 命令服务处理房间状态变更,守卫服务给腾讯云 IM 回调或 gateway 做发言和进群校验。
|
||
roomv1.RegisterRoomCommandServiceServer(server, roomServer)
|
||
roomv1.RegisterRoomGuardServiceServer(server, roomServer)
|
||
roomv1.RegisterRoomQueryServiceServer(server, roomServer)
|
||
healthState := healthcheck.NewState(cfg.NodeID, svc, repository, directory)
|
||
healthHTTP, err := servicehealth.Register(server, healthState, cfg.HealthHTTPAddr, cfg.NodeID)
|
||
if err != nil {
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
_ = repository.Close()
|
||
_ = redisClient.Close()
|
||
return nil, err
|
||
}
|
||
|
||
// listener 在 New 阶段创建,可以把端口占用作为启动失败暴露。
|
||
listener, err := net.Listen("tcp", cfg.GRPCAddr)
|
||
if err != nil {
|
||
_ = healthHTTP.Close(context.Background())
|
||
closeClientConn(activityConn)
|
||
_ = walletConn.Close()
|
||
_ = repository.Close()
|
||
_ = redisClient.Close()
|
||
return nil, err
|
||
}
|
||
closeRobotDepsOnError = false
|
||
|
||
return &App{
|
||
cfg: cfg,
|
||
grpcServer: server,
|
||
service: svc,
|
||
listener: listener,
|
||
walletConn: walletConn,
|
||
activityConn: activityConn,
|
||
robotConn: robotConn,
|
||
userConn: userConn,
|
||
repository: repository,
|
||
redisClose: redisClient.Close,
|
||
nodeRegistry: directory,
|
||
health: healthState,
|
||
healthHTTP: healthHTTP,
|
||
mqProducers: mqProducers,
|
||
mqConsumers: mqConsumers,
|
||
workers: serviceapp.NewBackground(context.Background()),
|
||
}, nil
|
||
}
|
||
|
||
// Run 启动 gRPC 服务。
|
||
func (a *App) Run() error {
|
||
a.runHealthHTTP()
|
||
// health 先标记 serving,再进入 Serve,ready 才能在监听开始后通过。
|
||
a.health.MarkGRPCServing()
|
||
if err := a.registerRoomNode(context.Background()); err != nil {
|
||
a.health.MarkGRPCStopped()
|
||
return err
|
||
}
|
||
if err := a.startMQ(); err != nil {
|
||
a.health.MarkGRPCStopped()
|
||
return err
|
||
}
|
||
defer func() {
|
||
a.workers.StopAndWait()
|
||
a.shutdownMQ()
|
||
}()
|
||
a.workers.Go(func(ctx context.Context) {
|
||
a.runNodeRegistryHeartbeat(ctx)
|
||
})
|
||
if a.cfg.PresenceStaleScanInterval > 0 {
|
||
// presence worker 只清理本节点已装载 Cell,命令仍走 Room Cell 持久化链路。
|
||
a.workers.Go(func(ctx context.Context) {
|
||
a.service.RunPresenceStaleWorker(ctx, a.cfg.PresenceStaleScanInterval)
|
||
})
|
||
}
|
||
if a.cfg.MicPublishScanInterval > 0 {
|
||
// MicUp 只代表业务占麦;后台 worker 负责释放未确认 RTC 发流的 pending_publish 麦位。
|
||
a.workers.Go(func(ctx context.Context) {
|
||
a.service.RunMicPublishTimeoutWorker(ctx, a.cfg.MicPublishScanInterval)
|
||
})
|
||
}
|
||
if a.cfg.RoomRocketLaunchScanInterval > 0 {
|
||
// 火箭发射仍通过 Room Cell 命令链路提交,worker 只负责触发到点的系统命令。
|
||
a.workers.Go(func(ctx context.Context) {
|
||
a.service.RunRoomRocketLaunchWorker(ctx, a.cfg.RoomRocketLaunchScanInterval)
|
||
})
|
||
}
|
||
a.workers.Go(func(ctx context.Context) {
|
||
// 火箭燃料进度允许 1 秒展示延迟;普通房写主 outbox,机器人展示到期后 best-effort 直发腾讯 IM。
|
||
a.service.RunRoomRocketProgressFlushWorker(ctx, 100*time.Millisecond)
|
||
})
|
||
if a.cfg.RoomListCacheRefreshInterval > 0 {
|
||
// Redis 发现页缓存是 MySQL owner 表的展示读模型,启动后先 warmup,再周期性修复 best-effort 增量丢失。
|
||
a.workers.Go(func(ctx context.Context) {
|
||
a.service.RunRoomListCacheRefreshWorker(ctx, a.cfg.RoomListCacheRefreshInterval)
|
||
})
|
||
}
|
||
if a.cfg.OutboxWorker.Enabled {
|
||
a.workers.Go(func(ctx context.Context) {
|
||
a.service.RunOutboxWorker(ctx, roomservice.OutboxWorkerOptions{
|
||
PollInterval: a.cfg.OutboxWorker.PollInterval,
|
||
BatchSize: a.cfg.OutboxWorker.BatchSize,
|
||
Concurrency: a.cfg.OutboxWorker.Concurrency,
|
||
PublishTimeout: a.cfg.OutboxWorker.PublishTimeout,
|
||
RetryStrategy: a.cfg.OutboxWorker.RetryStrategy,
|
||
MaxRetryCount: a.cfg.OutboxWorker.MaxRetryCount,
|
||
InitialBackoff: a.cfg.OutboxWorker.InitialBackoff,
|
||
MaxBackoff: a.cfg.OutboxWorker.MaxBackoff,
|
||
})
|
||
})
|
||
}
|
||
a.workers.Go(func(ctx context.Context) {
|
||
a.service.RunRobotRoomRuntimeManager(ctx, 10*time.Second)
|
||
})
|
||
a.workers.Go(func(ctx context.Context) {
|
||
a.service.RunHumanRoomRobotRuntimeManager(ctx, 15*time.Second)
|
||
})
|
||
|
||
err := a.grpcServer.Serve(a.listener)
|
||
a.health.MarkGRPCStopped()
|
||
if errors.Is(err, grpc.ErrServerStopped) {
|
||
// GracefulStop 触发的退出不是故障。
|
||
return nil
|
||
}
|
||
|
||
return err
|
||
}
|
||
|
||
// Close 关闭底层 gRPC 服务。
|
||
func (a *App) Close() {
|
||
a.closeOnce.Do(func() {
|
||
// 先进入 draining,避免下线中的节点继续接管房间 lease 或接收新命令。
|
||
a.health.MarkDraining()
|
||
if a.service != nil {
|
||
a.service.MarkDraining()
|
||
}
|
||
// 先停后台 worker 并等待当前投递结果落库,再关闭 gRPC/MySQL/Redis。
|
||
a.workers.StopAndWait()
|
||
a.shutdownMQ()
|
||
// GracefulStop 让已进入的 gRPC 请求自然结束。
|
||
servicegrpc.GracefulStop(a.grpcServer, 15*time.Second)
|
||
if a.service != nil {
|
||
// gRPC drain 完成后释放本节点持有的 room route,降低滚动发布时等待 Redis TTL 的窗口。
|
||
releaseCtx, releaseCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
a.service.ReleaseLoadedRoomLeases(releaseCtx)
|
||
releaseCancel()
|
||
}
|
||
a.unregisterRoomNode()
|
||
a.closeHealthHTTP()
|
||
if a.walletConn != nil {
|
||
_ = a.walletConn.Close()
|
||
}
|
||
if a.activityConn != nil {
|
||
_ = a.activityConn.Close()
|
||
}
|
||
if a.robotConn != nil {
|
||
_ = a.robotConn.Close()
|
||
}
|
||
if a.userConn != nil {
|
||
_ = a.userConn.Close()
|
||
}
|
||
if a.repository != nil {
|
||
// MySQL 连接池最后释放,保证 GracefulStop 期间仍可完成持久化。
|
||
_ = a.repository.Close()
|
||
}
|
||
if a.redisClose != nil {
|
||
// Redis client 关闭后本节点不再续约或接管房间。
|
||
_ = a.redisClose()
|
||
}
|
||
})
|
||
}
|
||
|
||
func (a *App) registerRoomNode(ctx context.Context) error {
|
||
if a.nodeRegistry == nil {
|
||
return nil
|
||
}
|
||
if _, ok := ctx.Deadline(); !ok {
|
||
var cancel context.CancelFunc
|
||
ctx, cancel = context.WithTimeout(ctx, 2*time.Second)
|
||
defer cancel()
|
||
}
|
||
return a.nodeRegistry.RegisterNode(ctx, router.NodeRegistration{
|
||
NodeID: a.cfg.NodeID,
|
||
GRPCAddr: a.cfg.AdvertiseAddr,
|
||
}, a.cfg.NodeRegistryTTL)
|
||
}
|
||
|
||
func (a *App) runNodeRegistryHeartbeat(ctx context.Context) {
|
||
if a.nodeRegistry == nil {
|
||
return
|
||
}
|
||
ticker := time.NewTicker(a.cfg.NodeRegistryHeartbeatInterval)
|
||
defer ticker.Stop()
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return
|
||
case <-ticker.C:
|
||
if err := a.registerRoomNode(ctx); err != nil {
|
||
logx.Warn(ctx, "room_node_register_failed",
|
||
slog.String("node_id", a.cfg.NodeID),
|
||
slog.String("advertise_addr", a.cfg.AdvertiseAddr),
|
||
slog.String("error", err.Error()),
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func (a *App) unregisterRoomNode() {
|
||
if a.nodeRegistry == nil {
|
||
return
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||
defer cancel()
|
||
if err := a.nodeRegistry.UnregisterNode(ctx, a.cfg.NodeID); err != nil {
|
||
logx.Warn(ctx, "room_node_unregister_failed",
|
||
slog.String("node_id", a.cfg.NodeID),
|
||
slog.String("error", err.Error()),
|
||
)
|
||
}
|
||
}
|
||
|
||
func (a *App) runHealthHTTP() {
|
||
if a.healthHTTP == nil {
|
||
return
|
||
}
|
||
go func() {
|
||
if err := a.healthHTTP.Run(); err != nil {
|
||
logx.Error(context.Background(), "health_http_run_failed", err)
|
||
}
|
||
}()
|
||
}
|
||
|
||
func (a *App) closeHealthHTTP() {
|
||
if a.healthHTTP == nil {
|
||
return
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||
defer cancel()
|
||
_ = a.healthHTTP.Close(ctx)
|
||
}
|
||
|
||
func (a *App) startMQ() error {
|
||
if err := servicemq.StartProducers(a.mqProducers); err != nil {
|
||
return err
|
||
}
|
||
if err := servicemq.StartConsumers(a.mqConsumers); err != nil {
|
||
servicemq.ShutdownProducers(a.mqProducers)
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (a *App) shutdownMQ() {
|
||
servicemq.ShutdownConsumers(a.mqConsumers)
|
||
servicemq.ShutdownProducers(a.mqProducers)
|
||
}
|
||
|
||
func rocketMQProducerConfig(cfg config.RocketMQConfig, groupName string) rocketmqx.ProducerConfig {
|
||
return rocketmqx.ProducerConfig{
|
||
EndpointConfig: rocketMQEndpointConfig(cfg),
|
||
GroupName: groupName,
|
||
SendTimeout: cfg.SendTimeout,
|
||
Retry: cfg.Retry,
|
||
}
|
||
}
|
||
|
||
func rocketMQConsumerConfig(cfg config.RocketMQConfig, groupName string, maxReconsumeTimes int32, consumeGoroutines int) rocketmqx.ConsumerConfig {
|
||
return rocketmqx.ConsumerConfig{
|
||
EndpointConfig: rocketMQEndpointConfig(cfg),
|
||
GroupName: groupName,
|
||
MaxReconsumeTimes: maxReconsumeTimes,
|
||
ConsumeRetryDelay: time.Second,
|
||
ConsumePullBatch: 32,
|
||
ConsumePullTimeout: 15 * time.Minute,
|
||
ConsumeGoroutines: consumeGoroutines,
|
||
}
|
||
}
|
||
|
||
func rocketMQEndpointConfig(cfg config.RocketMQConfig) rocketmqx.EndpointConfig {
|
||
return rocketmqx.EndpointConfig{
|
||
NameServers: cfg.NameServers,
|
||
NameServerDomain: cfg.NameServerDomain,
|
||
AccessKey: cfg.AccessKey,
|
||
SecretKey: cfg.SecretKey,
|
||
SecurityToken: cfg.SecurityToken,
|
||
Namespace: cfg.Namespace,
|
||
VIPChannel: cfg.VIPChannel,
|
||
}
|
||
}
|
||
|
||
func closeClientConn(conn *grpc.ClientConn) {
|
||
if conn != nil {
|
||
_ = conn.Close()
|
||
}
|
||
}
|