diff --git a/cache.go b/cache.go index e3b15cd..bd4621d 100644 --- a/cache.go +++ b/cache.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "reflect" "time" "github.com/gomodule/redigo/redis" @@ -11,6 +12,7 @@ import ( ) var ( + ErrExpired = errors.New("expired") ErrNotFound = errors.New("not found") OptWithoutGetter = optWithoutGetter{} // for get only OptWithCreateTime = optWithCreateTime{} // for set & get @@ -26,6 +28,13 @@ type Getter[T any] interface { GetById(id string) (dat *T, err error) } +type cacher[T any] struct { + cfg Config + getter Getter[T] + localCache *localCacher[T] + redisCache *redisCacher[T] +} + type ( Option any optWithoutGetter struct{} // for get only @@ -42,12 +51,13 @@ type Config struct { UseLocalCache bool // use localcache or not LocalCacheLifetimeSecond int64 // local cache lifetime(t<0: forever; t=0: default; t>0: seconds) UseRedisCache bool // use redis or not - RedisCacheConn redis.Conn // redis.Conn - RedisCacheConnPool *redis.Pool // redis.Pool + RedisCacheConn redis.Conn `json:"-"` // redis.Conn + RedisCacheConnPool *redis.Pool `json:"-"` // redis.Pool RedisCacheKeyPrefix string // redis key prefix RedisCacheLifetimeSecond int64 // redis cache lifetime(t<0: forever; t=0: default; t>0: seconds) UseGetter bool // not used GetterNoWarning bool // no warning if no getter + UseExpiredCache *bool // use expired cache or not (true: &[]bool{true}[0], false: &[]bool{true}[0]) } func NewCache[T any](getter Getter[T], cfg Config) Cacher[T] { @@ -59,6 +69,7 @@ func NewCache[T any](getter Getter[T], cfg Config) Cacher[T] { // Local cache if cfg.UseLocalCache { c.localCache = new(localCacher[T]) + c.localCache.cacher = c c.localCache.cacheItems = map[string]*cacheItem[T]{} c.localCache.cacheDuration = time.Duration(cfg.LocalCacheLifetimeSecond) * time.Second if c.localCache.cacheDuration.Nanoseconds() == 0 { @@ -72,6 +83,7 @@ func NewCache[T any](getter Getter[T], cfg Config) Cacher[T] { panic("redis cache's key prefix must not be null") } c.redisCache = new(redisCacher[T]) + c.redisCache.cacher = c c.redisCache.rds = cfg.RedisCacheConn c.redisCache.rdsPool = cfg.RedisCacheConnPool c.redisCache.cacheDuration = time.Duration(cfg.RedisCacheLifetimeSecond) * time.Second @@ -83,6 +95,13 @@ func NewCache[T any](getter Getter[T], cfg Config) Cacher[T] { panic("want to user redis cache, but redis.Conn is nil") } + //UseExpiredCache + if cfg.UseExpiredCache == nil { + cfg.UseExpiredCache = &[]bool{true}[0] + } + + name := reflect.TypeOf(*new(T)).String() + log.PrintPretty("new cache '"+name+"' by config:", cfg) return c } @@ -140,45 +159,55 @@ type cacheItem[T any] struct { } type localCacher[T any] struct { + cacher *cacher[T] cacheItems map[string]*cacheItem[T] cacheDuration time.Duration } type redisCacher[T any] struct { - rds redis.Conn - rdsPool *redis.Pool + cacher *cacher[T] cachePrefix string cacheDuration time.Duration -} - -type cacher[T any] struct { - cfg Config - getter Getter[T] - localCache *localCacher[T] - redisCache *redisCacher[T] + rds redis.Conn + rdsPool *redis.Pool } func (c *localCacher[T]) GetFromCache(id string, options ...Option) (t *T, err error) { + // Step 1. get from map var a, ok = c.cacheItems[id] if !ok { return nil, ErrNotFound } + // Step 2. check is expired or not if c.cacheDuration.Nanoseconds() > 0 { var earliestCreateTime = time.Now().Add(-c.cacheDuration) if a.CreateTime.Before(earliestCreateTime) { - log.Infof("cache(%s) is in local cache but expired", id) - //TODO: cocurrent - delete(c.cacheItems, id) - return nil, ErrNotFound + if *c.cacher.cfg.UseExpiredCache { + log.Infof("cache(%s) in local is expired, "+ + "we will use it when all other caches are missing", id) + err = ErrExpired + } else { + log.Infof("cache(%s) is in local cache but expired, we delete it", id) + //TODO: cocurrent + delete(c.cacheItems, id) + return nil, ErrNotFound + } } } + // Step 3. get cache creattime if need var opt = optionParser(options...) if isOptWithCreateTimeForGetter(opt) { *opt.withCreateTime.createtime = a.CreateTime } + // Step 4. return + if err == ErrExpired { + return a.Data, ErrExpired + } else if err != nil { + panic("unreachable code") + } return a.Data, nil } @@ -228,8 +257,14 @@ func (c *redisCacher[T]) GetFromCache(id string, options ...Option) (*T, error) if c.cacheDuration.Nanoseconds() > 0 { var earliestCreateTime = time.Now().Add(-c.cacheDuration) if a.CreateTime.Before(earliestCreateTime) { - log.Infof("app(%s) is in redis cache but expired", id) - return nil, ErrNotFound + if *c.cacher.cfg.UseExpiredCache { + log.Infof("cache(%s) in redis is expired, "+ + "we will use it when all other caches are missing", id) + err = ErrExpired + } else { + log.Infof("app(%s) is in redis cache but expired", id) + return nil, ErrNotFound + } } } @@ -238,6 +273,12 @@ func (c *redisCacher[T]) GetFromCache(id string, options ...Option) (*T, error) *opt.withCreateTime.createtime = a.CreateTime } + // Step 5. return + if err == ErrExpired { + return a.Data, ErrExpired + } else if err != nil { + panic("unreachable code") + } return a.Data, nil } @@ -287,13 +328,18 @@ func (c *cacher[T]) GetFromCache(id string, options ...Option) (dat *T, err erro } var opt = optionParser(options...) + var lastExpiredDat *T = nil // Step 2. get from [local] if c.localCache != nil { if dat, err := c.localCache.GetFromCache(id, options...); err == nil { log.Infof("get cache(id:%s) from localCacher success", id) return dat, nil + } else if err == ErrExpired { + log.Infof("get cache(id:%s) from localCacher success(but expired), need try next", id) + lastExpiredDat = dat + } else { + log.Infof("get cache(id:%s) from localCacher failed, need try next", id) } - log.Infof("get cache(id:%s) from localCacher failed, try next", id) } // Step 3. get from [redis] @@ -308,16 +354,28 @@ func (c *cacher[T]) GetFromCache(id string, options ...Option) (dat *T, err erro } log.Infof("get cache(id:%s) from redisCacher success", id) return dat, nil + } else if err == ErrExpired { + if c.localCache != nil { + c.localCache.SetIntoCache(id, dat, options...) //set create time from redis + log.Infof("set cache(id:%s) to localCache by redisCacher done", id) + } + log.Infof("get cache(id:%s) from redisCacher success(but expired), need try next", id) + lastExpiredDat = dat + } else { + log.Infof("get cache(id:%s) from redisCacher failed, need try next", id) } - log.Infof("get cache(id:%s) from redisCacher failed, try next", id) } var getter = c.getter if opt.withoutGetter != nil { - log.Infof("all cache failed, and option 'withnogetter' is set, return ErrNotFound") + log.Infof("all cache failed, and option 'withoutgetter' is set, return ErrNotFound") return nil, ErrNotFound } - if getter == nil { + + if getter == nil && lastExpiredDat != nil { + log.Infof("all cache failed(and getter is nil) but expired cache is avaliable, we use it") + return lastExpiredDat, nil + } else if getter == nil { log.Infof("all cache failed, and getter is nil, return ErrNotFound") if c.cfg.GetterNoWarning == false { log.Warningf("cache 'getter' is nil, did you save right config in database?") @@ -327,7 +385,10 @@ func (c *cacher[T]) GetFromCache(id string, options ...Option) (dat *T, err erro // Step 4. get from [storager(database or somewhere)] dat, err = getter.GetById(id) - if err != nil { + if err != nil && lastExpiredDat != nil { + log.Infof("all cache failed and getter failed, expired cache is avaliable, we will use it") + return lastExpiredDat, nil + } else if err != nil { log.Errorf("cache(id:%s) is not in database, something error: %s", id, err) return nil, ErrNotFound }