2026-05-06 21:22:39 +08:00

141 lines
4.1 KiB
Go

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"
"hyapp/pkg/grpchealth"
"hyapp/pkg/logx"
"hyapp/services/activity-service/internal/client"
"hyapp/services/activity-service/internal/config"
activityservice "hyapp/services/activity-service/internal/service/activity"
messageservice "hyapp/services/activity-service/internal/service/message"
mysqlstorage "hyapp/services/activity-service/internal/storage/mysql"
grpcserver "hyapp/services/activity-service/internal/transport/grpc"
)
// App 装配 activity-service gRPC 入口和底座依赖。
type App struct {
server *grpc.Server
listener net.Listener
health *grpchealth.ServingChecker
mysqlRepo *mysqlstorage.Repository
userConn *grpc.ClientConn
message *messageservice.Service
cfg config.Config
workerCtx context.Context
workerCancel context.CancelFunc
workerWG sync.WaitGroup
closeOnce sync.Once
}
// New 初始化 activity-service 应用。
func New(cfg config.Config) (*App, error) {
startupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// activity-service 的活动状态、消费幂等和 outbox 都必须使用 MySQL。
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
}
server := grpc.NewServer(grpc.UnaryInterceptor(logx.UnaryServerInterceptor("activity-service")))
svc := activityservice.New(activityservice.Config{NodeID: cfg.NodeID}, repository)
messageSvc := messageservice.New(messageservice.Config{NodeID: cfg.NodeID}, repository, messageservice.WithTargetSource(client.NewGRPCUserTargetSource(userConn)))
activityv1.RegisterActivityServiceServer(server, grpcserver.NewServer(svc))
activityv1.RegisterMessageInboxServiceServer(server, grpcserver.NewMessageServer(messageSvc))
health := grpchealth.NewServingChecker("activity-service")
healthgrpc.RegisterHealthServer(server, grpchealth.NewServer(health))
workerCtx, workerCancel := context.WithCancel(context.Background())
return &App{
server: server,
listener: listener,
health: health,
mysqlRepo: repository,
userConn: userConn,
message: messageSvc,
cfg: cfg,
workerCtx: workerCtx,
workerCancel: workerCancel,
}, nil
}
// Run 启动 gRPC 服务。
func (a *App) Run() error {
a.health.MarkServing()
defer func() {
if a.workerCancel != nil {
a.workerCancel()
}
a.workerWG.Wait()
}()
if a.cfg.MessageInbox.FanoutWorker.Enabled {
a.workerWG.Add(1)
go func() {
defer a.workerWG.Done()
a.message.RunFanoutWorker(a.workerCtx, messageservice.FanoutWorkerOptions{
WorkerID: a.cfg.NodeID,
PollInterval: parseDurationOrZero(a.cfg.MessageInbox.FanoutWorker.PollInterval),
LockTTL: parseDurationOrZero(a.cfg.MessageInbox.FanoutWorker.LockTTL),
BatchSize: a.cfg.MessageInbox.FanoutWorker.BatchSize,
MaxRetry: a.cfg.MessageInbox.FanoutWorker.MaxRetry,
})
}()
}
err := a.server.Serve(a.listener)
a.health.MarkStopped()
if errors.Is(err, grpc.ErrServerStopped) {
return nil
}
return err
}
// Close 关闭 gRPC 服务。
func (a *App) Close() {
a.closeOnce.Do(func() {
a.health.MarkDraining()
if a.workerCancel != nil {
a.workerCancel()
}
a.workerWG.Wait()
a.server.GracefulStop()
if a.userConn != nil {
_ = a.userConn.Close()
}
if a.mysqlRepo != nil {
// MySQL 连接池最后关闭,保证 drain 中的查询或消费提交能完成。
_ = a.mysqlRepo.Close()
}
})
}
func parseDurationOrZero(raw string) time.Duration {
duration, err := time.ParseDuration(raw)
if err != nil {
return 0
}
return duration
}