-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(storage): implement cached storage
- Loading branch information
Showing
4 changed files
with
563 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
Oops, something went wrong.