2024-01-16 21:10:29 +02:00
|
|
|
package redis
|
|
|
|
|
|
|
|
import (
|
2024-01-27 20:42:23 +02:00
|
|
|
"context"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"recipes/internal/domain/models"
|
|
|
|
"time"
|
|
|
|
|
2024-01-16 21:10:29 +02:00
|
|
|
"github.com/redis/go-redis/v9"
|
|
|
|
)
|
|
|
|
|
2024-01-27 20:42:23 +02:00
|
|
|
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) {
|
|
|
|
fmt.Println("--- STORAGE ---")
|
|
|
|
// 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())
|
|
|
|
}
|
|
|
|
fmt.Println("--- CACHE ---")
|
|
|
|
// 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) {
|
|
|
|
fmt.Println("--- STORAGE ---")
|
|
|
|
// 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())
|
|
|
|
}
|
|
|
|
fmt.Println("--- CACHE ---")
|
|
|
|
// 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) {
|
|
|
|
fmt.Println("--- STORAGE ---")
|
|
|
|
// 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())
|
|
|
|
}
|
|
|
|
fmt.Println("--- CACHE ---")
|
|
|
|
// 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
|
2024-01-16 21:10:29 +02:00
|
|
|
}
|
|
|
|
|
2024-01-27 20:42:23 +02:00
|
|
|
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
|
2024-01-16 21:10:29 +02:00
|
|
|
}
|