package app import ( "context" "errors" "log/slog" "net" "sync" "time" "google.golang.org/grpc" robotv1 "hyapp.local/api/proto/robot/v1" "hyapp/pkg/grpchealth" "hyapp/pkg/healthhttp" "hyapp/pkg/logx" servicegrpc "hyapp/pkg/servicekit/grpcserver" servicehealth "hyapp/pkg/servicekit/health" "hyapp/services/robot-service/internal/config" "hyapp/services/robot-service/internal/service/gamerobot" "hyapp/services/robot-service/internal/service/roomrobot" mysqlstorage "hyapp/services/robot-service/internal/storage/mysql" grpcserver "hyapp/services/robot-service/internal/transport/grpc" ) // App 持有 robot-service 进程生命周期;首版只管理机器人账号池,不主动驱动游戏局或房间状态机。 type App struct { server *grpc.Server listener net.Listener health *grpchealth.ServingChecker healthHTTP *healthhttp.Server repo *mysqlstorage.Repository closeOnce sync.Once } func New(cfg config.Config) (*App, error) { startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() repo, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN) if err != nil { return nil, err } migration, err := repo.MigrateLegacyGameRobots(startupCtx, cfg.LegacyGameDatabase) if err != nil { _ = repo.Close() return nil, err } if migration.Applied { logx.Info(startupCtx, "robot_legacy_game_robot_migration_applied", slog.String("legacy_database", migration.LegacyDatabase), slog.Int64("migrated_rows", migration.MigratedRows), ) } else if cfg.LegacyGameDatabase != "" && !migration.AlreadyCompleted && !migration.SourceFound { logx.Warn(startupCtx, "robot_legacy_game_robot_source_missing", slog.String("legacy_database", migration.LegacyDatabase), ) } listener, err := net.Listen("tcp", cfg.GRPCAddr) if err != nil { _ = repo.Close() return nil, err } server := servicegrpc.New("robot-service") transport := grpcserver.NewServer(gamerobot.New(repo), roomrobot.New(repo)) robotv1.RegisterGameRobotServiceServer(server, transport) robotv1.RegisterRoomRobotServiceServer(server, transport) health, healthHTTP, err := servicehealth.New(server, servicehealth.Config{ ServiceName: "robot-service", HTTPAddr: cfg.HealthHTTPAddr, NodeID: cfg.NodeID, Dependencies: []grpchealth.Dependency{{ Name: "mysql", Check: repo.Ping, }}, }) if err != nil { _ = listener.Close() _ = repo.Close() return nil, err } return &App{server: server, listener: listener, health: health, healthHTTP: healthHTTP, repo: repo}, nil } func (a *App) Run() error { a.runHealthHTTP() a.health.MarkServing() err := a.server.Serve(a.listener) a.health.MarkStopped() if errors.Is(err, grpc.ErrServerStopped) { return nil } return err } func (a *App) Close() { a.closeOnce.Do(func() { a.health.MarkDraining() servicegrpc.GracefulStop(a.server, 15*time.Second) a.closeHealthHTTP() if a.repo != nil { _ = a.repo.Close() } }) } func (a *App) runHealthHTTP() { if a.healthHTTP == nil { return } go func() { if err := a.healthHTTP.Run(); err != nil && !errors.Is(err, context.Canceled) { logx.Error(context.Background(), "robot_health_http_failed", err) } }() } func (a *App) closeHealthHTTP() { if a.healthHTTP == nil { return } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := a.healthHTTP.Close(ctx); err != nil { logx.Error(context.Background(), "robot_health_http_shutdown_failed", err) } }