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

216 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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"
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/healthhttp"
"hyapp/pkg/logx"
serviceapp "hyapp/pkg/servicekit/app"
servicegrpc "hyapp/pkg/servicekit/grpcserver"
servicehealth "hyapp/pkg/servicekit/health"
"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
workers *serviceapp.BackgroundGroup
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 := servicegrpc.New("cron-service")
health, healthHTTP, err := servicehealth.New(server, servicehealth.Config{
ServiceName: "cron-service",
HTTPAddr: cfg.HealthHTTPAddr,
NodeID: cfg.NodeID,
Dependencies: []grpchealth.Dependency{{
Name: "mysql",
Check: repository.Ping,
}},
})
if err != nil {
_ = walletConn.Close()
_ = gameConn.Close()
_ = activityConn.Close()
_ = userConn.Close()
_ = listener.Close()
_ = repository.Close()
return nil, err
}
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,
"agency_opening_settlement": activityCron.ProcessAgencyOpeningSettlementBatch,
"cp_weekly_rank_settlement": activityCron.ProcessCPWeeklyRankSettlementBatch,
"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,
workers: serviceapp.NewBackground(context.Background()),
}, nil
}
// Run starts scheduler loops and gRPC health serving.
func (a *App) Run() error {
a.runHealthHTTP()
a.health.MarkServing()
defer func() {
a.workers.StopAndWait()
}()
if a.scheduler != nil {
a.workers.Go(func(ctx context.Context) {
a.scheduler.Run(ctx)
})
}
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()
a.workers.StopAndWait()
servicegrpc.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)
}