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:
2026-09-19 22:17:12 +03:30
parent 15583009e0
commit 769e76313f
29 changed files with 1377 additions and 72 deletions
+35 -6
View File
@@ -1,23 +1,40 @@
package repository
import (
"time"
"gorm.io/gorm"
"dating-app/internal/cache"
"dating-app/internal/models"
)
const activeQuestionsCacheTTL = 5 * time.Minute
const activeQuestionsCacheKey = "active"
type QuestionRepository struct {
db *gorm.DB
db *gorm.DB
activeCache *cache.TTLCache[string, []models.ProfileQuestion]
}
func NewQuestionRepository(db *gorm.DB) *QuestionRepository {
return &QuestionRepository{db: db}
return &QuestionRepository{
db: db,
activeCache: cache.New[string, []models.ProfileQuestion](activeQuestionsCacheTTL),
}
}
func (r *QuestionRepository) ListActive() ([]models.ProfileQuestion, error) {
if cached, ok := r.activeCache.Get(activeQuestionsCacheKey); ok {
return cached, nil
}
var qs []models.ProfileQuestion
err := r.db.Where("is_active = ?", true).Order("sort_order asc, id asc").Find(&qs).Error
return qs, err
if err != nil {
return nil, err
}
r.activeCache.Set(activeQuestionsCacheKey, qs)
return qs, nil
}
func (r *QuestionRepository) ListAllAdmin() ([]models.ProfileQuestion, error) {
@@ -42,15 +59,27 @@ func (r *QuestionRepository) FindActiveByIDs(ids []uint) ([]models.ProfileQuesti
}
func (r *QuestionRepository) Create(q *models.ProfileQuestion) error {
return r.db.Create(q).Error
if err := r.db.Create(q).Error; err != nil {
return err
}
r.activeCache.Clear()
return nil
}
func (r *QuestionRepository) Update(q *models.ProfileQuestion) error {
return r.db.Save(q).Error
if err := r.db.Save(q).Error; err != nil {
return err
}
r.activeCache.Clear()
return nil
}
func (r *QuestionRepository) Delete(id uint) error {
return r.db.Delete(&models.ProfileQuestion{}, id).Error
if err := r.db.Delete(&models.ProfileQuestion{}, id).Error; err != nil {
return err
}
r.activeCache.Clear()
return nil
}
// ReplaceUserAnswers پاسخ‌های قبلی کاربر رو با پاسخ‌های جدید جایگزین می‌کنه (تراکنشی)