File size: 5,727 Bytes
9853396 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | package xcache
import (
"context"
"errors"
"fmt"
"time"
"github.com/eko/gocache/lib/v4/store"
cachelib "github.com/eko/gocache/lib/v4/cache"
gocache_store "github.com/eko/gocache/store/go_cache/v4"
gocache "github.com/patrickmn/go-cache"
redis "github.com/redis/go-redis/v9"
"github.com/looplj/axonhub/internal/log"
redis_store "github.com/looplj/axonhub/internal/pkg/xcache/redis"
"github.com/looplj/axonhub/internal/pkg/xredis"
)
// Cache is an alias to the gocache CacheInterface for convenience.
// It allows you to depend on xcache while still exposing the common methods:
// - Get(ctx, key) (T, error)
// - Set(ctx, key, value, options ...Option) error
// - Delete(ctx, key) error
// - Invalidate(ctx, options ...store.InvalidateOption) error
// - Clear(ctx) error
// - GetType() string
// For setter caches (memory/redis/chain), GetWithTTL and GetCodec are also available on the returned value.
// See: github.com/eko/gocache/lib/v4/cache
//
// Usage example:
// mem := xcache.NewMemory[string](gocache.New(5*time.Minute, 10*time.Minute))
// rdb := redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379"})
// rds := xcache.NewRedis[string](rdb, store.WithExpiration(15*time.Second))
// l2 := xcache.NewTwoLevel[string](mem, rds)
//
// _ = mem.Set(ctx, "foo", "bar")
// val, _ := l2.Get(ctx, "foo")
type Cache[T any] = cachelib.CacheInterface[T]
type SetterCache[T any] = cachelib.SetterCacheInterface[T]
// NewMemory creates a pure in-memory cache using patrickmn/go-cache as the backend.
// Pass an existing *gocache.Cache so you control default expiration & cleanup interval.
// Optionally pass store options (e.g., store.WithExpiration) when setting values.
func NewMemory[T any](client *gocache.Cache, options ...Option) SetterCache[T] {
store := gocache_store.NewGoCache(client, options...)
return cachelib.New[T](store)
}
// NewMemoryWithOptions is a convenience constructor that builds the patrickmn/go-cache client
// for you using the provided default expiration and cleanup interval.
func NewMemoryWithOptions[T any](defaultExpiration, cleanupInterval time.Duration, options ...Option) SetterCache[T] {
client := gocache.New(defaultExpiration, cleanupInterval)
return NewMemory[T](client, options...)
}
// NewFromConfig builds a typed cache from the given Config.
// Modes:
// - memory: in-memory only
// - redis: redis only
// - two-level: memory + redis chain
//
// Memory and Redis expiration can be configured separately.
// If mode is not set or invalid, returns a noop cache that does nothing.
func NewFromConfig[T any](cfg Config) Cache[T] {
// If mode is not set or empty, return noop cache
if cfg.Mode == "" {
return NewNoop[T]()
}
// Build memory setter cache with separate expiration settings
memExpiration := defaultIfZero(cfg.Memory.Expiration, 5*time.Minute)
memCleanupInterval := defaultIfZero(cfg.Memory.CleanupInterval, 10*time.Minute)
memClient := gocache.New(memExpiration, memCleanupInterval)
memStore := gocache_store.NewGoCache(memClient, store.WithExpiration(memExpiration))
mem := cachelib.New[T](memStore)
// Build redis setter cache if configured
var rds SetterCache[T]
if (cfg.Redis.Addr != "" || cfg.Redis.URL != "") && cfg.Mode != ModeMemory {
client, err := xredis.NewClient(cfg.Redis)
if err != nil {
panic(fmt.Errorf("invalid redis config: %w", err))
}
redisExpiration := defaultIfZero(cfg.Redis.Expiration, 30*time.Minute) // Default longer for Redis
rdsStore := redis_store.NewRedisStore[T](client, store.WithExpiration(redisExpiration))
rds = cachelib.New[T](rdsStore)
}
switch cfg.Mode {
case ModeTwoLevel:
if rds != nil {
log.Info(context.Background(), "Using two-level cache")
return cachelib.NewChain[T](mem, rds)
}
return mem
case ModeRedis:
if rds == nil {
panic(errors.New("redis cache config is invalid"))
}
log.Info(context.Background(), "Using redis cache")
return rds
case ModeMemory:
log.Info(context.Background(), "Using memory cache")
return mem
default:
log.Info(context.Background(), "Disable cache")
// Return noop cache for invalid modes
return NewNoop[T]()
}
}
func defaultIfZero(d, def time.Duration) time.Duration {
if d == 0 {
return def
}
return d
}
// NewRedis creates a pure Redis cache using github.com/redis/go-redis/v9 as the client.
// You can pass store options like store.WithExpiration, store.WithTags, etc.
func NewRedis[T any](client *redis.Client, options ...Option) SetterCache[T] {
store := redis_store.NewRedisStore[T](client, options...)
return cachelib.New[T](store)
}
// NewRedisWithOptions builds a redis.Client for you and returns the cache.
func NewRedisWithOptions[T any](opts *redis.Options, options ...Option) SetterCache[T] {
client := redis.NewClient(opts)
return NewRedis[T](client, options...)
}
// NewTwoLevel constructs a 2-level cache: memory first, then Redis.
// It takes already constructed setter caches so you can mix any memory/redis implementations.
// Typical usage: NewTwoLevel(NewMemory(...), NewRedis(...)).
func NewTwoLevel[T any](memory SetterCache[T], redis SetterCache[T]) Cache[T] {
return cachelib.NewChain[T](memory, redis)
}
// NewTwoLevelWithClients is a convenience to create a 2-level cache from raw clients directly.
func NewTwoLevelWithClients[T any](memClient *gocache.Cache, redisClient *redis.Client, memOptions []Option, redisOptions []Option) Cache[T] {
mem := NewMemory[T](memClient, memOptions...)
rds := NewRedis[T](redisClient, redisOptions...)
return NewTwoLevel[T](mem, rds)
}
|