package redis import ( "context" "encoding/json" "errors" "fmt" "recipes/internal/domain/models" "time" "github.com/redis/go-redis/v9" ) type Storage interface { AddRecipe(ctx context.Context, recipe models.Recipe) error RecipeExists(ctx context.Context, title string) (bool, error) GetRecipes(ctx context.Context, offset, limit int) ([]models.Recipe, error) GetRecipe(ctx context.Context, r_id uint) (models.Recipe, error) GetRecipesByCategory(ctx context.Context, offset, limit int, category string) ([]models.Recipe, error) } type CacheProvider struct { rdb *redis.Client storage Storage } const cacheExpiration = time.Hour func New(addr, password string, s Storage) *CacheProvider { rdb := redis.NewClient(&redis.Options{ Addr: addr, Password: password, }) return &CacheProvider{ rdb: rdb, storage: s, } } // GetRecipe gets recipe from cache (if exists) or from storage. func (c *CacheProvider) GetRecipe(ctx context.Context, r_id uint) (models.Recipe, error) { const op = "cache_provider.redis.GetRecipe" // try to get from cache cache_data := c.rdb.Get(ctx, fmt.Sprintf("recipe:%d", r_id)) if cache_data.Err() != nil { if errors.Is(cache_data.Err(), redis.Nil) { // if empty - get from storage recipe, err := c.addRecipe(ctx, r_id) if err != nil { return models.Recipe{}, fmt.Errorf("%s: %w", op, err) } return recipe, nil } return models.Recipe{}, fmt.Errorf("%s: %w", op, cache_data.Err()) } // decode var recipe models.Recipe err := json.Unmarshal([]byte(cache_data.Val()), &recipe) if err != nil { return models.Recipe{}, fmt.Errorf("%s: %w", op, err) } // return return recipe, nil } // addRecipe adds recipe from storage to cache and returned it. func (c *CacheProvider) addRecipe(ctx context.Context, r_id uint) (models.Recipe, error) { const op = "cache_provider.redis.addRecipe" // Get from storage recipe, err := c.storage.GetRecipe(ctx, r_id) if err != nil { return models.Recipe{}, fmt.Errorf("%s: %w", op, err) } // Save to cache key := fmt.Sprintf("recipe:%d", recipe.ID) // if _, err := c.rdb.Pipelined(ctx, func(p redis.Pipeliner) error { // p.HSet(ctx, key, "title", recipe.Title) // p.HSet(ctx, key, "desc", recipe.Description) // p.HSet(ctx, key, "img", recipe.Image) // p.HSet(ctx, key, "ctime", recipe.CookingTime) // p.HSet(ctx, key, "snum", recipe.ServingsNum) // p.HSet(ctx, key, "cal", recipe.Calories) // return nil // }); err != nil { // return models.Recipe{}, fmt.Errorf("%s: %w", op, err) // } // encode cache_data, err := json.Marshal(recipe) if err != nil { return models.Recipe{}, fmt.Errorf("%s: %w", op, err) } s := c.rdb.Set(ctx, key, cache_data, cacheExpiration) if s.Err() != nil { return models.Recipe{}, fmt.Errorf("%s: %w", op, err) } return recipe, nil } // GetRecipes gets recipes from cache if exists or from storage. func (c *CacheProvider) GetRecipes(ctx context.Context, offset, limit int) ([]models.Recipe, error) { const op = "cache_provider.redis.GetRecipes" // try to get from cache key := fmt.Sprintf("recipes_page:%d:%d", offset, limit) cache_data := c.rdb.Get(ctx, key) if cache_data.Err() != nil { if errors.Is(cache_data.Err(), redis.Nil) { // get from storage recipes, err := c.addRecipesPage(ctx, offset, limit) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } return recipes, nil } return nil, fmt.Errorf("%s: %w", op, cache_data.Err()) } // decode var recipes []models.Recipe = make([]models.Recipe, 0, limit) err := json.Unmarshal([]byte(cache_data.Val()), &recipes) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } return recipes, nil } // addRecipesPage adds recipes list (page) to cache. func (c *CacheProvider) addRecipesPage(ctx context.Context, offset, limit int) ([]models.Recipe, error) { const op = "cache_provider.redis.addRecipesPage" // Get from storage recipes, err := c.storage.GetRecipes(ctx, offset, limit) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } // Save to cache cache_data, err := json.Marshal(recipes) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } key := fmt.Sprintf("recipes_page:%d:%d", offset, limit) s := c.rdb.Set(ctx, key, cache_data, cacheExpiration) if s.Err() != nil { return nil, fmt.Errorf("%s: %w", op, err) } return recipes, nil } // GetRecipesByCategory gets recipes by category from cache if exists or from storage if not. func (c *CacheProvider) GetRecipesByCategory(ctx context.Context, offset, limit int, category string) ([]models.Recipe, error) { const op = "cache_provider.redis.GetRecipesByCategory" // try to get from cache key := fmt.Sprintf("r:%s:%d:%d", category, offset, limit) cache_data := c.rdb.Get(ctx, key) if cache_data.Err() != nil { if errors.Is(cache_data.Err(), redis.Nil) { // get from storage recipes, err := c.addRecipesByCategory(ctx, offset, limit, category) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } return recipes, nil } return nil, fmt.Errorf("%s: %w", op, cache_data.Err()) } // decode var recipes []models.Recipe = make([]models.Recipe, 0, limit) err := json.Unmarshal([]byte(cache_data.Val()), &recipes) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } return recipes, nil } func (c *CacheProvider) addRecipesByCategory(ctx context.Context, offset, limit int, category string) ([]models.Recipe, error) { const op = "cache_provider.redis.addRecipesByCategory" // Get from storage recipes, err := c.storage.GetRecipesByCategory(ctx, offset, limit, category) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } // Save to cache cache_data, err := json.Marshal(recipes) if err != nil { return nil, fmt.Errorf("%s: %w", op, err) } key := fmt.Sprintf("r:%s:%d:%d", category, offset, limit) s := c.rdb.Set(ctx, key, cache_data, cacheExpiration) if s.Err() != nil { return nil, fmt.Errorf("%s: %w", op, err) } return recipes, nil }