diff --git a/shoppingbasketapp/app.go b/shoppingbasketapp/app.go index 8505883b..0f0258e8 100644 --- a/shoppingbasketapp/app.go +++ b/shoppingbasketapp/app.go @@ -1 +1,132 @@ package shoppingbasketapp + +import ( + "context" + "fmt" + "git.gocasts.ir/ebhomengo/niki/adapter/redis" + logger "git.gocasts.ir/ebhomengo/niki/logger" + "git.gocasts.ir/ebhomengo/niki/pkg/httpserver" + "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/delivery/http" + "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository" + "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/service/cart" + "log/slog" + "os" + "os/signal" + "sync" + "syscall" +) + +type Application struct { + Repo repository.Repo + Service cart.Service + Handler http.Handler + Server http.Server + Config Config + Logger *slog.Logger +} + +func (app Application) Setup(cfg Config) (Application, error) { + l := logger.New(cfg.Logger, &slog.HandlerOptions{ + Level: slog.LevelDebug, + }) + + adapter := redis.New(cfg.Redis) + repo := repository.New(adapter.Client(), cfg.Repo) + + validator := cart.NewValidate() + svc := cart.New(validator, l, repo) + + handler := http.NewHandler(svc) + + httpServer, err := httpserver.New(cfg.HTTPServer) + if err != nil { + l.Error("failed to initialize http server", "error", err) + return Application{}, err + } + server := http.NewServer(handler, httpServer) + + return Application{ + Repo: repo, + Service: svc, + Handler: handler, + Server: server, + Config: cfg, + Logger: l, + }, nil +} + +func (app Application) Start() { + var wg sync.WaitGroup + + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + + startServers(app, &wg) + <-ctx.Done() + + app.Logger.Info("Shutdown signal received...") + + shutdownTimeoutCtx, cancel := context.WithTimeout(context.Background(), app.Config.HTTPServer.ShutdownTimeout) + defer cancel() + + if app.shutdownServers(shutdownTimeoutCtx) { + app.Logger.Info("Servers shutdown gracefully") + } else { + app.Logger.Warn("Shutdown timed out, exiting application") + return + } + + wg.Wait() + app.Logger.Info("shopping-basket-app stopped") +} + +func startServers(app Application, wg *sync.WaitGroup) { + wg.Add(1) + go func() { + defer wg.Wait() + app.Logger.Info(fmt.Sprintf("HTTP server starting on port: %d", app.Config.HTTPServer.Port)) + if err := app.Server.Serve(); err != nil { + app.Logger.Error(fmt.Sprintf("error listen and serve HTTP server on port %d", app.Config.HTTPServer.Port)) + } + + app.Logger.Info(fmt.Sprintf("HTTP server stopped on port %d", app.Config.HTTPServer.Port)) + }() +} + +func (app Application) shutdownServers(ctx context.Context) bool { + app.Logger.Info("Starting server shutdown process...") + + shutdownDone := make(chan struct{}) + + go func() { + var shutdownWg sync.WaitGroup + shutdownWg.Add(1) + go app.shutdownHTTPServe(ctx, &shutdownWg) + + shutdownWg.Wait() + close(shutdownDone) + app.Logger.Info("All servers have been shut down successfully.") + + }() + + select { + case <-shutdownDone: + return true + case <-ctx.Done(): + return false + } +} + +func (app Application) shutdownHTTPServe(parentCtx context.Context, wg *sync.WaitGroup) { + app.Logger.Info(fmt.Sprintf("Starting gracefully shutdown for http server on port %d", app.Config.HTTPServer.Port)) + + defer wg.Done() + httpShutdownCtx, httpCancel := context.WithTimeout(parentCtx, app.Config.HTTPServer.ShutdownTimeout) + defer httpCancel() + + if err := app.Server.Stop(httpShutdownCtx); err != nil { + app.Logger.Error(fmt.Sprintf("failed http server gracefully shutdown: %v", err)) + } + + app.Logger.Info("Successfully http server gracefully shutdown") +} diff --git a/shoppingbasketapp/config.go b/shoppingbasketapp/config.go index c87b97df..39a0e4af 100644 --- a/shoppingbasketapp/config.go +++ b/shoppingbasketapp/config.go @@ -2,12 +2,14 @@ package shoppingbasketapp import ( "git.gocasts.ir/ebhomengo/niki/adapter/redis" + "git.gocasts.ir/ebhomengo/niki/logger" "git.gocasts.ir/ebhomengo/niki/pkg/httpserver" "git.gocasts.ir/ebhomengo/niki/shoppingbasketapp/repository" ) type Config struct { Redis redis.Config `koanf:"redis" json:"redis"` - Repo repository.Repo `koanf:"repo" json:"repo"` + Repo repository.Config `koanf:"repo" json:"repo"` HTTPServer httpserver.Config `koanf:"http_server" json:"http_server"` + Logger logger.Config `koanf:"logger" json:"logger"` } diff --git a/shoppingbasketapp/service/cart/service.go b/shoppingbasketapp/service/cart/service.go index df860b0b..d84a3f73 100644 --- a/shoppingbasketapp/service/cart/service.go +++ b/shoppingbasketapp/service/cart/service.go @@ -4,6 +4,7 @@ import ( "context" richerror "git.gocasts.ir/ebhomengo/niki/pkg/rich_error" "git.gocasts.ir/ebhomengo/niki/types" + "log/slog" "time" ) @@ -17,17 +18,19 @@ type Repository interface { type Service struct { validate Validate + logger *slog.Logger repo Repository } -func New(val Validate, repo Repository) Service { - return Service{validate: val, repo: repo} +func New(val Validate, logger *slog.Logger, repo Repository) Service { + return Service{validate: val, logger: logger, repo: repo} } func (s Service) AddToBasket(ctx context.Context, req AddToCartRequest) error { const op = "shoppingbasketapp.service.AddToBasket" if err := s.validate.ValidateAddToCart(req); err != nil { + s.logger.Error("shoppingbasket-service-AddToBasket", "error", err) return err } @@ -43,11 +46,13 @@ func (s Service) AddToBasket(ctx context.Context, req AddToCartRequest) error { func (s Service) GetCart(ctx context.Context, userID types.ID) (GetCartResponse, error) { const op = "shoppingbasketapp.service.GetCart" if userID < 1 { + s.logger.Error("shoppingbasket-service-GetCart", "error", "user id must be greater than 1") return GetCartResponse{}, richerror.New(op).WithKind(richerror.KindInvalid).WithMessage("invalid user id") } res, err := s.repo.GetCart(ctx, userID) if err != nil { + s.logger.Error("shoppingbasket-service-GetCart", "error", err) return GetCartResponse{}, richerror.New(op).WithErr(err) } @@ -64,6 +69,7 @@ func (s Service) RemoveFromCart(ctx context.Context, req RemoveFromCartRequest) const op = "shoppingbaskerapp.service.RemoveFromCart" if err := s.validate.ValidateRemoveFromCart(req); err != nil { + s.logger.Error("shoppingbasket-service-RemoveFromCart", "error", err) return err } @@ -74,6 +80,7 @@ func (s Service) UpdateQuantity(ctx context.Context, req UpdateQuantityRequest) const op = "shoppingbaskerapp.service.UpdateQuantity" if err := s.validate.ValidateUpdateQuantity(req); err != nil { + s.logger.Error("shoppingbasket-service-UpdateQuantity", "error", err) return err } @@ -88,6 +95,7 @@ func (s Service) ClearCart(ctx context.Context, userID types.ID) error { const op = "shoppingbaskerapp.service.ClearCart" if userID < 1 { + s.logger.Error("shoppingbasket-service-ClearCart", "error", "user id must be greater than 1") return richerror.New(op).WithKind(richerror.KindInvalid). WithMessage("invalid user id") }