tests: cache

This commit is contained in:
Felipe M 2025-03-23 23:37:26 +01:00
parent 92732f9682
commit 597feb7b54
Signed by: fmartingr
GPG key ID: CCFBC5637D4000A8
8 changed files with 822 additions and 32 deletions

86
cache/file.go vendored
View file

@ -1,6 +1,7 @@
package cache package cache
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -17,7 +18,23 @@ type FileCache struct {
} }
func (c *FileCache) Get(key string) (result any, err error) { func (c *FileCache) Get(key string) (result any, err error) {
path := filepath.Join(c.path, key) path := c.getPathForItem(key)
if _, err := os.Stat(path); os.IsNotExist(err) {
return result, model.ErrCacheKeyDontExist
}
metadata, err := c.getMetadata(key)
if err != nil {
return result, fmt.Errorf("error getting metadata: %s", err)
}
if metadata.TTL != nil && metadata.TTL.Before(time.Now()) {
if err := c.Delete(key); err != nil {
return result, fmt.Errorf("error deleting cache key: %s", err)
}
return result, model.ErrCacheKeyDontExist
}
contents, err := os.ReadFile(path) contents, err := os.ReadFile(path)
if err != nil { if err != nil {
@ -30,23 +47,20 @@ func (c *FileCache) Get(key string) (result any, err error) {
return contents, nil return contents, nil
} }
func (c *FileCache) GetWithExpiry(key string, expiration time.Duration) (result any, err error) { func (c *FileCache) Set(key string, value any, opts ...model.CacheOption) error {
path := filepath.Join(c.path, key) path := c.getPathForItem(key)
info, err := os.Stat(path)
if err != nil { metadata := model.CacheItem{
return result, model.ErrCacheKeyDontExist Key: key,
} }
if info.ModTime().Add(expiration).Before(time.Now()) { for _, opt := range opts {
c.Delete(key) opt(&metadata)
return result, model.ErrCacheKeyDontExist
} }
return c.Get(key) if err := c.setMetadata(key, metadata); err != nil {
} return fmt.Errorf("error setting metadata: %s", err)
}
func (c *FileCache) Set(key string, value any) error {
path := filepath.Join(c.path, key)
if err := os.WriteFile(path, value.([]byte), 0766); err != nil { if err := os.WriteFile(path, value.([]byte), 0766); err != nil {
return fmt.Errorf("error writting cache file: %s", err) return fmt.Errorf("error writting cache file: %s", err)
@ -56,8 +70,48 @@ func (c *FileCache) Set(key string, value any) error {
} }
func (c *FileCache) Delete(key string) error { func (c *FileCache) Delete(key string) error {
path := filepath.Join(c.path, key) path := c.getPathForItem(key)
return os.Remove(path)
if err := os.Remove(path); err != nil {
return fmt.Errorf("error deleting cache file: %s", err)
}
if err := os.Remove(path + ".metadata"); err != nil {
return fmt.Errorf("error deleting cache metadata: %s", err)
}
return nil
}
func (c *FileCache) getPathForItem(key string) string {
return filepath.Join(c.path, key)
}
func (c *FileCache) getMetadata(key string) (model.CacheItem, error) {
path := c.getPathForItem(key) + ".metadata"
contents, err := os.ReadFile(path)
if err != nil {
return model.CacheItem{}, fmt.Errorf("error reading cache file: %s", err)
}
var metadata model.CacheItem
if err := json.Unmarshal(contents, &metadata); err != nil {
return model.CacheItem{}, fmt.Errorf("error unmarshalling cache file: %s", err)
}
return metadata, nil
}
func (c *FileCache) setMetadata(key string, metadata model.CacheItem) error {
path := c.getPathForItem(key) + ".metadata"
contents, err := json.Marshal(metadata)
if err != nil {
return fmt.Errorf("error marshalling cache file: %s", err)
}
return os.WriteFile(path, contents, 0644)
} }
func NewFileCache(folderName string) (*FileCache, error) { func NewFileCache(folderName string) (*FileCache, error) {

412
cache/file_test.go vendored Normal file
View file

@ -0,0 +1,412 @@
package cache
import (
"os"
"path/filepath"
"testing"
"time"
"git.nakama.town/fmartingr/gotoolkit/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewFileCache(t *testing.T) {
// Create a test folder name
testFolderName := "test-cache"
// Test initialization with the temp directory
cache, err := NewFileCache(testFolderName)
require.NoError(t, err, "NewFileCache should not return an error")
require.NotNil(t, cache, "NewFileCache should return a non-nil cache")
// Verify the directory was created (not checking the exact path as it depends on the os.UserCacheDir output)
dirInfo, err := os.Stat(cache.path)
require.NoError(t, err, "Cache directory should exist")
assert.True(t, dirInfo.IsDir(), "Cache path should be a directory")
}
func TestFileCache_Set(t *testing.T) {
// Create a temporary directory for testing
tempDir := t.TempDir()
cache := setupTestFileCache(t, tempDir)
// Test simple set operation
testKey := "test-key"
testValue := []byte("test-value")
err := cache.Set(testKey, testValue)
require.NoError(t, err, "Set should not return an error")
// Verify the file exists
filePath := filepath.Join(cache.path, testKey)
assert.FileExists(t, filePath, "Cache file should exist")
assert.FileExists(t, filePath+".metadata", "Cache metadata file should exist")
// Read the file contents
content, err := os.ReadFile(filePath)
require.NoError(t, err, "Should be able to read cache file")
assert.Equal(t, testValue, content, "File content should match set value")
// Test set with TTL
ttlKey := "ttl-key"
ttlValue := []byte("ttl-value")
ttlDuration := 1 * time.Hour
withTTL := func(item *model.CacheItem) {
expiry := time.Now().Add(ttlDuration)
item.TTL = &expiry
}
err = cache.Set(ttlKey, ttlValue, withTTL)
require.NoError(t, err, "Set with TTL should not return an error")
// Verify file exists
ttlFilePath := filepath.Join(cache.path, ttlKey)
assert.FileExists(t, ttlFilePath, "Cache file with TTL should exist")
assert.FileExists(t, ttlFilePath+".metadata", "Cache metadata file with TTL should exist")
// Verify the metadata has TTL set
metadata, err := cache.getMetadata(ttlKey)
require.NoError(t, err, "Should be able to get metadata")
assert.NotNil(t, metadata.TTL, "TTL should be set in metadata")
}
func TestFileCache_SetTableDriven(t *testing.T) {
tempDir := t.TempDir()
tests := []struct {
name string
key string
value []byte
withTTL bool
ttlDelta time.Duration
}{
{
name: "basic set without TTL",
key: "key1",
value: []byte("value1"),
},
{
name: "set with future TTL",
key: "key2",
value: []byte("value2"),
withTTL: true,
ttlDelta: 1 * time.Hour, // Future time
},
{
name: "set with past TTL",
key: "key3",
value: []byte("value3"),
withTTL: true,
ttlDelta: -1 * time.Hour, // Past time
},
{
name: "set with empty value",
key: "key4",
value: []byte{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cache := setupTestFileCache(t, tempDir)
var opts []model.CacheOption
if tt.withTTL {
opts = append(opts, func(item *model.CacheItem) {
expiry := time.Now().Add(tt.ttlDelta)
item.TTL = &expiry
})
}
err := cache.Set(tt.key, tt.value, opts...)
require.NoError(t, err, "Set should not return an error")
// Verify file and metadata exist
filePath := filepath.Join(cache.path, tt.key)
assert.FileExists(t, filePath, "Cache file should exist")
assert.FileExists(t, filePath+".metadata", "Cache metadata file should exist")
// Verify file content
content, err := os.ReadFile(filePath)
require.NoError(t, err, "Should be able to read cache file")
assert.Equal(t, tt.value, content, "File content should match set value")
// Verify metadata
metadata, err := cache.getMetadata(tt.key)
require.NoError(t, err, "Should be able to get metadata")
assert.Equal(t, tt.key, metadata.Key, "Metadata key should match")
if tt.withTTL {
assert.NotNil(t, metadata.TTL, "TTL should be set in metadata")
expectedTime := time.Now().Add(tt.ttlDelta)
assert.WithinDuration(t, expectedTime, *metadata.TTL, 2*time.Second, "TTL should be close to expected value")
} else {
assert.Nil(t, metadata.TTL, "TTL should not be set for items without TTL")
}
})
}
}
func TestFileCache_Get(t *testing.T) {
// Create a temporary directory for testing
tempDir := t.TempDir()
cache := setupTestFileCache(t, tempDir)
// Set up test data
validKey := "valid-key"
validValue := []byte("valid-value")
err := cache.Set(validKey, validValue)
require.NoError(t, err, "Set should not return an error for setup")
// Test getting valid key
retrievedValue, err := cache.Get(validKey)
require.NoError(t, err, "Get should not return an error for valid key")
assert.Equal(t, validValue, retrievedValue, "Retrieved value should match original")
// Test getting non-existent key
_, err = cache.Get("nonexistent-key")
assert.ErrorIs(t, err, model.ErrCacheKeyDontExist, "Get should return ErrCacheKeyDontExist for non-existent key")
// Test getting expired key
expiredKey := "expired-key"
expiredValue := []byte("expired-value")
expiredTTL := func(item *model.CacheItem) {
expiry := time.Now().Add(-1 * time.Minute) // Past time
item.TTL = &expiry
}
err = cache.Set(expiredKey, expiredValue, expiredTTL)
require.NoError(t, err, "Set should not return an error for expired key setup")
_, err = cache.Get(expiredKey)
assert.ErrorIs(t, err, model.ErrCacheKeyDontExist, "Get should return ErrCacheKeyDontExist for expired key")
// Verify the expired files are deleted
expiredFilePath := filepath.Join(cache.path, expiredKey)
expiredMetadataPath := expiredFilePath + ".metadata"
assert.NoFileExists(t, expiredFilePath, "Expired cache file should be deleted")
assert.NoFileExists(t, expiredMetadataPath, "Expired cache metadata file should be deleted")
}
func TestFileCache_GetTableDriven(t *testing.T) {
tempDir := t.TempDir()
tests := []struct {
name string
setupKeys map[string][]byte
setupTTLs map[string]time.Duration
getKey string
expectValue []byte
expectError error
checkFileDeleted bool
}{
{
name: "get existing key",
setupKeys: map[string][]byte{
"test-key": []byte("test-value"),
},
getKey: "test-key",
expectValue: []byte("test-value"),
expectError: nil,
},
{
name: "get non-existent key",
setupKeys: map[string][]byte{
"test-key": []byte("test-value"),
},
getKey: "nonexistent-key",
expectValue: nil,
expectError: model.ErrCacheKeyDontExist,
},
{
name: "get expired key",
setupKeys: map[string][]byte{
"expired-key": []byte("expired-value"),
},
setupTTLs: map[string]time.Duration{
"expired-key": -1 * time.Minute, // Past time
},
getKey: "expired-key",
expectValue: nil,
expectError: model.ErrCacheKeyDontExist,
checkFileDeleted: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cache := setupTestFileCache(t, tempDir)
// Setup test data
for key, value := range tt.setupKeys {
var opts []model.CacheOption
if ttl, ok := tt.setupTTLs[key]; ok {
opts = append(opts, func(item *model.CacheItem) {
expiry := time.Now().Add(ttl)
item.TTL = &expiry
})
}
err := cache.Set(key, value, opts...)
require.NoError(t, err, "Setup: Set should not return an error")
}
// Test get
value, err := cache.Get(tt.getKey)
if tt.expectError != nil {
assert.ErrorIs(t, err, tt.expectError)
assert.Nil(t, value)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.expectValue, value)
}
// Check if expired files are deleted
if tt.checkFileDeleted {
filePath := filepath.Join(cache.path, tt.getKey)
metadataPath := filePath + ".metadata"
assert.NoFileExists(t, filePath, "Expired cache file should be deleted")
assert.NoFileExists(t, metadataPath, "Expired cache metadata file should be deleted")
}
})
}
}
func TestFileCache_Delete(t *testing.T) {
// Create a temporary directory for testing
tempDir := t.TempDir()
cache := setupTestFileCache(t, tempDir)
// Set up test data
testKey := "delete-key"
testValue := []byte("delete-value")
err := cache.Set(testKey, testValue)
require.NoError(t, err, "Set should not return an error for setup")
// Verify files exist before delete
filePath := filepath.Join(cache.path, testKey)
metadataPath := filePath + ".metadata"
assert.FileExists(t, filePath, "Cache file should exist before delete")
assert.FileExists(t, metadataPath, "Cache metadata file should exist before delete")
// Test delete
err = cache.Delete(testKey)
require.NoError(t, err, "Delete should not return an error")
// Verify files are deleted
assert.NoFileExists(t, filePath, "Cache file should be deleted")
assert.NoFileExists(t, metadataPath, "Cache metadata file should be deleted")
// Test deleting non-existent key
err = cache.Delete("nonexistent-key")
assert.Error(t, err, "Delete should return an error for non-existent key")
}
func TestFileCache_DeleteTableDriven(t *testing.T) {
tempDir := t.TempDir()
tests := []struct {
name string
setupKey string
setupValue []byte
deleteKey string
expectError bool
checkNotExist bool
}{
{
name: "delete existing key",
setupKey: "existing-key",
setupValue: []byte("existing-value"),
deleteKey: "existing-key",
expectError: false,
checkNotExist: true,
},
{
name: "delete non-existent key",
deleteKey: "nonexistent-key",
expectError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cache := setupTestFileCache(t, tempDir)
// Setup test data if needed
if tt.setupKey != "" {
err := cache.Set(tt.setupKey, tt.setupValue)
require.NoError(t, err, "Setup: Set should not return an error")
// Verify files exist before delete
filePath := filepath.Join(cache.path, tt.setupKey)
metadataPath := filePath + ".metadata"
assert.FileExists(t, filePath, "Cache file should exist before delete")
assert.FileExists(t, metadataPath, "Cache metadata file should exist before delete")
}
// Test delete
err := cache.Delete(tt.deleteKey)
if tt.expectError {
assert.Error(t, err, "Delete should return an error")
} else {
assert.NoError(t, err, "Delete should not return an error")
}
// Check files are deleted if needed
if tt.checkNotExist {
filePath := filepath.Join(cache.path, tt.deleteKey)
metadataPath := filePath + ".metadata"
assert.NoFileExists(t, filePath, "Cache file should be deleted")
assert.NoFileExists(t, metadataPath, "Cache metadata file should be deleted")
}
})
}
}
func TestFileCache_GetPathForItem(t *testing.T) {
tempDir := t.TempDir()
cache := setupTestFileCache(t, tempDir)
key := "test-key"
expectedPath := filepath.Join(cache.path, key)
actualPath := cache.getPathForItem(key)
assert.Equal(t, expectedPath, actualPath, "Path for item should match expected format")
}
func TestFileCache_GetSetMetadata(t *testing.T) {
tempDir := t.TempDir()
cache := setupTestFileCache(t, tempDir)
// Create test metadata
key := "metadata-key"
now := time.Now()
metadata := model.CacheItem{
Key: key,
Value: nil,
TTL: &now,
}
// Test setMetadata
err := cache.setMetadata(key, metadata)
require.NoError(t, err, "setMetadata should not return an error")
// Verify metadata file exists
metadataPath := filepath.Join(cache.path, key) + ".metadata"
assert.FileExists(t, metadataPath, "Metadata file should exist")
// Test getMetadata
retrievedMetadata, err := cache.getMetadata(key)
require.NoError(t, err, "getMetadata should not return an error")
assert.Equal(t, key, retrievedMetadata.Key, "Retrieved metadata key should match original")
assert.WithinDuration(t, now, *retrievedMetadata.TTL, time.Second, "Retrieved metadata TTL should be close to original")
}
// Helper function to create a FileCache for testing
func setupTestFileCache(t *testing.T, tempDir string) *FileCache {
return &FileCache{
path: tempDir,
}
}

37
cache/memory.go vendored
View file

@ -10,30 +10,44 @@ import (
var _ model.Cache = (*MemoryCache)(nil) var _ model.Cache = (*MemoryCache)(nil)
type MemoryCache struct { type MemoryCache struct {
data map[string]any data map[string]model.CacheItem
dataMu sync.RWMutex dataMu sync.RWMutex
} }
func (c *MemoryCache) Get(key string) (result any, err error) { func (c *MemoryCache) Get(key string) (result any, err error) {
c.dataMu.RLock() c.dataMu.RLock()
defer c.dataMu.RUnlock()
result, exists := c.data[key] item, exists := c.data[key]
if !exists { if !exists {
c.dataMu.RUnlock()
return result, model.ErrCacheKeyDontExist return result, model.ErrCacheKeyDontExist
} }
return // Check expiration while still holding the lock
if item.TTL != nil && item.TTL.Before(time.Now()) {
c.dataMu.RUnlock()
_ = c.Delete(key)
return result, model.ErrCacheKeyDontExist
}
value := item.Value
c.dataMu.RUnlock()
return value, nil
} }
func (c *MemoryCache) GetWithExpiry(key string, expiration time.Duration) (any, error) { func (c *MemoryCache) Set(key string, value any, opts ...model.CacheOption) error {
// TODO: Implement expiration in memory cache. item := model.CacheItem{
return c.Get(key) Key: key,
} Value: value,
}
for _, opt := range opts {
opt(&item)
}
func (c *MemoryCache) Set(key string, value any) error {
c.dataMu.Lock() c.dataMu.Lock()
c.data[key] = value c.data[key] = item
c.dataMu.Unlock() c.dataMu.Unlock()
return nil return nil
@ -48,6 +62,7 @@ func (c *MemoryCache) Delete(key string) error {
func NewMemoryCache() *MemoryCache { func NewMemoryCache() *MemoryCache {
return &MemoryCache{ return &MemoryCache{
data: make(map[string]any), data: make(map[string]model.CacheItem),
dataMu: sync.RWMutex{},
} }
} }

285
cache/memory_test.go vendored Normal file
View file

@ -0,0 +1,285 @@
package cache
import (
"sync"
"testing"
"time"
"git.nakama.town/fmartingr/gotoolkit/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewMemoryCache(t *testing.T) {
cache := NewMemoryCache()
require.NotNil(t, cache, "NewMemoryCache returned nil")
require.NotNil(t, cache.data, "Cache data map was not initialized")
}
func TestMemoryCache_Set(t *testing.T) {
cache := NewMemoryCache()
// Test basic set
err := cache.Set("key1", "value1")
require.NoError(t, err, "Set returned unexpected error")
// Verify the item was stored correctly
cache.dataMu.RLock()
item, exists := cache.data["key1"]
cache.dataMu.RUnlock()
require.True(t, exists, "Set did not store the item in the cache")
assert.Equal(t, "key1", item.Key, "Wrong key stored")
assert.Equal(t, "value1", item.Value, "Wrong value stored")
assert.Nil(t, item.TTL, "TTL should be nil")
// Test set with TTL
withTTL := func(item *model.CacheItem) {
expiry := time.Now().Add(10 * time.Minute)
item.TTL = &expiry
}
err = cache.Set("key2", "value2", withTTL)
require.NoError(t, err, "Set with TTL returned unexpected error")
// Verify the item with TTL was stored correctly
cache.dataMu.RLock()
itemWithTTL, exists := cache.data["key2"]
cache.dataMu.RUnlock()
require.True(t, exists, "Set did not store the item with TTL in the cache")
assert.NotNil(t, itemWithTTL.TTL, "TTL option was not applied")
}
func TestMemoryCache_SetTableDriven(t *testing.T) {
tests := []struct {
name string
key string
value interface{}
ttl *time.Duration
}{
{
name: "string value without TTL",
key: "string-key",
value: "string-value",
ttl: nil,
},
{
name: "int value without TTL",
key: "int-key",
value: 42,
ttl: nil,
},
{
name: "struct value without TTL",
key: "struct-key",
value: struct{ Name string }{"test"},
ttl: nil,
},
{
name: "string value with TTL",
key: "string-key-ttl",
value: "string-value-ttl",
ttl: ptrDuration(5 * time.Minute),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cache := NewMemoryCache()
var opts []model.CacheOption
if tt.ttl != nil {
opts = append(opts, func(item *model.CacheItem) {
expiry := time.Now().Add(*tt.ttl)
item.TTL = &expiry
})
}
err := cache.Set(tt.key, tt.value, opts...)
assert.NoError(t, err)
cache.dataMu.RLock()
item, exists := cache.data[tt.key]
cache.dataMu.RUnlock()
assert.True(t, exists)
assert.Equal(t, tt.key, item.Key)
assert.Equal(t, tt.value, item.Value)
if tt.ttl != nil {
assert.NotNil(t, item.TTL)
} else {
assert.Nil(t, item.TTL)
}
})
}
}
func TestMemoryCache_Get(t *testing.T) {
cache := NewMemoryCache()
// Test getting non-existent key
_, err := cache.Get("nonexistent")
assert.ErrorIs(t, err, model.ErrCacheKeyDontExist, "Wrong error returned for non-existent key")
// Test getting existing key
require.NoError(t, cache.Set("key1", "value1"))
value, err := cache.Get("key1")
require.NoError(t, err, "Get returned unexpected error")
assert.Equal(t, "value1", value, "Wrong value returned")
// Test getting expired key
expiredTTL := func(item *model.CacheItem) {
expiry := time.Now().Add(-1 * time.Minute) // Past time
item.TTL = &expiry
}
require.NoError(t, cache.Set("expired", "value", expiredTTL))
_, err = cache.Get("expired")
assert.ErrorIs(t, err, model.ErrCacheKeyDontExist, "Wrong error returned for expired item")
// Verify the expired key was deleted
cache.dataMu.RLock()
_, exists := cache.data["expired"]
cache.dataMu.RUnlock()
assert.False(t, exists, "Expired item was not deleted during Get")
}
func TestMemoryCache_GetTableDriven(t *testing.T) {
tests := []struct {
name string
setupKey string
setupValue any
setupTTL *time.Duration
getKey string
expectValue any
expectError error
}{
{
name: "get existing key",
setupKey: "test-key",
setupValue: "test-value",
getKey: "test-key",
expectValue: "test-value",
expectError: nil,
},
{
name: "get non-existent key",
setupKey: "test-key",
setupValue: "test-value",
getKey: "different-key",
expectValue: nil,
expectError: model.ErrCacheKeyDontExist,
},
{
name: "get expired key",
setupKey: "expired-key",
setupValue: "expired-value",
setupTTL: ptrDuration(-1 * time.Minute), // Negative to create an expired key
getKey: "expired-key",
expectValue: nil,
expectError: model.ErrCacheKeyDontExist,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cache := NewMemoryCache()
// Setup
var opts []model.CacheOption
if tt.setupTTL != nil {
opts = append(opts, func(item *model.CacheItem) {
expiry := time.Now().Add(*tt.setupTTL)
item.TTL = &expiry
})
}
if tt.setupKey != "" {
err := cache.Set(tt.setupKey, tt.setupValue, opts...)
require.NoError(t, err)
}
// Test
value, err := cache.Get(tt.getKey)
if tt.expectError != nil {
assert.ErrorIs(t, err, tt.expectError)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.expectValue, value)
// If it was an expired key, ensure it's deleted
if tt.setupTTL != nil && *tt.setupTTL < 0 {
cache.dataMu.RLock()
_, exists := cache.data[tt.setupKey]
cache.dataMu.RUnlock()
assert.False(t, exists, "Expired item was not deleted during Get")
}
})
}
}
func TestMemoryCache_Delete(t *testing.T) {
cache := NewMemoryCache()
// Setup test data
require.NoError(t, cache.Set("key1", "value1"))
// Test deleting existing key
err := cache.Delete("key1")
require.NoError(t, err, "Delete returned unexpected error")
// Verify the key was deleted
cache.dataMu.RLock()
_, exists := cache.data["key1"]
cache.dataMu.RUnlock()
assert.False(t, exists, "Item was not deleted from cache")
// Test deleting non-existent key (should not error)
err = cache.Delete("nonexistent")
assert.NoError(t, err, "Delete on non-existent key returned error")
}
func TestMemoryCache_Concurrency(t *testing.T) {
cache := NewMemoryCache()
const goroutines = 10
const operations = 100
wg := sync.WaitGroup{}
wg.Add(goroutines * 2)
// Writers
for i := range goroutines {
go func(id int) {
for j := range operations {
key := "key" + string(rune('A'+id)) + string(rune('0'+j%10))
err := cache.Set(key, j)
assert.NoError(t, err)
}
wg.Done()
}(i)
}
// Readers
for i := range goroutines {
go func(id int) {
for j := range operations {
key := "key" + string(rune('A'+id)) + string(rune('0'+j%10))
_, _ = cache.Get(key) // We ignore errors here as keys might not exist yet
}
wg.Done()
}(i)
}
// Wait for all goroutines to complete
wg.Wait()
// If we reached here without deadlock or panic, the test passes
}
// Helper function to create duration pointers
func ptrDuration(d time.Duration) *time.Duration {
return &d
}

14
cache/options.go vendored Normal file
View file

@ -0,0 +1,14 @@
package cache
import (
"time"
"git.nakama.town/fmartingr/gotoolkit/model"
)
func WithTTL(ttl time.Duration) model.CacheOption {
return func(item *model.CacheItem) {
exp := time.Now().Add(ttl)
item.TTL = &exp
}
}

4
go.mod
View file

@ -4,17 +4,21 @@ go 1.23.3
require ( require (
github.com/pelletier/go-toml/v2 v2.2.3 github.com/pelletier/go-toml/v2 v2.2.3
github.com/stretchr/testify v1.9.0
modernc.org/sqlite v1.34.1 modernc.org/sqlite v1.34.1
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/sys v0.22.0 // indirect golang.org/x/sys v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.55.3 // indirect modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.6.0 // indirect

2
go.sum
View file

@ -27,6 +27,8 @@ golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=

View file

@ -2,17 +2,21 @@ package model
import ( import (
"errors" "errors"
"time"
) )
var ErrCacheKeyDontExist = errors.New("cache key don't exist") var ErrCacheKeyDontExist = errors.New("cache key don't exist")
type Cache interface { type Cache interface {
Get(key string) (any, error) Get(key string) (any, error)
GetWithOptions(key string, opts ...CacheGetOption) Set(key string, value any, opts ...CacheOption) error
Set(key string, value any) error
SetWithOptions(key string, value any, opts ...CacheSetOption)
Delete(key string) error Delete(key string) error
} }
type CacheGetOption func() type CacheItem struct {
type CacheSetOption func() Key string
Value any
TTL *time.Time
}
type CacheOption func(item *CacheItem)