2026-06-23 11:53:00 +08:00

129 lines
3.4 KiB
Go

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)
}
}