package app import ( "context" "errors" "net" "sync" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" luckygiftv1 "hyapp.local/api/proto/luckygift/v1" walletv1 "hyapp.local/api/proto/wallet/v1" "hyapp/pkg/grpchealth" "hyapp/pkg/healthhttp" "hyapp/pkg/logx" serviceapp "hyapp/pkg/servicekit/app" servicegrpc "hyapp/pkg/servicekit/grpcserver" servicehealth "hyapp/pkg/servicekit/health" "hyapp/services/lucky-gift-service/internal/config" luckygiftservice "hyapp/services/lucky-gift-service/internal/service/luckygift" mysqlstorage "hyapp/services/lucky-gift-service/internal/storage/mysql" grpcserver "hyapp/services/lucky-gift-service/internal/transport/grpc" ) // App 装配 lucky-gift-service 的 owner 进程。 // 该进程是幸运礼物规则、抽奖、外部 App 接入和 outbox 的唯一写入者;其他服务只持有 gRPC client。 type App struct { cfg config.Config server *grpc.Server listener net.Listener health *grpchealth.ServingChecker healthHTTP *healthhttp.Server repository *mysqlstorage.Repository walletConn *grpc.ClientConn service *luckygiftservice.Service workers *serviceapp.BackgroundGroup closeOnce sync.Once } func New(cfg config.Config) (*App, error) { startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() repository, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN) if err != nil { return nil, err } if cfg.MySQLAutoMigrate { if err := repository.Migrate(startupCtx); err != nil { _ = repository.Close() return nil, err } } walletConn, err := grpc.Dial(cfg.WalletServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { _ = repository.Close() return nil, err } listener, err := net.Listen("tcp", cfg.GRPCAddr) if err != nil { _ = walletConn.Close() _ = repository.Close() return nil, err } server := servicegrpc.New("lucky-gift-service") svc := luckygiftservice.New(repository, luckygiftservice.WithWallet(walletv1.NewWalletServiceClient(walletConn))) luckygiftv1.RegisterLuckyGiftServiceServer(server, grpcserver.NewLuckyGiftServer(svc)) luckygiftv1.RegisterAdminLuckyGiftServiceServer(server, grpcserver.NewAdminLuckyGiftServer(svc)) health, healthHTTP, err := servicehealth.New(server, servicehealth.Config{ ServiceName: "lucky-gift-service", HTTPAddr: cfg.HealthHTTPAddr, NodeID: cfg.NodeID, Dependencies: []grpchealth.Dependency{{ Name: "mysql", Check: repository.Ping, }}, }) if err != nil { _ = listener.Close() _ = walletConn.Close() _ = repository.Close() return nil, err } return &App{ cfg: cfg, server: server, listener: listener, health: health, healthHTTP: healthHTTP, repository: repository, walletConn: walletConn, service: svc, workers: serviceapp.NewBackground(context.Background()), }, nil } func (a *App) Run() error { if a.healthHTTP != nil { go func() { if err := a.healthHTTP.Run(); err != nil { logx.Error(context.Background(), "lucky_gift_health_http_run_failed", err) } }() } a.health.MarkServing() if a.cfg.LuckyGiftWorker.Enabled && a.service != nil { options := luckyGiftWorkerOptions(a.cfg.NodeID, a.cfg.LuckyGiftWorker) a.workers.Go(func(ctx context.Context) { a.service.RunWorker(ctx, options) }) } err := a.server.Serve(a.listener) if a.workers != nil { a.workers.StopAndWait() } a.health.MarkStopped() if errors.Is(err, grpc.ErrServerStopped) { return nil } return err } func (a *App) Close() { a.closeOnce.Do(func() { if a.health != nil { a.health.MarkDraining() } if a.workers != nil { a.workers.StopAndWait() } servicegrpc.GracefulStop(a.server, 15*time.Second) if a.healthHTTP != nil { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() _ = a.healthHTTP.Close(ctx) } if a.walletConn != nil { _ = a.walletConn.Close() } if a.repository != nil { _ = a.repository.Close() } }) } func luckyGiftWorkerOptions(nodeID string, cfg config.LuckyGiftWorkerConfig) luckygiftservice.WorkerOptions { return luckygiftservice.WorkerOptions{ WorkerID: nodeID, PollInterval: cfg.WorkerPollInterval, BatchSize: cfg.WorkerBatchSize, Concurrency: cfg.WorkerConcurrency, LockTTL: cfg.WorkerLockTTL, MaxRetry: cfg.WorkerMaxRetry, PublishTimeout: cfg.PublishTimeout, StatsInterval: cfg.StatsRefreshInterval, StatsBatchSize: cfg.StatsBatchSize, } }