// Package app 负责把 room-service 的领域服务、存储、路由目录和 gRPC 入口装配成进程。 package app import ( "context" "errors" "net" "sync" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" healthgrpc "google.golang.org/grpc/health/grpc_health_v1" roomv1 "hyapp/api/proto/room/v1" "hyapp/pkg/grpchealth" "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 // listener 是 room-service 的唯一网络入口。 listener net.Listener // walletConn 是 SendGift 同步扣费依赖。 walletConn *grpc.ClientConn // imConn 是房间系统事件低时延同步广播依赖。 imConn *grpc.ClientConn // repository 是 MySQL 持久化实现,保存 meta、command log、snapshot 和 outbox。 repository *mysqlstorage.Repository // redisClose 关闭 Redis client,Redis directory 本身不拥有额外生命周期。 redisClose func() error // health 汇总 gRPC、runtime、MySQL 和 Redis lease 探测状态。 health *healthcheck.State // closeOnce 防止信号退出和 Serve 错误同时触发重复关闭。 closeOnce sync.Once } // New 初始化 room-service 应用。 func New(cfg config.Config) (*App, error) { // wallet-service 是 SendGift 主链路依赖,连接对象创建失败时不能启动。 walletConn, err := grpc.Dial(cfg.WalletServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { return nil, err } // im-service 失败不影响已提交状态的补偿语义,但同步广播客户端装配失败仍不应启动。 imConn, err := grpc.Dial(cfg.IMServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { _ = walletConn.Close() return nil, err } 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 { _ = walletConn.Close() _ = imConn.Close() return nil, err } if cfg.MySQLAutoMigrate { // 本地和测试环境可自动建表,线上应通过显式迁移控制结构变更。 if err := repository.Migrate(startupCtx); err != nil { _ = repository.Close() _ = walletConn.Close() _ = imConn.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() _ = walletConn.Close() _ = imConn.Close() return nil, err } directory := router.NewRedisDirectory(redisClient) // 领域服务只依赖接口,App 层负责选择 MySQL/Redis/gRPC 具体实现。 svc := roomservice.New(roomservice.Config{ NodeID: cfg.NodeID, LeaseTTL: cfg.LeaseTTL, RankLimit: cfg.RankLimit, SnapshotEveryN: cfg.SnapshotEveryN, }, directory, repository, integration.NewGRPCWalletClient(walletConn), integration.NewGRPCRoomEventPublisher(imConn), integration.NewNoopOutboxPublisher()) server := grpc.NewServer() // 命令服务处理房间状态变更,守卫服务给 im-service 做发言和订阅校验。 roomv1.RegisterRoomCommandServiceServer(server, grpcserver.NewServer(svc)) roomv1.RegisterRoomGuardServiceServer(server, grpcserver.NewServer(svc)) healthState := healthcheck.NewState(cfg.NodeID, svc, repository, directory) healthgrpc.RegisterHealthServer(server, grpchealth.NewServer(healthState)) // listener 在 New 阶段创建,可以把端口占用作为启动失败暴露。 listener, err := net.Listen("tcp", cfg.GRPCAddr) if err != nil { _ = walletConn.Close() _ = imConn.Close() _ = repository.Close() _ = redisClient.Close() return nil, err } return &App{ cfg: cfg, grpcServer: server, listener: listener, walletConn: walletConn, imConn: imConn, repository: repository, redisClose: redisClient.Close, health: healthState, }, nil } // Run 启动 gRPC 服务。 func (a *App) Run() error { // health 先标记 serving,再进入 Serve,ready 才能在监听开始后通过。 a.health.MarkGRPCServing() 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() // GracefulStop 让已进入的 gRPC 请求自然结束。 a.grpcServer.GracefulStop() if a.walletConn != nil { _ = a.walletConn.Close() } if a.imConn != nil { _ = a.imConn.Close() } if a.repository != nil { // MySQL 连接池最后释放,保证 GracefulStop 期间仍可完成持久化。 _ = a.repository.Close() } if a.redisClose != nil { // Redis client 关闭后本节点不再续约或接管房间。 _ = a.redisClose() } }) }