diff --git a/internal/app/pcache/pcache.go b/internal/app/pcache/pcache.go index ccd260a4..7a911d80 100644 --- a/internal/app/pcache/pcache.go +++ b/internal/app/pcache/pcache.go @@ -10,28 +10,35 @@ import ( "github.com/ErikKalkoken/evebuddy/internal/app/storage" ) +// PCache is a persistent cache. It can automatically remove expired items. type PCache struct { st *storage.Storage closeC chan struct{} } +// New returns a new PCache. +// +// cleanUpTimeout is the timeout between automatic clean-up intervals. When set to 0 no cleanUp will be done. +// Make sure to close this object again to free all it's resources. func New(st *storage.Storage, cleanUpTimeout time.Duration) *PCache { c := &PCache{ st: st, closeC: make(chan struct{}), } - ticker := time.NewTicker(cleanUpTimeout) - go func() { - for { - select { - case <-c.closeC: - slog.Info("cache closed") - return - case <-ticker.C: - c.CleanUp() + if cleanUpTimeout > 0 { + ticker := time.NewTicker(cleanUpTimeout) + go func() { + for { + select { + case <-c.closeC: + slog.Info("cache closed") + return + case <-ticker.C: + c.CleanUp() + } } - } - }() + }() + } return c } @@ -49,6 +56,11 @@ func (c *PCache) Clear() { } } +// Close closes the cache and frees allocated resources. +func (c *PCache) Close() { + close(c.closeC) +} + func (c *PCache) Delete(key string) { err := c.st.CacheDelete(context.Background(), key) if err != nil { @@ -64,7 +76,7 @@ func (c *PCache) Exists(key string) bool { return found } -func (c *PCache) Get(key string) (any, bool) { +func (c *PCache) Get(key string) ([]byte, bool) { v, err := c.st.CacheGet(context.Background(), key) if errors.Is(err, storage.ErrNotFound) { return nil, false @@ -76,14 +88,14 @@ func (c *PCache) Get(key string) (any, bool) { return v, true } -func (c *PCache) Set(key string, value any, timeout time.Duration) { +func (c *PCache) Set(key string, value []byte, timeout time.Duration) { var expiresAt time.Time if timeout > 0 { expiresAt = time.Now().Add(timeout) } arg := storage.CacheSetParams{ Key: key, - Value: value.([]byte), + Value: value, ExpiresAt: expiresAt, } err := c.st.CacheSet(context.Background(), arg) diff --git a/internal/app/pcache/pcache_test.go b/internal/app/pcache/pcache_test.go new file mode 100644 index 00000000..afdfb90b --- /dev/null +++ b/internal/app/pcache/pcache_test.go @@ -0,0 +1,30 @@ +package pcache_test + +import ( + "testing" + "time" + + "github.com/ErikKalkoken/evebuddy/internal/app/pcache" + "github.com/ErikKalkoken/evebuddy/internal/app/storage/testutil" + "github.com/stretchr/testify/assert" +) + +func TestPCache(t *testing.T) { + db, st, _ := testutil.New() + defer db.Close() + t.Run("can set and get a cache entry", func(t *testing.T) { + // given + testutil.TruncateTables(db) + c := pcache.New(st, 0) + defer c.Close() + key := "key" + value := []byte("value") + // when + c.Set(key, value, time.Minute) + // then + x, found := c.Get(key) + if assert.True(t, found) { + assert.Equal(t, value, x) + } + }) +} diff --git a/internal/eveimage/eveimage.go b/internal/eveimage/eveimage.go index 33906751..101fec4e 100644 --- a/internal/eveimage/eveimage.go +++ b/internal/eveimage/eveimage.go @@ -31,8 +31,8 @@ var ( // Defines a cache service type CacheService interface { Clear() - Get(string) (any, bool) - Set(string, any, time.Duration) + Get(string) ([]byte, bool) + Set(string, []byte, time.Duration) } // EveImageService provides cached access to images on the Eve Online image server. @@ -170,7 +170,8 @@ func (m *EveImageService) InventoryTypeSKIN(id int32, size int) (fyne.Resource, func (m *EveImageService) image(url string, timeout time.Duration) (fyne.Resource, error) { key := "eveimage-" + makeMD5Hash(url) var dat []byte - x, found := m.cache.Get(key) + var found bool + dat, found = m.cache.Get(key) if !found { if m.isOffline { return resourceBrokenimageSvg, nil @@ -187,12 +188,6 @@ func (m *EveImageService) image(url string, timeout time.Duration) (fyne.Resourc return nil, err } dat = x.([]byte) - } else { - var ok bool - dat, ok = x.([]byte) - if !ok { - return resourceBrokenimageSvg, nil - } } r := fyne.NewStaticResource(key, dat) return r, nil diff --git a/internal/eveimage/eveimage_internal_test.go b/internal/eveimage/eveimage_internal_test.go index 72ff0702..d0974854 100644 --- a/internal/eveimage/eveimage_internal_test.go +++ b/internal/eveimage/eveimage_internal_test.go @@ -5,8 +5,8 @@ import ( "net/http" "os" "testing" + "time" - "github.com/ErikKalkoken/evebuddy/internal/cache" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" ) @@ -54,8 +54,29 @@ func TestLoadResourceFromURL(t *testing.T) { }) } +type cache map[string][]byte + +func newCache() cache { + return make(cache) +} + +func (c cache) Get(k string) ([]byte, bool) { + v, ok := c[k] + return v, ok +} + +func (c cache) Set(k string, v []byte, d time.Duration) { + c[k] = v +} + +func (c cache) Clear() { + for k := range c { + delete(c, k) + } +} + func TestImageFetching(t *testing.T) { - c := cache.New() + c := newCache() httpmock.Activate() defer httpmock.DeactivateAndReset() dat, err := os.ReadFile("testdata/character_93330670_64.jpeg") diff --git a/internal/eveimage/eveimage_test.go b/internal/eveimage/eveimage_test.go index 4b8b6df3..d33215ad 100644 --- a/internal/eveimage/eveimage_test.go +++ b/internal/eveimage/eveimage_test.go @@ -4,15 +4,35 @@ import ( "net/http" "os" "testing" + "time" - "github.com/ErikKalkoken/evebuddy/internal/cache" "github.com/ErikKalkoken/evebuddy/internal/eveimage" "github.com/jarcoal/httpmock" "github.com/stretchr/testify/assert" ) +type cache map[string][]byte + +func newCache() cache { + return make(cache) +} + +func (c cache) Get(k string) ([]byte, bool) { + v, ok := c[k] + return v, ok +} + +func (c cache) Set(k string, v []byte, d time.Duration) { + c[k] = v +} + +func (c cache) Clear() { + for k := range c { + delete(c, k) + } +} + func TestImageFetching(t *testing.T) { - c := cache.New() httpmock.Activate() defer httpmock.DeactivateAndReset() dat, err := os.ReadFile("testdata/character_93330670_64.jpeg") @@ -21,7 +41,7 @@ func TestImageFetching(t *testing.T) { } t.Run("can fetch an alliance logo from the image server", func(t *testing.T) { // given - c.Clear() + c := newCache() httpmock.Reset() httpmock.RegisterResponder( "GET", @@ -37,6 +57,7 @@ func TestImageFetching(t *testing.T) { }) t.Run("can fetch a character portrait from the image server", func(t *testing.T) { // given + c := newCache() httpmock.Reset() httpmock.RegisterResponder( "GET", @@ -52,7 +73,7 @@ func TestImageFetching(t *testing.T) { }) t.Run("can fetch a corporation logo from the image server", func(t *testing.T) { // given - c.Clear() + c := newCache() httpmock.Reset() httpmock.RegisterResponder( "GET", @@ -68,6 +89,7 @@ func TestImageFetching(t *testing.T) { }) t.Run("can fetch a faction logo from the image server", func(t *testing.T) { // given + c := newCache() httpmock.Reset() httpmock.RegisterResponder( "GET", @@ -83,7 +105,7 @@ func TestImageFetching(t *testing.T) { }) t.Run("can fetch a type icon from the image server", func(t *testing.T) { // given - c.Clear() + c := newCache() httpmock.Reset() httpmock.RegisterResponder( "GET", @@ -99,7 +121,7 @@ func TestImageFetching(t *testing.T) { }) t.Run("can fetch a type render from the image server", func(t *testing.T) { // given - c.Clear() + c := newCache() httpmock.Reset() httpmock.RegisterResponder( "GET", @@ -115,7 +137,7 @@ func TestImageFetching(t *testing.T) { }) t.Run("can fetch a type BPO from the image server", func(t *testing.T) { // given - c.Clear() + c := newCache() httpmock.Reset() httpmock.RegisterResponder( "GET", @@ -131,7 +153,7 @@ func TestImageFetching(t *testing.T) { }) t.Run("can fetch a type BPC from the image server", func(t *testing.T) { // given - c.Clear() + c := newCache() httpmock.Reset() httpmock.RegisterResponder( "GET", @@ -147,7 +169,7 @@ func TestImageFetching(t *testing.T) { }) t.Run("should convert images size errors", func(t *testing.T) { // given - c.Clear() + c := newCache() httpmock.Reset() httpmock.RegisterResponder( "GET", @@ -161,7 +183,7 @@ func TestImageFetching(t *testing.T) { }) t.Run("can clear cache", func(t *testing.T) { // given - c.Clear() + c := newCache() httpmock.Reset() httpmock.RegisterResponder( "GET",