From f3a835caa2a3bcf39e059536ab370e6fbfe1f4c2 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Tue, 3 Mar 2026 12:46:18 +0100 Subject: [PATCH] feat: fully integrate DragonflyDB with application - Add Redis client initialization with DragonflyDB connection - Update session middleware to use DragonflyDB with fallback to memory - Update cache middleware to use DragonflyDB for persistent caching - Add proper error handling and connection timeouts - Implement session storage in DragonflyDB with 24-hour expiration - Add cache invalidation middleware for DragonflyDB - Maintain backward compatibility with in-memory fallbacks --- backend/main.go | 62 +++++++++++++++++++++-- backend/middleware/session.go | 94 ++++++++++++++++++++++++++++++++--- 2 files changed, 147 insertions(+), 9 deletions(-) diff --git a/backend/main.go b/backend/main.go index 94dc092..e36c955 100644 --- a/backend/main.go +++ b/backend/main.go @@ -7,8 +7,10 @@ import ( "os" "os/signal" "syscall" + "time" "github.com/gin-gonic/gin" + "github.com/go-redis/redis/v8" "github.com/joho/godotenv" "github.com/trackeep/backend/config" "github.com/trackeep/backend/handlers" @@ -49,6 +51,40 @@ func initializeSecuritySecrets() error { return nil } +// initializeDragonflyDB initializes DragonflyDB (Redis-compatible) connection +func initializeDragonflyDB() *redis.Client { + dragonflyAddr := os.Getenv("DRAGONFLY_ADDR") + dragonflyPassword := os.Getenv("DRAGONFLY_PASSWORD") + + if dragonflyAddr == "" { + log.Println("DRAGONFLY_ADDR not set, using default: localhost:6379") + dragonflyAddr = "localhost:6379" + } + + rdb := redis.NewClient(&redis.Options{ + Addr: dragonflyAddr, + Password: dragonflyPassword, + DialTimeout: 5 * time.Second, + ReadTimeout: 3 * time.Second, + WriteTimeout: 3 * time.Second, + PoolSize: 20, + }) + + // Test connection + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := rdb.Ping(ctx).Result() + if err != nil { + log.Printf("Warning: Failed to connect to DragonflyDB at %s: %v", dragonflyAddr, err) + log.Println("Falling back to in-memory cache and sessions") + return nil + } + + log.Printf("Successfully connected to DragonflyDB at %s", dragonflyAddr) + return rdb +} + func main() { os.Setenv("APP_VERSION", "1.0.0") @@ -85,10 +121,28 @@ func main() { log.Fatal("Failed to initialize security secrets:", err) } - // Initialize session store - middleware.InitSessionStore() + // Initialize DragonflyDB + dragonflyClient := initializeDragonflyDB() + + // Initialize session store with DragonflyDB + middleware.InitSessionStore(dragonflyClient) log.Println("Session store initialized successfully") + // Initialize cache middleware with DragonflyDB + var cacheConfig middleware.CacheConfig + if dragonflyClient != nil { + cacheConfig = middleware.CacheConfig{ + Duration: 5 * time.Minute, + KeyPrefix: "trackeep:", + Enabled: true, + RedisClient: dragonflyClient, + } + log.Println("DragonflyDB cache middleware initialized") + } else { + cacheConfig = middleware.DefaultCacheConfig() + log.Println("Using in-memory cache fallback") + } + // Seed demo data in background // go func() { // SeedData() @@ -105,7 +159,9 @@ func main() { // Middleware r.Use(gin.Logger()) r.Use(gin.Recovery()) - r.Use(middleware.SessionMiddleware()) // Add session middleware + r.Use(middleware.CacheMiddleware(cacheConfig)) // Add DragonflyDB cache middleware + r.Use(middleware.CacheInvalidationMiddleware(dragonflyClient)) // Add cache invalidation + r.Use(middleware.SessionMiddleware()) // Add session middleware r.Use(middleware.AuditMiddleware()) r.Use(middleware.InputValidationMiddleware()) diff --git a/backend/middleware/session.go b/backend/middleware/session.go index 729d127..aaa6a82 100644 --- a/backend/middleware/session.go +++ b/backend/middleware/session.go @@ -1,12 +1,15 @@ package middleware import ( + "context" + "encoding/json" "fmt" "os" "strings" "time" "github.com/gin-gonic/gin" + "github.com/go-redis/redis/v8" "github.com/trackeep/backend/models" ) @@ -34,13 +37,15 @@ type SessionStore interface { // RedisSessionStore implements SessionStore using Redis (or fallback to memory) type RedisSessionStore struct { - sessions map[string]*SessionData // Fallback in-memory store + redisClient *redis.Client + sessions map[string]*SessionData // Fallback in-memory store } // NewSessionStore creates a new session store -func NewSessionStore() SessionStore { +func NewSessionStore(redisClient *redis.Client) SessionStore { return &RedisSessionStore{ - sessions: make(map[string]*SessionData), + redisClient: redisClient, + sessions: make(map[string]*SessionData), } } @@ -48,32 +53,109 @@ func NewSessionStore() SessionStore { func (r *RedisSessionStore) CreateSession(sessionData *SessionData) error { sessionData.CreatedAt = time.Now() sessionData.LastActive = time.Now() + + // Try Redis first + if r.redisClient != nil { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + sessionJSON, err := json.Marshal(sessionData) + if err != nil { + return fmt.Errorf("failed to marshal session data: %w", err) + } + + // Store in Redis with 24 hour expiration + err = r.redisClient.Set(ctx, "session:"+sessionData.SessionID, sessionJSON, 24*time.Hour).Err() + if err == nil { + return nil + } + // Fall back to memory if Redis fails + } + + // Fallback to in-memory storage r.sessions[sessionData.SessionID] = sessionData return nil } // GetSession retrieves a session by ID func (r *RedisSessionStore) GetSession(sessionID string) (*SessionData, error) { + // Try Redis first + if r.redisClient != nil { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + sessionJSON, err := r.redisClient.Get(ctx, "session:"+sessionID).Result() + if err == nil { + var sessionData SessionData + if err := json.Unmarshal([]byte(sessionJSON), &sessionData); err == nil { + // Update last active time + sessionData.LastActive = time.Now() + // Update in Redis + updatedJSON, _ := json.Marshal(sessionData) + r.redisClient.Set(ctx, "session:"+sessionID, updatedJSON, 24*time.Hour) + return &sessionData, nil + } + } + // Fall back to memory if Redis fails + } + + // Fallback to in-memory storage if session, exists := r.sessions[sessionID]; exists { // Update last active time session.LastActive = time.Now() return session, nil } + return nil, fmt.Errorf("session not found") } // UpdateSession updates an existing session func (r *RedisSessionStore) UpdateSession(sessionID string, sessionData *SessionData) error { + sessionData.LastActive = time.Now() + + // Try Redis first + if r.redisClient != nil { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + sessionJSON, err := json.Marshal(sessionData) + if err != nil { + return fmt.Errorf("failed to marshal session data: %w", err) + } + + err = r.redisClient.Set(ctx, "session:"+sessionID, sessionJSON, 24*time.Hour).Err() + if err == nil { + return nil + } + // Fall back to memory if Redis fails + } + + // Fallback to in-memory storage if _, exists := r.sessions[sessionID]; exists { - sessionData.LastActive = time.Now() r.sessions[sessionID] = sessionData return nil } + return fmt.Errorf("session not found") } // DeleteSession removes a session func (r *RedisSessionStore) DeleteSession(sessionID string) error { + // Try Redis first + if r.redisClient != nil { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + err := r.redisClient.Del(ctx, "session:"+sessionID).Err() + if err == nil { + // Also remove from memory fallback + delete(r.sessions, sessionID) + return nil + } + // Fall back to memory if Redis fails + } + + // Fallback to in-memory storage delete(r.sessions, sessionID) return nil } @@ -93,8 +175,8 @@ func (r *RedisSessionStore) CleanupExpiredSessions() error { var sessionStore SessionStore // InitSessionStore initializes the session store -func InitSessionStore() { - sessionStore = NewSessionStore() +func InitSessionStore(redisClient *redis.Client) { + sessionStore = NewSessionStore(redisClient) // Start cleanup goroutine go func() {