Add production hardening: monitoring, backups, caching, and high-traffic resilience
- Recovery middleware (no more crash-on-panic), request metrics/monitoring store + admin API, in-memory TTL caching for read-heavy option/question/ category lookups, gzip + rate-limiting middleware, sanitized chat message encoding, and pg_dump-based database backups with an admin API. - Fix N+1 queries in the discover feed, candidate detail, and match list endpoints by batching selection/photo/user/last-message lookups into single queries instead of one-per-item loops. - Tune DB connection pooling and add HTTP server timeouts + graceful shutdown so the API degrades gracefully instead of falling over under load. - Fix a null (vs empty array) JSON response for candidates/matches without photos or interests, which crashed the web app's swipe card.
This commit is contained in:
+55
-4
@@ -1,13 +1,22 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"dating-app/internal/config"
|
||||
"dating-app/internal/database"
|
||||
"dating-app/internal/handler"
|
||||
"dating-app/internal/middleware"
|
||||
"dating-app/internal/monitoring"
|
||||
"dating-app/internal/repository"
|
||||
"dating-app/internal/router"
|
||||
"dating-app/internal/service"
|
||||
@@ -65,6 +74,10 @@ func main() {
|
||||
profileService := service.NewProfileService(photoRepo, userRepo, photoStorage)
|
||||
discoveryService := service.NewDiscoveryService(userRepo, photoRepo, swipeRepo, matchRepo, optionRepo, questionRepo)
|
||||
chatService := service.NewChatService(matchRepo, messageRepo, userRepo, photoRepo)
|
||||
backupService := service.NewBackupService(cfg, "./backups")
|
||||
|
||||
// -------- مانیتورینگ --------
|
||||
metricsStore := monitoring.NewStore()
|
||||
|
||||
// -------- Handlers --------
|
||||
handlers := router.Handlers{
|
||||
@@ -74,16 +87,54 @@ func main() {
|
||||
Admin: handler.NewAdminHandler(adminService, optionRepo, questionRepo, photoRepo, onboardingService),
|
||||
Discovery: handler.NewDiscoveryHandler(discoveryService),
|
||||
Chat: handler.NewChatHandler(chatService),
|
||||
Monitoring: handler.NewMonitoringHandler(metricsStore),
|
||||
Backup: handler.NewBackupHandler(backupService),
|
||||
}
|
||||
|
||||
if cfg.AppEnv == "production" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
engine := gin.Default()
|
||||
// بهجای gin.Default() (که فقط Logger+Recovery پیشفرض رو میده)، دستی
|
||||
// میسازیم تا هم Metrics رو اضافه کنیم و هم Recovery خودمون که envelope
|
||||
// استاندارد JSON برمیگردونه. ترتیب مهمه: Metrics باید بیرونیترین لایه
|
||||
// باشه تا حتی درخواستهایی که پنیک میکنن (و توسط Recovery مهار میشن) هم
|
||||
// توی آمار لحاظ بشن.
|
||||
engine := gin.New()
|
||||
engine.Use(middleware.Metrics(metricsStore))
|
||||
engine.Use(middleware.Recovery())
|
||||
engine.Use(gin.Logger())
|
||||
router.Setup(engine, handlers, jwtManager, userRepo)
|
||||
|
||||
log.Printf("سرور روی پورت %s در حال اجراست (env=%s)", cfg.AppPort, cfg.AppEnv)
|
||||
if err := engine.Run(":" + cfg.AppPort); err != nil {
|
||||
log.Fatalf("اجرای سرور ناموفق بود: %v", err)
|
||||
// یک http.Server دستی بهجای engine.Run() ساده، به دو دلیل:
|
||||
// ۱. تایماوتهای صریح روی خوندن/نوشتن، تا یک کلاینت کند یا مخرب (slowloris)
|
||||
// نتونه goroutine/فایلدسکریپتورهای سرور رو زیر بار بالا قفل نگه داره.
|
||||
// ۲. Shutdown مهلتدار، تا موقع دیپلوی/ریاستارت، درخواستهای در حال پردازش
|
||||
// ناتمام قطع نشن (بهجای کشتن فوری پروسه).
|
||||
srv := &http.Server{
|
||||
Addr: ":" + cfg.AppPort,
|
||||
Handler: engine,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 90 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("سرور روی پورت %s در حال اجراست (env=%s)", cfg.AppPort, cfg.AppEnv)
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("اجرای سرور ناموفق بود: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Println("سیگنال خاموشی دریافت شد؛ منتظر تمامشدن درخواستهای در حال پردازش...")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Fatalf("خاموش کردن سرور با مشکل مواجه شد: %v", err)
|
||||
}
|
||||
log.Println("سرور با موفقیت خاموش شد")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user