Skip to content

Commit

Permalink
feat(storage): implement cached storage
Browse files Browse the repository at this point in the history
  • Loading branch information
eklmv committed Jan 24, 2024
1 parent a78bb66 commit 29a7b52
Show file tree
Hide file tree
Showing 4 changed files with 563 additions and 0 deletions.
80 changes: 80 additions & 0 deletions internal/storage/cachedstorage.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package storage

import (
"log/slog"
"time"

"github.com/eklmv/pdfcertificates/internal/cache"
)

type CachedStorage struct {
Storage
c cache.Cache[uint32, certFile]
}

type certFile struct {
file []byte
timestamp time.Time
size uint64
}

func (cf certFile) Size() uint64 {
return cf.size
}

func NewCachedStorage(storage Storage) *CachedStorage {
c := cache.NewSafeCache(cache.NewLRUCache[uint32, certFile](0))
return &CachedStorage{Storage: storage, c: c}
}

func (cs *CachedStorage) Add(id string, cert []byte, timestamp time.Time) error {
err := cs.Storage.Add(id, cert, timestamp)
if err == nil {
hash := cache.HashString(id)
cf := certFile{
file: cert,
timestamp: timestamp,
}
cf.size = cache.SizeOf(cf)
cs.c.Add(hash, cf)
}
return err
}

func (cs *CachedStorage) Get(id string, timestamp time.Time) (cert []byte, err error) {
hash := cache.HashString(id)
cf, ok := cs.c.Peek(hash)
if ok && (cf.timestamp.Equal(timestamp) || cf.timestamp.After(timestamp)) {
cs.c.Touch(hash)
slog.Info("sucessful storage cache hit", slog.String("id", id), slog.Time("timestamp", timestamp))
return cf.file, nil
}
cert, err = cs.Storage.Get(id, timestamp)
if err == nil {
cf := certFile{
file: cert,
timestamp: timestamp,
}
cf.size = cache.SizeOf(cf)
cs.c.Add(hash, cf)
}
return
}

func (cs *CachedStorage) Delete(id string, timestamp time.Time) {
hash := cache.HashString(id)
cf, ok := cs.c.Peek(hash)
if ok && (cf.timestamp.Equal(timestamp) || cf.timestamp.Before(timestamp)) {
cs.c.Remove(hash)
}
cs.Storage.Delete(id, timestamp)
}

func (cs *CachedStorage) Exists(id string, timestamp time.Time) bool {
hash := cache.HashString(id)
cf, ok := cs.c.Peek(hash)
if ok && (cf.timestamp.Equal(timestamp) || cf.timestamp.After(timestamp)) {
return true
}
return cs.Storage.Exists(id, timestamp)
}
211 changes: 211 additions & 0 deletions internal/storage/cachedstorage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
package storage

