recipes2/internal/cache_provider/redis/redis.go

199 lines
6.0 KiB
Go

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) {
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
}
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
}