220 lines
6.7 KiB
Go
220 lines
6.7 KiB
Go
// Package app wires cron-service configuration, scheduler, storage and gRPC health.
|
||
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"
|
||
activityv1 "hyapp.local/api/proto/activity/v1"
|
||
gamev1 "hyapp.local/api/proto/game/v1"
|
||
userv1 "hyapp.local/api/proto/user/v1"
|
||
walletv1 "hyapp.local/api/proto/wallet/v1"
|
||
"hyapp/pkg/grpchealth"
|
||
"hyapp/pkg/grpcshutdown"
|
||
"hyapp/pkg/healthhttp"
|
||
"hyapp/pkg/logx"
|
||
"hyapp/services/cron-service/internal/config"
|
||
"hyapp/services/cron-service/internal/integration"
|
||
"hyapp/services/cron-service/internal/scheduler"
|
||
mysqlstorage "hyapp/services/cron-service/internal/storage/mysql"
|
||
)
|
||
|
||
// App owns the cron-service process lifecycle.
|
||
type App struct {
|
||
server *grpc.Server
|
||
listener net.Listener
|
||
health *grpchealth.ServingChecker
|
||
healthHTTP *healthhttp.Server
|
||
repository *mysqlstorage.Repository
|
||
scheduler *scheduler.Scheduler
|
||
userConn *grpc.ClientConn
|
||
activityConn *grpc.ClientConn
|
||
gameConn *grpc.ClientConn
|
||
walletConn *grpc.ClientConn
|
||
workerCtx context.Context
|
||
workerCancel context.CancelFunc
|
||
workerWG sync.WaitGroup
|
||
closeOnce sync.Once
|
||
}
|
||
|
||
// New initializes cron metadata storage, health service and scheduler.
|
||
func New(cfg config.Config) (*App, error) {
|
||
startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
|
||
// cron-service owns only scheduler metadata. Business data stays in owner service databases.
|
||
repository, err := mysqlstorage.Open(startupCtx, cfg.MySQLDSN)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
listener, err := net.Listen("tcp", cfg.GRPCAddr)
|
||
if err != nil {
|
||
_ = repository.Close()
|
||
return nil, err
|
||
}
|
||
userConn, err := grpc.Dial(cfg.UserServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||
if err != nil {
|
||
_ = listener.Close()
|
||
_ = repository.Close()
|
||
return nil, err
|
||
}
|
||
activityConn, err := grpc.Dial(cfg.ActivityServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||
if err != nil {
|
||
_ = userConn.Close()
|
||
_ = listener.Close()
|
||
_ = repository.Close()
|
||
return nil, err
|
||
}
|
||
gameConn, err := grpc.Dial(cfg.GameServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||
if err != nil {
|
||
_ = activityConn.Close()
|
||
_ = userConn.Close()
|
||
_ = listener.Close()
|
||
_ = repository.Close()
|
||
return nil, err
|
||
}
|
||
walletConn, err := grpc.Dial(cfg.WalletServiceAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||
if err != nil {
|
||
_ = gameConn.Close()
|
||
_ = activityConn.Close()
|
||
_ = userConn.Close()
|
||
_ = listener.Close()
|
||
_ = repository.Close()
|
||
return nil, err
|
||
}
|
||
|
||
server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("cron-service")))
|
||
health := grpchealth.NewServingChecker("cron-service", grpchealth.Dependency{
|
||
Name: "mysql",
|
||
Check: repository.Ping,
|
||
})
|
||
healthgrpc.RegisterHealthServer(server, grpchealth.NewServer(health))
|
||
healthHTTP, err := healthhttp.New(cfg.HealthHTTPAddr, cfg.NodeID, health)
|
||
if err != nil {
|
||
_ = walletConn.Close()
|
||
_ = gameConn.Close()
|
||
_ = activityConn.Close()
|
||
_ = userConn.Close()
|
||
_ = listener.Close()
|
||
_ = repository.Close()
|
||
return nil, err
|
||
}
|
||
|
||
workerCtx, workerCancel := context.WithCancel(context.Background())
|
||
userCron := integration.NewUserCronClient(userv1.NewUserCronServiceClient(userConn))
|
||
activityCron := integration.NewActivityCronClient(activityv1.NewActivityCronServiceClient(activityConn))
|
||
gameCron := integration.NewGameCronClient(gamev1.NewGameCronServiceClient(gameConn))
|
||
walletCron := integration.NewWalletCronClient(walletv1.NewWalletCronServiceClient(walletConn))
|
||
taskScheduler := scheduler.New(cfg.NodeID, cfg.Tasks, repository, map[string]scheduler.Handler{
|
||
"login_ip_risk": userCron.ProcessLoginIPRiskBatch,
|
||
"user_region_rebuild": userCron.ProcessRegionRebuildBatch,
|
||
"mic_open_session_compensation": userCron.CompensateMicOpenSessions,
|
||
"manager_user_block_expiry": userCron.ExpireManagerUserBlocks,
|
||
// CP 榜单任务只占 cron 租约并调用 user-service;真实 MySQL 读取、wallet 头像框补齐和 Redis 替换都在 user-service 内完成。
|
||
"cp_intimacy_leaderboard": userCron.RefreshCPIntimacyLeaderboard,
|
||
"message_fanout": activityCron.ProcessMessageFanoutBatch,
|
||
"growth_level_reward": activityCron.ProcessLevelRewardBatch,
|
||
"achievement_reward": activityCron.ProcessAchievementRewardBatch,
|
||
"weekly_star_settlement": activityCron.ProcessWeeklyStarSettlementBatch,
|
||
"room_turnover_reward_settlement": activityCron.ProcessRoomTurnoverRewardSettlementBatch,
|
||
"game_level_event_relay": gameCron.ProcessLevelEventOutboxBatch,
|
||
"host_salary_daily_settlement": walletCron.ProcessHostSalaryDailySettlementBatch,
|
||
"host_salary_half_month_settlement": walletCron.ProcessHostSalaryHalfMonthSettlementBatch,
|
||
"host_salary_month_end": walletCron.ProcessHostSalaryMonthEndBatch,
|
||
})
|
||
|
||
return &App{
|
||
server: server,
|
||
listener: listener,
|
||
health: health,
|
||
healthHTTP: healthHTTP,
|
||
repository: repository,
|
||
scheduler: taskScheduler,
|
||
userConn: userConn,
|
||
activityConn: activityConn,
|
||
gameConn: gameConn,
|
||
walletConn: walletConn,
|
||
workerCtx: workerCtx,
|
||
workerCancel: workerCancel,
|
||
}, nil
|
||
}
|
||
|
||
// Run starts scheduler loops and gRPC health serving.
|
||
func (a *App) Run() error {
|
||
a.runHealthHTTP()
|
||
a.health.MarkServing()
|
||
defer func() {
|
||
if a.workerCancel != nil {
|
||
a.workerCancel()
|
||
}
|
||
a.workerWG.Wait()
|
||
}()
|
||
if a.scheduler != nil {
|
||
a.workerWG.Go(func() {
|
||
a.scheduler.Run(a.workerCtx)
|
||
})
|
||
}
|
||
|
||
err := a.server.Serve(a.listener)
|
||
a.health.MarkStopped()
|
||
if errors.Is(err, grpc.ErrServerStopped) {
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
|
||
// Close drains scheduler loops before closing gRPC and MySQL resources.
|
||
func (a *App) Close() {
|
||
a.closeOnce.Do(func() {
|
||
a.health.MarkDraining()
|
||
if a.workerCancel != nil {
|
||
a.workerCancel()
|
||
}
|
||
a.workerWG.Wait()
|
||
grpcshutdown.GracefulStop(a.server, 15*time.Second)
|
||
a.closeHealthHTTP()
|
||
if a.repository != nil {
|
||
_ = a.repository.Close()
|
||
}
|
||
if a.userConn != nil {
|
||
_ = a.userConn.Close()
|
||
}
|
||
if a.activityConn != nil {
|
||
_ = a.activityConn.Close()
|
||
}
|
||
if a.gameConn != nil {
|
||
_ = a.gameConn.Close()
|
||
}
|
||
if a.walletConn != nil {
|
||
_ = a.walletConn.Close()
|
||
}
|
||
})
|
||
}
|
||
|
||
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)
|
||
}
|