Skip to content

Commit 90cf3ab

Browse files
authored
[minor] added panic recovery (#12)
[-] added tests to cover more scenarios [-] updated README
1 parent 5e73983 commit 90cf3ab

File tree

3 files changed

+176
-37
lines changed

3 files changed

+176
-37
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,4 +182,4 @@ func main() {
182182

183183
## The gopher
184184

185-
The gopher used here was created using [Gopherize.me](https://gopherize.me/). Incache helps you keep your application latency low and your database destressed.
185+
The gopher used here was created using [Gopherize.me](https://gopherize.me/). Pocache helps you to stop the herd from thundering.

cache.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414

1515
var (
1616
ErrValidation = errors.New("invalid")
17+
ErrPanic = errors.New("panicked")
1718
)
1819

1920
type (
@@ -69,16 +70,15 @@ func (cfg *Config[K, T]) Sanitize() {
6970
cfg.QLength = 1000
7071
}
7172

72-
// there's no practical usecase of cache less than a second, as of now
73-
if cfg.CacheAge == 0 {
73+
if cfg.CacheAge <= 0 {
7474
cfg.CacheAge = time.Minute
7575
}
7676

77-
if cfg.Threshold == 0 {
77+
if cfg.Threshold <= 0 {
7878
cfg.Threshold = cfg.CacheAge - time.Second
7979
}
8080

81-
if cfg.UpdaterTimeout == 0 {
81+
if cfg.UpdaterTimeout <= 0 {
8282
cfg.UpdaterTimeout = time.Second
8383
}
8484
}
@@ -211,6 +211,20 @@ func (ch *Cache[K, T]) updateListener(keys <-chan K) {
211211
}
212212

213213
func (ch *Cache[K, T]) update(key K) {
214+
defer func() {
215+
rec := recover()
216+
if rec == nil {
217+
return
218+
}
219+
ch.updateInProgress.Delete(key)
220+
err, isErr := rec.(error)
221+
if isErr {
222+
ch.errCallback(errors.Join(ErrPanic, err))
223+
return
224+
}
225+
ch.errCallback(errors.Join(ErrPanic, fmt.Errorf("%+v", rec)))
226+
}()
227+
214228
ctx, cancel := context.WithTimeout(context.Background(), ch.updaterTimeout)
215229
defer cancel()
216230

cache_test.go

Lines changed: 157 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package pocache
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
"sync/atomic"
78
"testing"
@@ -153,66 +154,51 @@ func TestCache(tt *testing.T) {
153154
asserter.True(found)
154155
})
155156

156-
tt.Run("err watcher", func(t *testing.T) {
157-
forcedErr := fmt.Errorf("forced error")
158-
ranUpdater := atomic.Bool{}
159-
ranErrWatcher := atomic.Bool{}
160-
157+
tt.Run("disabled", func(t *testing.T) {
161158
cache, err := New(Config[string, any]{
162159
LRUCacheSize: 10000,
163160
CacheAge: time.Minute,
164161
Threshold: time.Second * 59,
165-
DisableCache: false,
162+
DisableCache: true,
166163
Updater: func(ctx context.Context, key string) (any, error) {
167-
ranUpdater.Store(true)
168-
return nil, forcedErr
169-
},
170-
ErrWatcher: func(watcherErr error) {
171-
ranErrWatcher.Store(true)
172-
asserter.ErrorIs(watcherErr, forcedErr)
164+
return key, nil
173165
},
174166
})
175167
requirer.NoError(err)
176168

177169
_ = cache.BulkAdd([]Tuple[string, any]{{Key: prefix, Value: value}})
178170
// wait for threshold window
179-
time.Sleep(time.Second)
171+
time.Sleep(time.Second * 2)
172+
180173
// trigger auto update within threshold window
181174
_ = cache.Get(prefix)
182175

183-
// wait for the updater callback to be executed
184-
time.Sleep(time.Second * 2)
185-
asserter.True(ranUpdater.Load())
186-
asserter.True(ranErrWatcher.Load())
176+
// wait for updater to be executed
177+
time.Sleep(time.Second * 1)
178+
v := cache.Get(prefix)
179+
asserter.False(v.Found)
187180
})
188181

189-
tt.Run("no err watcher", func(t *testing.T) {
190-
forcedErr := fmt.Errorf("forced error")
191-
ranUpdater := atomic.Bool{}
192-
ranErrWatcher := atomic.Bool{}
193-
182+
tt.Run("no updater", func(t *testing.T) {
194183
cache, err := New(Config[string, any]{
195184
LRUCacheSize: 10000,
196185
CacheAge: time.Minute,
197186
Threshold: time.Second * 59,
198187
DisableCache: false,
199-
Updater: func(ctx context.Context, key string) (any, error) {
200-
ranUpdater.Store(true)
201-
return nil, forcedErr
202-
},
188+
Updater: nil,
203189
})
204190
requirer.NoError(err)
205191

206-
_ = cache.BulkAdd([]Tuple[string, any]{{Key: prefix, Value: value}})
192+
_ = cache.Add(prefix, value)
207193
// wait for threshold window
208-
time.Sleep(time.Second)
194+
time.Sleep(time.Second * 2)
209195
// trigger auto update within threshold window
210196
_ = cache.Get(prefix)
211-
212-
// wait for the updater callback to be executed
197+
// wait for updater to run
213198
time.Sleep(time.Second * 2)
214-
asserter.True(ranUpdater.Load())
215-
asserter.False(ranErrWatcher.Load())
199+
200+
v := cache.Get(prefix)
201+
asserter.EqualValues(value, v.V)
216202
})
217203

218204
}
@@ -369,3 +355,142 @@ func TestPayload(tt *testing.T) {
369355
asserter.EqualValues(time.Time{}, pyl.Expiry())
370356
})
371357
}
358+
359+
func TestErrWatcher(tt *testing.T) {
360+
var (
361+
prefix = "prefix"
362+
value = "value"
363+
requirer = require.New(tt)
364+
asserter = require.New(tt)
365+
)
366+
367+
tt.Run("err watcher", func(t *testing.T) {
368+
forcedErr := fmt.Errorf("forced error")
369+
ranUpdater := atomic.Bool{}
370+
ranErrWatcher := atomic.Bool{}
371+
372+
cache, err := New(Config[string, any]{
373+
LRUCacheSize: 10000,
374+
CacheAge: time.Minute,
375+
Threshold: time.Second * 59,
376+
DisableCache: false,
377+
Updater: func(ctx context.Context, key string) (any, error) {
378+
ranUpdater.Store(true)
379+
return nil, forcedErr
380+
},
381+
ErrWatcher: func(watcherErr error) {
382+
ranErrWatcher.Store(true)
383+
asserter.ErrorIs(watcherErr, forcedErr)
384+
},
385+
})
386+
requirer.NoError(err)
387+
388+
_ = cache.BulkAdd([]Tuple[string, any]{{Key: prefix, Value: value}})
389+
// wait for threshold window
390+
time.Sleep(time.Second)
391+
// trigger auto update within threshold window
392+
_ = cache.Get(prefix)
393+
394+
// wait for the updater callback to be executed
395+
time.Sleep(time.Second * 2)
396+
asserter.True(ranUpdater.Load())
397+
asserter.True(ranErrWatcher.Load())
398+
})
399+
400+
tt.Run("no err watcher", func(t *testing.T) {
401+
forcedErr := fmt.Errorf("forced error")
402+
ranUpdater := atomic.Bool{}
403+
ranErrWatcher := atomic.Bool{}
404+
405+
cache, err := New(Config[string, any]{
406+
LRUCacheSize: 10000,
407+
CacheAge: time.Minute,
408+
Threshold: time.Second * 59,
409+
DisableCache: false,
410+
Updater: func(ctx context.Context, key string) (any, error) {
411+
ranUpdater.Store(true)
412+
return nil, forcedErr
413+
},
414+
})
415+
requirer.NoError(err)
416+
417+
_ = cache.BulkAdd([]Tuple[string, any]{{Key: prefix, Value: value}})
418+
// wait for threshold window
419+
time.Sleep(time.Second)
420+
// trigger auto update within threshold window
421+
_ = cache.Get(prefix)
422+
423+
// wait for the updater callback to be executed
424+
time.Sleep(time.Second * 2)
425+
asserter.True(ranUpdater.Load())
426+
asserter.False(ranErrWatcher.Load())
427+
})
428+
429+
tt.Run("err watcher: catch panic text", func(t *testing.T) {
430+
ranUpdater := atomic.Bool{}
431+
ranErrWatcher := atomic.Bool{}
432+
433+
cache, err := New(Config[string, any]{
434+
LRUCacheSize: 10000,
435+
CacheAge: time.Minute,
436+
Threshold: time.Second * 59,
437+
DisableCache: false,
438+
Updater: func(ctx context.Context, key string) (any, error) {
439+
ranUpdater.Store(true)
440+
panic("force panicked")
441+
},
442+
ErrWatcher: func(watcherErr error) {
443+
ranErrWatcher.Store(true)
444+
asserter.ErrorContains(watcherErr, "force panicked")
445+
},
446+
})
447+
requirer.NoError(err)
448+
cache.Add(prefix, value)
449+
450+
// wait for threshold window
451+
time.Sleep(time.Second)
452+
// trigger auto update within threshold window
453+
_ = cache.Get(prefix)
454+
455+
// wait for the updater callback to be executed
456+
time.Sleep(time.Second * 2)
457+
asserter.True(ranUpdater.Load())
458+
asserter.True(ranErrWatcher.Load())
459+
460+
})
461+
462+
tt.Run("err watcher: catch panic err", func(t *testing.T) {
463+
ranUpdater := atomic.Bool{}
464+
ranErrWatcher := atomic.Bool{}
465+
ErrPanic := errors.New("panic err")
466+
467+
cache, err := New(Config[string, any]{
468+
LRUCacheSize: 10000,
469+
CacheAge: time.Minute,
470+
Threshold: time.Second * 59,
471+
DisableCache: false,
472+
Updater: func(ctx context.Context, key string) (any, error) {
473+
ranUpdater.Store(true)
474+
panic(ErrPanic)
475+
},
476+
ErrWatcher: func(watcherErr error) {
477+
ranErrWatcher.Store(true)
478+
asserter.ErrorIs(watcherErr, ErrPanic)
479+
},
480+
})
481+
requirer.NoError(err)
482+
cache.Add(prefix, value)
483+
484+
// wait for threshold window
485+
time.Sleep(time.Second)
486+
// trigger auto update within threshold window
487+
_ = cache.Get(prefix)
488+
489+
// wait for the updater callback to be executed
490+
time.Sleep(time.Second * 2)
491+
asserter.True(ranUpdater.Load())
492+
asserter.True(ranErrWatcher.Load())
493+
494+
})
495+
496+
}

0 commit comments

Comments
 (0)