feat: apply cache to tag

This commit is contained in:
Sosokker 2025-04-21 00:36:21 +07:00
parent 1ddffbc026
commit 0ef57400ba
4 changed files with 140 additions and 16 deletions

View File

@ -14,6 +14,7 @@ import (
"time" "time"
"github.com/Sosokker/todolist-backend/internal/api" "github.com/Sosokker/todolist-backend/internal/api"
"github.com/Sosokker/todolist-backend/internal/cache"
"github.com/Sosokker/todolist-backend/internal/config" "github.com/Sosokker/todolist-backend/internal/config"
"github.com/Sosokker/todolist-backend/internal/repository" "github.com/Sosokker/todolist-backend/internal/repository"
"github.com/Sosokker/todolist-backend/internal/service" "github.com/Sosokker/todolist-backend/internal/service"
@ -53,7 +54,10 @@ func main() {
os.Exit(1) os.Exit(1)
} }
repoRegistry := repository.NewRepositoryRegistry(pool) // --- Cache Setup ---
appCache := cache.NewMemoryCache(cfg.Cache, logger)
repoRegistry := repository.NewRepositoryRegistry(pool, appCache, logger)
var storageService service.FileStorageService var storageService service.FileStorageService
storageService, err = service.NewGCStorageService(cfg.Storage.GCS, logger) storageService, err = service.NewGCStorageService(cfg.Storage.GCS, logger)

View File