import (
"fmt"
"testing"
"time"

"github.com/eklmv/pdfcertificates/internal/cache"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCachedStorageImplementsInterface(t *testing.T) {
assert.Implements(t, (*Storage)(nil), &CachedStorage{})
}

func TestCachedStorageAdd(t *testing.T) {
t.Run("should cache certificate if underlying Add was successful", func(t *testing.T) {
id := "00000000"
cert := []byte("Hello, world!")
timestamp := time.Now()
m := NewMockStorage(t)
cs := NewCachedStorage(m)

m.EXPECT().Add(id, cert, timestamp).Return(nil).Once()

err := cs.Add(id, cert, timestamp)

require.NoError(t, err)
assert.Equal(t, uint64(1), cs.c.Len())
assert.Equal(t, cache.HashString(id), cs.c.Keys()[0])
assert.Equal(t, cert, cs.c.Values()[0].file)
m.AssertExpectations(t)
})
t.Run("should not affect cache if underlying Add failed", func(t *testing.T) {
id := "00000000"
cert := []byte("Hello, world!")
timestamp := time.Now()
m := NewMockStorage(t)
cs := NewCachedStorage(m)

m.EXPECT().Add(id, cert, timestamp).Return(fmt.Errorf("failed")).Once()

err := cs.Add(id, cert, timestamp)

require.Error(t, err)
assert.Empty(t, cs.c.Len())
assert.Empty(t, cs.c.Size())
assert.Empty(t, cs.c.Keys())
m.AssertExpectations(t)
})
}

func TestCachedStorageGet(t *testing.T) {
t.Run("return cached file if requested timestamp same or older, avoid underlying call", func(t *testing.T) {
id := "00000000"
exp := []byte("Hello, world!")
timestamp := time.Now()
older_timestamp := timestamp.Add(-1 * time.Hour)
m := NewMockStorage(t)
cs := NewCachedStorage(m)
m.EXPECT().Add(id, exp, timestamp).Return(nil).Once()
err := cs.Add(id, exp, timestamp)
require.NoError(t, err)

got, err := cs.Get(id, timestamp)

require.NoError(t, err)
assert.Equal(t, exp, got)

got, err = cs.Get(id, older_timestamp)

require.NoError(t, err)
assert.Equal(t, exp, got)
m.AssertExpectations(t)
})
t.Run("if requested timestamp is newer than stored make underlying call and cache result", func(t *testing.T) {
id := "00000000"
cert := []byte("Hello, world!")
exp := []byte("Newer certificate")
timestamp := time.Now()
newer_timestamp := timestamp.Add(1 * time.Hour)
m := NewMockStorage(t)
cs := NewCachedStorage(m)
m.EXPECT().Add(id, cert, timestamp).Return(nil).Once()
err := cs.Add(id, cert, timestamp)
require.NoError(t, err)

m.EXPECT().Get(id, newer_timestamp).Return(exp, nil).Once()
got, err := cs.Get(id, newer_timestamp)

require.NoError(t, err)
assert.Equal(t, exp, got)
assert.Equal(t, uint64(1), cs.c.Len())
assert.Equal(t, cache.HashString(id), cs.c.Keys()[0])
assert.Equal(t, exp, cs.c.Values()[0].file)
m.AssertExpectations(t)
})
}

func TestCachedStorageDelete(t *testing.T) {
t.Run("remove cached certificate and make underlying call", func(t *testing.T) {
id := "00000000"
cert := []byte("Hello, world!")
timestamp := time.Now()
m := NewMockStorage(t)
cs := NewCachedStorage(m)
m.EXPECT().Add(id, cert, timestamp).Return(nil).Once()
err := cs.Add(id, cert, timestamp)
require.NoError(t, err)

m.EXPECT().Delete(id, timestamp).Once()
cs.Delete(id, timestamp)

assert.Empty(t, cs.c.Len())
assert.Empty(t, cs.c.Size())
assert.Empty(t, cs.c.Keys())
assert.Empty(t, cs.c.Values())
m.AssertExpectations(t)
})
t.Run("remove cached certificate if requested timestamp newer and make underlying call", func(t *testing.T) {
id := "00000000"
cert := []byte("Hello, world!")
timestamp := time.Now()
newer_timestamp := timestamp.Add(1 * time.Hour)
m := NewMockStorage(t)
cs := NewCachedStorage(m)
m.EXPECT().Add(id, cert, timestamp).Return(nil).Once()
err := cs.Add(id, cert, timestamp)
require.NoError(t, err)

m.EXPECT().Delete(id, newer_timestamp).Once()
cs.Delete(id, newer_timestamp)

assert.Empty(t, cs.c.Len())
assert.Empty(t, cs.c.Size())
assert.Empty(t, cs.c.Keys())
assert.Empty(t, cs.c.Values())
m.AssertExpectations(t)
})
t.Run("do not affect cache if requested timestamp older and make underlying call", func(t *testing.T) {
id := "00000000"
cert := []byte("Hello, world!")
timestamp := time.Now()
older_timestamp := timestamp.Add(-1 * time.Hour)
m := NewMockStorage(t)
cs := NewCachedStorage(m)
m.EXPECT().Add(id, cert, timestamp).Return(nil).Once()
err := cs.Add(id, cert, timestamp)
require.NoError(t, err)

m.EXPECT().Delete(id, older_timestamp).Once()
cs.Delete(id, older_timestamp)

assert.Equal(t, uint64(1), cs.c.Len())
assert.Equal(t, cache.HashString(id), cs.c.Keys()[0])
assert.Equal(t, cert, cs.c.Values()[0].file)
m.AssertExpectations(t)
})
}

func TestCachedStorageExists(t *testing.T) {
t.Run("return true if certificate cached with same or newer timestamp, avoid underlying call", func(t *testing.T) {
id := "00000000"
cert := []byte("Hello, world!")
timestamp := time.Now()
older_timestamp := timestamp.Add(-1 * time.Hour)
m := NewMockStorage(t)
cs := NewCachedStorage(m)
m.EXPECT().Add(id, cert, timestamp).Return(nil).Once()
err := cs.Add(id, cert, timestamp)
require.NoError(t, err)

got := cs.Exists(id, timestamp)
assert.True(t, got)

got = cs.Exists(id, older_timestamp)
assert.True(t, got)

m.AssertExpectations(t)
})
t.Run("if certificate cached with older timestamp make underlying call", func(t *testing.T) {
id := "00000000"
cert := []byte("Hello, world!")
timestamp := time.Now()
newer_timestamp := timestamp.Add(1 * time.Hour)
m := NewMockStorage(t)
cs := NewCachedStorage(m)
m.EXPECT().Add(id, cert, timestamp).Return(nil).Once()
err := cs.Add(id, cert, timestamp)
require.NoError(t, err)

m.EXPECT().Exists(id, newer_timestamp).Return(false).Once()
got := cs.Exists(id, newer_timestamp)

assert.False(t, got)
m.AssertExpectations(t)
})
t.Run("if certificate not cached make underlying call", func(t *testing.T) {
id := "00000000"
timestamp := time.Now()
m := NewMockStorage(t)
cs := NewCachedStorage(m)

m.EXPECT().Exists(id, timestamp).Return(true).Once()
got := cs.Exists(id, timestamp)

assert.True(t, got)
m.AssertExpectations(t)
})
}
Loading

0 comments on commit 29a7b52

Please sign in to comment.