2026-05-13 16:07:25 +08:00

174 lines
4.7 KiB
Go

// Package app wires notice-service modules, health endpoints and background workers.
package app
import (
"context"
"errors"
"net"
"sync"
"time"
"google.golang.org/grpc"
healthgrpc "google.golang.org/grpc/health/grpc_health_v1"
"hyapp/pkg/grpchealth"
"hyapp/pkg/grpcshutdown"
"hyapp/pkg/healthhttp"
"hyapp/pkg/logx"
"hyapp/pkg/tencentim"
"hyapp/services/notice-service/internal/config"
"hyapp/services/notice-service/internal/modules/walletnotice"
mysqlplatform "hyapp/services/notice-service/internal/platform/mysql"
)
// App owns the notice-service process lifecycle.
type App struct {
server *grpc.Server
listener net.Listener
health *grpchealth.ServingChecker
healthHTTP *healthhttp.Server
store *mysqlplatform.Store
walletNoticeService *walletnotice.Service
walletWorkerOptions walletnotice.WalletNoticeWorkerOptions
walletWorkerEnabled bool
workerCtx context.Context
workerCancel context.CancelFunc
workerWG sync.WaitGroup
closeOnce sync.Once
}
// New initializes MySQL, module repositories and optional Tencent IM publisher.
func New(cfg config.Config) (*App, error) {
startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
store, err := mysqlplatform.Open(startupCtx, cfg.MySQLDSN)
if err != nil {
return nil, err
}
listener, err := net.Listen("tcp", cfg.GRPCAddr)
if err != nil {
_ = store.Close()
return nil, err
}
walletRepo, err := walletnotice.NewMySQLRepository(store.DB, cfg.WalletDatabase)
if err != nil {
_ = listener.Close()
_ = store.Close()
return nil, err
}
var publisher walletnotice.UserMessagePublisher
if cfg.TencentIM.Enabled {
tencentClient, err := tencentim.NewRESTClient(cfg.TencentIM.RESTConfig())
if err != nil {
_ = listener.Close()
_ = store.Close()
return nil, err
}
publisher = tencentClient
}
if cfg.WalletNoticeWorker.Enabled && publisher == nil {
_ = listener.Close()
_ = store.Close()
return nil, errors.New("wallet notice worker requires tencent_im.enabled")
}
server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("notice-service")))
health := grpchealth.NewServingChecker("notice-service", grpchealth.Dependency{
Name: "mysql",
Check: store.Ping,
})
healthgrpc.RegisterHealthServer(server, grpchealth.NewServer(health))
healthHTTP, err := healthhttp.New(cfg.HealthHTTPAddr, cfg.NodeID, health)
if err != nil {
_ = listener.Close()
_ = store.Close()
return nil, err
}
workerCtx, workerCancel := context.WithCancel(context.Background())
return &App{
server: server,
listener: listener,
health: health,
healthHTTP: healthHTTP,
store: store,
walletNoticeService: walletnotice.New(walletnotice.Config{NodeID: cfg.NodeID}, walletRepo, publisher),
walletWorkerOptions: walletNoticeWorkerOptions(cfg.WalletNoticeWorker),
walletWorkerEnabled: cfg.WalletNoticeWorker.Enabled,
workerCtx: workerCtx,
workerCancel: workerCancel,
}, nil
}
// Run starts health serving, optional workers and the gRPC health endpoint.
func (a *App) Run() error {
a.runHealthHTTP()
a.health.MarkServing()
defer func() {
if a.workerCancel != nil {
a.workerCancel()
}
a.workerWG.Wait()
a.health.MarkStopped()
}()
if a.walletWorkerEnabled && a.walletNoticeService != nil {
a.workerWG.Go(func() {
a.walletNoticeService.RunWalletBalanceWorker(a.workerCtx, a.walletWorkerOptions)
})
}
err := a.server.Serve(a.listener)
if errors.Is(err, grpc.ErrServerStopped) {
return nil
}
return err
}
// Close drains workers 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.store != nil {
_ = a.store.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)
}
func walletNoticeWorkerOptions(cfg config.WalletNoticeWorkerConfig) walletnotice.WalletNoticeWorkerOptions {
return walletnotice.WalletNoticeWorkerOptions{
PollInterval: cfg.PollInterval,
BatchSize: cfg.BatchSize,
LockTTL: cfg.LockTTL,
PublishTimeout: cfg.PublishTimeout,
MaxRetryCount: cfg.MaxRetryCount,
InitialBackoff: cfg.InitialBackoff,
MaxBackoff: cfg.MaxBackoff,
}
}