diff --git a/README.md b/README.md index f87f2a1..60c6bf0 100644 --- a/README.md +++ b/README.md @@ -182,4 +182,4 @@ func main() { ## The gopher -The gopher used here was created using [Gopherize.me](https://gopherize.me/). Incache helps you keep your application latency low and your database destressed. +The gopher used here was created using [Gopherize.me](https://gopherize.me/). Pocache helps you to stop the herd from thundering. diff --git a/cache.go b/cache.go index 5938074..45ecb2e 100644 --- a/cache.go +++ b/cache.go @@ -14,6 +14,7 @@ import ( var ( ErrValidation = errors.New("invalid") + ErrPanic = errors.New("panicked") ) type ( @@ -69,16 +70,15 @@ func (cfg *Config[K, T]) Sanitize() { cfg.QLength = 1000 } - // there's no practical usecase of cache less than a second, as of now - if cfg.CacheAge == 0 { + if cfg.CacheAge <= 0 { cfg.CacheAge = time.Minute } - if cfg.Threshold == 0 { + if cfg.Threshold <= 0 { cfg.Threshold = cfg.CacheAge - time.Second } - if cfg.UpdaterTimeout == 0 { + if cfg.UpdaterTimeout <= 0 { cfg.UpdaterTimeout = time.Second } } @@ -211,6 +211,20 @@ func (ch *Cache[K, T]) updateListener(keys <-chan K) { } func (ch *Cache[K, T]) update(key K) { + defer func() { + rec := recover() + if rec == nil { + return + } + ch.updateInProgress.Delete(key) + err, isErr := rec.(error) + if isErr { + ch.errCallback(errors.Join(ErrPanic, err)) + return + } + ch.errCallback(errors.Join(ErrPanic, fmt.Errorf("%+v", rec))) + }() + ctx, cancel := context.WithTimeout(context.Background(), ch.updaterTimeout) defer cancel() diff --git a/cache_test.go b/cache_test.go index 17ed73d..4872c42 100644 --- a/cache_test.go +++ b/cache_test.go @@ -2,6 +2,7 @@ package pocache import ( "context" + "errors" "fmt" "sync/atomic" "testing" @@ -153,66 +154,51 @@ func TestCache(tt *testing.T) { asserter.True(found) }) - tt.Run("err watcher", func(t *testing.T) { - forcedErr := fmt.Errorf("forced error") - ranUpdater := atomic.Bool{} - ranErrWatcher := atomic.Bool{} - + tt.Run("disabled", func(t *testing.T) { cache, err := New(Config[string, any]{ LRUCacheSize: 10000, CacheAge: time.Minute, Threshold: time.Second * 59, - DisableCache: false, + DisableCache: true, Updater: func(ctx context.Context, key string) (any, error) { - ranUpdater.Store(true) - return nil, forcedErr - }, - ErrWatcher: func(watcherErr error) { - ranErrWatcher.Store(true) - asserter.ErrorIs(watcherErr, forcedErr) + return key, nil }, }) requirer.NoError(err) _ = cache.BulkAdd([]Tuple[string, any]{{Key: prefix, Value: value}}) // wait for threshold window - time.Sleep(time.Second) + time.Sleep(time.Second * 2) + // trigger auto update within threshold window _ = cache.Get(prefix) - // wait for the updater callback to be executed - time.Sleep(time.Second * 2) - asserter.True(ranUpdater.Load()) - asserter.True(ranErrWatcher.Load()) + // wait for updater to be executed + time.Sleep(time.Second * 1) + v := cache.Get(prefix) + asserter.False(v.Found) }) - tt.Run("no err watcher", func(t *testing.T) { - forcedErr := fmt.Errorf("forced error") - ranUpdater := atomic.Bool{} - ranErrWatcher := atomic.Bool{} - + tt.Run("no updater", func(t *testing.T) { cache, err := New(Config[string, any]{ LRUCacheSize: 10000, CacheAge: time.Minute, Threshold: time.Second * 59, DisableCache: false, - Updater: func(ctx context.Context, key string) (any, error) { - ranUpdater.Store(true) - return nil, forcedErr - }, + Updater: nil, }) requirer.NoError(err) - _ = cache.BulkAdd([]Tuple[string, any]{{Key: prefix, Value: value}}) + _ = cache.Add(prefix, value) // wait for threshold window - time.Sleep(time.Second) + time.Sleep(time.Second * 2) // trigger auto update within threshold window _ = cache.Get(prefix) - - // wait for the updater callback to be executed + // wait for updater to run time.Sleep(time.Second * 2) - asserter.True(ranUpdater.Load()) - asserter.False(ranErrWatcher.Load()) + + v := cache.Get(prefix) + asserter.EqualValues(value, v.V) }) } @@ -369,3 +355,142 @@ func TestPayload(tt *testing.T) { asserter.EqualValues(time.Time{}, pyl.Expiry()) }) } + +func TestErrWatcher(tt *testing.T) { + var ( + prefix = "prefix" + value = "value" + requirer = require.New(tt) + asserter = require.New(tt) + ) + + tt.Run("err watcher", func(t *testing.T) { + forcedErr := fmt.Errorf("forced error") + ranUpdater := atomic.Bool{} + ranErrWatcher := atomic.Bool{} + + cache, err := New(Config[string, any]{ + LRUCacheSize: 10000, + CacheAge: time.Minute, + Threshold: time.Second * 59, + DisableCache: false, + Updater: func(ctx context.Context, key string) (any, error) { + ranUpdater.Store(true) + return nil, forcedErr + }, + ErrWatcher: func(watcherErr error) { + ranErrWatcher.Store(true) + asserter.ErrorIs(watcherErr, forcedErr) + }, + }) + requirer.NoError(err) + + _ = cache.BulkAdd([]Tuple[string, any]{{Key: prefix, Value: value}}) + // wait for threshold window + time.Sleep(time.Second) + // trigger auto update within threshold window + _ = cache.Get(prefix) + + // wait for the updater callback to be executed + time.Sleep(time.Second * 2) + asserter.True(ranUpdater.Load()) + asserter.True(ranErrWatcher.Load()) + }) + + tt.Run("no err watcher", func(t *testing.T) { + forcedErr := fmt.Errorf("forced error") + ranUpdater := atomic.Bool{} + ranErrWatcher := atomic.Bool{} + + cache, err := New(Config[string, any]{ + LRUCacheSize: 10000, + CacheAge: time.Minute, + Threshold: time.Second * 59, + DisableCache: false, + Updater: func(ctx context.Context, key string) (any, error) { + ranUpdater.Store(true) + return nil, forcedErr + }, + }) + requirer.NoError(err) + + _ = cache.BulkAdd([]Tuple[string, any]{{Key: prefix, Value: value}}) + // wait for threshold window + time.Sleep(time.Second) + // trigger auto update within threshold window + _ = cache.Get(prefix) + + // wait for the updater callback to be executed + time.Sleep(time.Second * 2) + asserter.True(ranUpdater.Load()) + asserter.False(ranErrWatcher.Load()) + }) + + tt.Run("err watcher: catch panic text", func(t *testing.T) { + ranUpdater := atomic.Bool{} + ranErrWatcher := atomic.Bool{} + + cache, err := New(Config[string, any]{ + LRUCacheSize: 10000, + CacheAge: time.Minute, + Threshold: time.Second * 59, + DisableCache: false, + Updater: func(ctx context.Context, key string) (any, error) { + ranUpdater.Store(true) + panic("force panicked") + }, + ErrWatcher: func(watcherErr error) { + ranErrWatcher.Store(true) + asserter.ErrorContains(watcherErr, "force panicked") + }, + }) + requirer.NoError(err) + cache.Add(prefix, value) + + // wait for threshold window + time.Sleep(time.Second) + // trigger auto update within threshold window + _ = cache.Get(prefix) + + // wait for the updater callback to be executed + time.Sleep(time.Second * 2) + asserter.True(ranUpdater.Load()) + asserter.True(ranErrWatcher.Load()) + + }) + + tt.Run("err watcher: catch panic err", func(t *testing.T) { + ranUpdater := atomic.Bool{} + ranErrWatcher := atomic.Bool{} + ErrPanic := errors.New("panic err") + + cache, err := New(Config[string, any]{ + LRUCacheSize: 10000, + CacheAge: time.Minute, + Threshold: time.Second * 59, + DisableCache: false, + Updater: func(ctx context.Context, key string) (any, error) { + ranUpdater.Store(true) + panic(ErrPanic) + }, + ErrWatcher: func(watcherErr error) { + ranErrWatcher.Store(true) + asserter.ErrorIs(watcherErr, ErrPanic) + }, + }) + requirer.NoError(err) + cache.Add(prefix, value) + + // wait for threshold window + time.Sleep(time.Second) + // trigger auto update within threshold window + _ = cache.Get(prefix) + + // wait for the updater callback to be executed + time.Sleep(time.Second * 2) + asserter.True(ranUpdater.Load()) + asserter.True(ranErrWatcher.Load()) + + }) + +}