@ -20,17 +20,28 @@ type Cache interface {
type memoryCache struct { type memoryCache struct {
client *gocache.Cache client *gocache.Cache
logger *slog.Logger logger *slog.Logger
defaultExpiration time.Duration
} }
// NewMemoryCache creates a new in-memory cache // NewMemoryCache creates a new in-memory cache
func NewMemoryCache(cfg config.CacheConfig, logger *slog.Logger) Cache { func NewMemoryCache(cfg config.CacheConfig, logger *slog.Logger) Cache {
c := gocache.New(cfg.DefaultExpiration, cfg.CleanupInterval) defaultExp := cfg.DefaultExpiration
if defaultExp <= 0 {
defaultExp = 5 * time.Minute
}
cleanupInterval := cfg.CleanupInterval
if cleanupInterval <= 0 {
cleanupInterval = 10 * time.Minute
}
c := gocache.New(defaultExp, cleanupInterval)
logger.Info("In-memory cache initialized", logger.Info("In-memory cache initialized",
"defaultExpiration", cfg.DefaultExpiration, "defaultExpiration", defaultExp,
"cleanupInterval", cfg.CleanupInterval) "cleanupInterval", cleanupInterval)
return &memoryCache{ return &memoryCache{
client: c, client: c,
logger: logger, logger: logger.With("component", "memoryCache"),
defaultExpiration: defaultExp,
} }
} }
@ -45,8 +56,12 @@ func (m *memoryCache) Get(ctx context.Context, key string) (interface{}, bool) {
} }
func (m *memoryCache) Set(ctx context.Context, key string, value interface{}, duration time.Duration) { func (m *memoryCache) Set(ctx context.Context, key string, value interface{}, duration time.Duration) {
m.logger.DebugContext(ctx, "Setting cache", "key", key, "duration", duration) exp := duration
m.client.Set(key, value, duration) // duration=0 means use default, -1 means never expire (DefaultExpiration) if exp <= 0 {
exp = m.defaultExpiration
}
m.logger.DebugContext(ctx, "Setting cache", "key", key, "duration", exp)
m.client.Set(key, value, exp)
} }
func (m *memoryCache) Delete(ctx context.Context, key string) { func (m *memoryCache) Delete(ctx context.Context, key string) {

View File

@ -2,8 +2,10 @@ package repository
import ( import (
"context" "context"
"log/slog"
"time" "time"
"github.com/Sosokker/todolist-backend/internal/cache"
"github.com/Sosokker/todolist-backend/internal/domain" "github.com/Sosokker/todolist-backend/internal/domain"
db "github.com/Sosokker/todolist-backend/internal/repository/sqlc/generated" db "github.com/Sosokker/todolist-backend/internal/repository/sqlc/generated"
"github.com/google/uuid" "github.com/google/uuid"
@ -82,14 +84,22 @@ type RepositoryRegistry struct {
Pool *pgxpool.Pool Pool *pgxpool.Pool
} }
// NewRepositoryRegistry creates a new registry // NewRepositoryRegistry creates a new registry, now with caching decorators
func NewRepositoryRegistry(pool *pgxpool.Pool) *RepositoryRegistry { func NewRepositoryRegistry(pool *pgxpool.Pool, cache cache.Cache, logger *slog.Logger) *RepositoryRegistry {
queries := db.New(pool) queries := db.New(pool)
pgxUserRepo := NewPgxUserRepository(queries)
pgxTagRepo := NewPgxTagRepository(queries)
pgxTodoRepo := NewPgxTodoRepository(queries, pool)
pgxSubtaskRepo := NewPgxSubtaskRepository(queries)
cachingTagRepo := NewCachingTagRepository(pgxTagRepo, cache, logger)
return &RepositoryRegistry{ return &RepositoryRegistry{
UserRepo: NewPgxUserRepository(queries), UserRepo: pgxUserRepo, // Not cached yet in this example
TagRepo: NewPgxTagRepository(queries), TagRepo: cachingTagRepo, // Use the caching decorator
TodoRepo: NewPgxTodoRepository(queries, pool), TodoRepo: pgxTodoRepo, // Not cached yet in this example
SubtaskRepo: NewPgxSubtaskRepository(queries), SubtaskRepo: pgxSubtaskRepo, // Not cached yet in this example
Queries: queries, Queries: queries,
Pool: pool, Pool: pool,
} }

View File

@ -0,0 +1,95 @@
package repository
import (
"context"
"fmt"
"log/slog"
"github.com/Sosokker/todolist-backend/internal/cache"
"github.com/Sosokker/todolist-backend/internal/domain"
"github.com/google/uuid"
)
type cachingTagRepository struct {
next TagRepository
cache cache.Cache
logger *slog.Logger
}
func NewCachingTagRepository(next TagRepository, cache cache.Cache, logger *slog.Logger) TagRepository {
return &cachingTagRepository{
next: next,
cache: cache,
logger: logger.With("repository", "tag_cache_decorator"),
}
}
// --- Cache Key Generation ---
func tagCacheKey(userID, tagID uuid.UUID) string {
return fmt.Sprintf("user:%s:tag:%s", userID, tagID)
}
// --- TagRepository Interface Implementation ---
func (r *cachingTagRepository) Create(ctx context.Context, tag *domain.Tag) (*domain.Tag, error) {
createdTag, err := r.next.Create(ctx, tag)
// Invalidate list caches if/when implemented
return createdTag, err
}
func (r *cachingTagRepository) GetByID(ctx context.Context, id, userID uuid.UUID) (*domain.Tag, error) {
cacheKey := tagCacheKey(userID, id)
if cached, found := r.cache.Get(ctx, cacheKey); found {
if tag, ok := cached.(*domain.Tag); ok {
r.logger.DebugContext(ctx, "GetTagByID cache hit", "key", cacheKey)
return tag, nil
}
// Invalid type in cache, treat as miss and delete
r.logger.WarnContext(ctx, "Invalid type found in tag cache", "key", cacheKey, "type", fmt.Sprintf("%T", cached))
r.cache.Delete(ctx, cacheKey)
}
r.logger.DebugContext(ctx, "GetTagByID cache miss", "key", cacheKey)
tag, err := r.next.GetByID(ctx, id, userID)
if err != nil {
return nil, err
}
if tag != nil {
r.cache.Set(ctx, cacheKey, tag, 0)
r.logger.DebugContext(ctx, "Set tag in cache", "key", cacheKey)
}
return tag, nil
}
func (r *cachingTagRepository) Update(ctx context.Context, id, userID uuid.UUID, updateData *domain.Tag) (*domain.Tag, error) {
updatedTag, err := r.next.Update(ctx, id, userID, updateData)
if err != nil {
return nil, err
}
cacheKey := tagCacheKey(userID, id)
r.cache.Delete(ctx, cacheKey)
r.logger.DebugContext(ctx, "Invalidated tag cache after update", "key", cacheKey)
return updatedTag, nil
}
func (r *cachingTagRepository) Delete(ctx context.Context, id, userID uuid.UUID) error {
err := r.next.Delete(ctx, id, userID)
if err != nil {
return err
}
cacheKey := tagCacheKey(userID, id)
r.cache.Delete(ctx, cacheKey)
r.logger.DebugContext(ctx, "Invalidated tag cache after delete", "key", cacheKey)
return nil
}
// --- Pass-through methods ---
func (r *cachingTagRepository) GetByIDs(ctx context.Context, ids []uuid.UUID, userID uuid.UUID) ([]domain.Tag, error) {
return r.next.GetByIDs(ctx, ids, userID)
}
func (r *cachingTagRepository) ListByUser(ctx context.Context, userID uuid.UUID) ([]domain.Tag, error) {
return r.next.ListByUser(ctx, userID)
}