After this commit, dev auth would start working with the `REMARK_URL` hostname instead of the previously hardcoded 127.0.0.1. Breaks development setup where `REMARK_URL` was set to a non-standard value and dev auth was running on 127.0.0.1 and working, as, after that change, it would stop working.
172 lines
4.0 KiB
Go
172 lines
4.0 KiB
Go
package lcw
|
|
|
|
import (
|
|
"fmt"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/go-pkgz/lcw/eventbus"
|
|
"github.com/go-pkgz/lcw/internal/cache"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ExpirableCache implements LoadingCache with TTL.
|
|
type ExpirableCache struct {
|
|
options
|
|
CacheStat
|
|
currentSize int64
|
|
id string
|
|
backend *cache.LoadingCache
|
|
}
|
|
|
|
// NewExpirableCache makes expirable LoadingCache implementation, 1000 max keys by default and 5m TTL
|
|
func NewExpirableCache(opts ...Option) (*ExpirableCache, error) {
|
|
res := ExpirableCache{
|
|
options: options{
|
|
maxKeys: 1000,
|
|
maxValueSize: 0,
|
|
ttl: 5 * time.Minute,
|
|
eventBus: &eventbus.NopPubSub{},
|
|
},
|
|
id: uuid.New().String(),
|
|
}
|
|
|
|
for _, opt := range opts {
|
|
if err := opt(&res.options); err != nil {
|
|
return nil, fmt.Errorf("failed to set cache option: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := res.eventBus.Subscribe(res.onBusEvent); err != nil {
|
|
return nil, fmt.Errorf("can't subscribe to event bus: %w", err)
|
|
}
|
|
|
|
backend, err := cache.NewLoadingCache(
|
|
cache.MaxKeys(res.maxKeys),
|
|
cache.TTL(res.ttl),
|
|
cache.PurgeEvery(res.ttl/2),
|
|
cache.OnEvicted(func(key string, value interface{}) {
|
|
if res.onEvicted != nil {
|
|
res.onEvicted(key, value)
|
|
}
|
|
if s, ok := value.(Sizer); ok {
|
|
size := s.Size()
|
|
atomic.AddInt64(&res.currentSize, -1*int64(size))
|
|
}
|
|
// ignore the error on Publish as we don't have log inside the module and
|
|
// there is no other way to handle it: we publish the cache invalidation
|
|
// and hope for the best
|
|
_ = res.eventBus.Publish(res.id, key)
|
|
}),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error creating backend: %w", err)
|
|
}
|
|
res.backend = backend
|
|
|
|
return &res, nil
|
|
}
|
|
|
|
// Get gets value by key or load with fn if not found in cache
|
|
func (c *ExpirableCache) Get(key string, fn func() (interface{}, error)) (data interface{}, err error) {
|
|
if v, ok := c.backend.Get(key); ok {
|
|
atomic.AddInt64(&c.Hits, 1)
|
|
return v, nil
|
|
}
|
|
|
|
if data, err = fn(); err != nil {
|
|
atomic.AddInt64(&c.Errors, 1)
|
|
return data, err
|
|
}
|
|
atomic.AddInt64(&c.Misses, 1)
|
|
|
|
if !c.allowed(key, data) {
|
|
return data, nil
|
|
}
|
|
|
|
if s, ok := data.(Sizer); ok {
|
|
if c.maxCacheSize > 0 && atomic.LoadInt64(&c.currentSize)+int64(s.Size()) >= c.maxCacheSize {
|
|
c.backend.DeleteExpired()
|
|
return data, nil
|
|
}
|
|
atomic.AddInt64(&c.currentSize, int64(s.Size()))
|
|
}
|
|
|
|
c.backend.Set(key, data)
|
|
|
|
return data, nil
|
|
}
|
|
|
|
// Invalidate removes keys with passed predicate fn, i.e. fn(key) should be true to get evicted
|
|
func (c *ExpirableCache) Invalidate(fn func(key string) bool) {
|
|
c.backend.InvalidateFn(fn)
|
|
}
|
|
|
|
// Peek returns the key value (or undefined if not found) without updating the "recently used"-ness of the key.
|
|
func (c *ExpirableCache) Peek(key string) (interface{}, bool) {
|
|
return c.backend.Peek(key)
|
|
}
|
|
|
|
// Purge clears the cache completely.
|
|
func (c *ExpirableCache) Purge() {
|
|
c.backend.Purge()
|
|
atomic.StoreInt64(&c.currentSize, 0)
|
|
}
|
|
|
|
// Delete cache item by key
|
|
func (c *ExpirableCache) Delete(key string) {
|
|
c.backend.Invalidate(key)
|
|
}
|
|
|
|
// Keys returns cache keys
|
|
func (c *ExpirableCache) Keys() (res []string) {
|
|
return c.backend.Keys()
|
|
}
|
|
|
|
// Stat returns cache statistics
|
|
func (c *ExpirableCache) Stat() CacheStat {
|
|
return CacheStat{
|
|
Hits: c.Hits,
|
|
Misses: c.Misses,
|
|
Size: c.size(),
|
|
Keys: c.keys(),
|
|
Errors: c.Errors,
|
|
}
|
|
}
|
|
|
|
// Close kills cleanup goroutine
|
|
func (c *ExpirableCache) Close() error {
|
|
c.backend.Close()
|
|
return nil
|
|
}
|
|
|
|
// onBusEvent reacts on invalidation message triggered by event bus from another cache instance
|
|
func (c *ExpirableCache) onBusEvent(id, key string) {
|
|
if id != c.id {
|
|
c.backend.Invalidate(key)
|
|
}
|
|
}
|
|
|
|
func (c *ExpirableCache) size() int64 {
|
|
return atomic.LoadInt64(&c.currentSize)
|
|
}
|
|
|
|
func (c *ExpirableCache) keys() int {
|
|
return c.backend.ItemCount()
|
|
}
|
|
|
|
func (c *ExpirableCache) allowed(key string, data interface{}) bool {
|
|
if c.backend.ItemCount() >= c.maxKeys {
|
|
return false
|
|
}
|
|
if c.maxKeySize > 0 && len(key) > c.maxKeySize {
|
|
return false
|
|
}
|
|
if s, ok := data.(Sizer); ok {
|
|
if c.maxValueSize > 0 && s.Size() >= c.maxValueSize {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|