Add distibuted cache with Redis

This commit is contained in:
2024-06-11 11:35:50 +01:00
parent a01a4da9b5
commit 12e0294945
15 changed files with 407 additions and 89 deletions
+16 -33
View File
@@ -18,8 +18,6 @@ type Cache struct {
globalTTL time.Duration
compressPool sync.Pool
decompressPool sync.Pool
cacheHits int
cacheMisses int
sync.RWMutex // Reintroduced to provide lock methods
}
@@ -53,6 +51,7 @@ func (c *Cache) cleanupRoutine(globalTTL time.Duration) {
c.CleanExpiredEntries()
}
}
func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
c.Lock() // use the lock
defer c.Unlock()
@@ -77,16 +76,13 @@ func (c *Cache) Get(key string) ([]byte, bool) {
entry, ok := c.entries.Load(key)
if !ok || entry.(CacheEntry).ExpiresAt.Before(time.Now()) {
c.cacheMisses++
return nil, false
}
compressedValue := entry.(CacheEntry).Value
value, err := c.decompress(compressedValue)
if err != nil {
c.cacheMisses++
return nil, false
}
c.cacheHits++
return value, true
}
@@ -102,24 +98,11 @@ func (c *Cache) Delete(key string) {
c.entries.Delete(key)
}
func (c *Cache) CleanExpiredEntries() {
now := time.Now()
c.entries.Range(func(key, value interface{}) bool {
entry := value.(CacheEntry)
if entry.ExpiresAt.Before(now) {
c.entries.Delete(key)
}
return true
})
func (c *Cache) Clear() {
c.entries = sync.Map{}
}
type CacheStats struct {
CachedQueries int `json:"cached_queries"`
CacheHits int `json:"cache_hits"`
CacheMisses int `json:"cache_misses"`
}
func (c *Cache) ShowStats() CacheStats {
func (c *Cache) CountQueries() int {
c.RLock()
defer c.RUnlock()
var count int
@@ -127,18 +110,7 @@ func (c *Cache) ShowStats() CacheStats {
count++
return true
})
cs := CacheStats{
CachedQueries: count,
CacheHits: c.cacheHits,
CacheMisses: c.cacheMisses,
}
return cs
}
func (c *Cache) ClearCache() {
c.cacheHits = 0
c.cacheMisses = 0
c.entries = sync.Map{}
return count
}
func (c *Cache) compress(data []byte) ([]byte, error) {
@@ -183,3 +155,14 @@ func (c *Cache) decompress(data []byte) ([]byte, error) {
}
return decompressedData, nil
}
func (c *Cache) CleanExpiredEntries() {
now := time.Now()
c.entries.Range(func(key, value interface{}) bool {
entry := value.(CacheEntry)
if entry.ExpiresAt.Before(now) {
c.entries.Delete(key)
}
return true
})
}
-33
View File
@@ -110,36 +110,3 @@ func (suite *CacheTestSuite) Test_CacheExpire() {
})
}
}
func (suite *CacheTestSuite) Test_CacheStats() {
cache := New(30 * time.Second)
tests := []struct {
name string
cache_value string
ttl time.Duration
}{
{
name: "test1",
cache_value: "test1-123",
ttl: 2 * time.Second,
},
{
name: "test2",
cache_value: "test2-123",
ttl: 5 * time.Second,
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
cache.Set(tt.name, []byte(tt.name), tt.ttl)
c, ok := cache.Get(tt.name)
suite.Equal(true, ok)
suite.Equal(tt.name, string(c))
})
}
cache.Get("non-existent-non-cached-key")
stats := cache.ShowStats()
suite.Equal(2, stats.CacheHits, "CacheHits")
suite.Equal(1, stats.CacheMisses, "CacheMisses")
suite.Equal(2, stats.CachedQueries, "CachedQueries")
}
+73
View File
@@ -0,0 +1,73 @@
package libpack_redis
import (
"context"
"time"
redis "github.com/redis/go-redis/v9"
)
var ()
type RedisConfig struct {
client *redis.Client
ctx context.Context
}
func prependKeyName(key string) string {
return "gmp_cache:" + key
}
type RedisClientConfig struct {
RedisServer string
RedisPassword string
RedisDB int
}
func NewClient(redisClientConfig *RedisClientConfig) *RedisConfig {
c := &RedisConfig{
client: redis.NewClient(&redis.Options{
Addr: redisClientConfig.RedisServer,
Password: redisClientConfig.RedisPassword,
DB: redisClientConfig.RedisDB,
}),
ctx: context.Background(),
}
return c
}
func (c *RedisConfig) Set(key string, value []byte, ttl time.Duration) {
c.client.Set(c.ctx, prependKeyName(key), value, ttl)
}
func (c *RedisConfig) Get(key string) ([]byte, bool) {
val, err := c.client.Get(c.ctx, prependKeyName(key)).Result()
if err == redis.Nil || err != nil {
return nil, false
}
return []byte(val), true
}
func (c *RedisConfig) Delete(key string) {
c.client.Del(c.ctx, prependKeyName(key))
}
func (c *RedisConfig) Clear() {
c.client.FlushDB(c.ctx)
}
func (c *RedisConfig) CountQueries() int {
keys, err := c.client.Keys(c.ctx, prependKeyName("*")).Result()
if err != nil {
return 0
}
return len(keys)
}
func (c *RedisConfig) CountQueriesWithPattern(pattern string) int {
keys, err := c.client.Keys(c.ctx, prependKeyName(pattern)).Result()
if err != nil {
return 0
}
return len(keys)
}
+123
View File
@@ -0,0 +1,123 @@
package libpack_redis
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type RedisConfigSuite struct {
suite.Suite
redisConfig *RedisConfig
}
func (suite *RedisConfigSuite) SetupTest() {
suite.redisConfig = NewClient(&RedisClientConfig{
RedisServer: "localhost:6379",
RedisPassword: "",
RedisDB: 0,
})
suite.redisConfig.Delete("testkey")
}
func TestRedisConfigSuite(t *testing.T) {
suite.Run(t, new(RedisConfigSuite))
}
func (suite *RedisConfigSuite) TestSet() {
key := "testkey"
value := []byte("testvalue")
suite.redisConfig.Delete(key) // Ensure the key is deleted before the test
// Test writing a new key-value pair
suite.redisConfig.Set(key, value, 0)
storedValue, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
// Test overwriting an existing key-value pair
newValue := []byte("newvalue")
suite.redisConfig.Set(key, newValue, 0)
storedValue, found = suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), newValue, storedValue)
suite.redisConfig.Delete(key) // Clean up after the test
}
func (suite *RedisConfigSuite) TestSetWithExpiry() {
key := "testkey"
value := []byte("testvalue")
expiry := 1 * time.Second
suite.redisConfig.Delete(key) // Ensure the key is deleted before the test
// Test writing a new key-value pair
suite.redisConfig.Set(key, value, expiry)
storedValue, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
// Test that key expires after the specified time
time.Sleep(2 * time.Second)
_, found = suite.redisConfig.Get(key)
assert.False(suite.T(), found)
suite.redisConfig.Delete(key) // Clean up after the test
}
func (suite *RedisConfigSuite) TestGet() {
key := "testkey"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, 0) // Set the key-value pair
storedValue, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
assert.Equal(suite.T(), value, storedValue)
}
func (suite *RedisConfigSuite) TestDeleteKey() {
key := "testkey"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, 0) // Set the key-value pair
suite.redisConfig.Delete(key)
_, found := suite.redisConfig.Get(key)
assert.False(suite.T(), found)
}
func (suite *RedisConfigSuite) TestCheckIfKeyExists() {
ttl := time.Duration(10) * time.Second
key := "testkey"
value := []byte("testvalue")
suite.redisConfig.Set(key, value, ttl) // Set the key-value pair
_, found := suite.redisConfig.Get(key)
assert.True(suite.T(), found)
suite.redisConfig.Delete(key)
_, found = suite.redisConfig.Get(key)
assert.False(suite.T(), found)
}
func (suite *RedisConfigSuite) TestGetKeys() {
ttl := time.Duration(10) * time.Second
suite.redisConfig.Set("testkey1", []byte("testvalue1"), ttl)
suite.redisConfig.Set("testkey2", []byte("testvalue2"), ttl)
suite.redisConfig.Set("otherkey", []byte("othervalue"), ttl)
keys, _ := suite.redisConfig.client.Keys(suite.redisConfig.ctx, prependKeyName("testkey*")).Result()
expectedKeys := []string{prependKeyName("testkey1"), prependKeyName("testkey2")}
assert.ElementsMatch(suite.T(), expectedKeys, keys)
suite.redisConfig.client.Del(suite.redisConfig.ctx, "testkey1", "testkey2", "otherkey")
}
func (suite *RedisConfigSuite) TestGetKeysCount() {
ttl := time.Duration(10) * time.Second
suite.redisConfig.Set("testkey1", []byte("testvalue1"), ttl)
suite.redisConfig.Set("testkey2", []byte("testvalue2"), ttl)
suite.redisConfig.Set("otherkey", []byte("othervalue"), ttl)
assert.Equal(suite.T(), 2, suite.redisConfig.CountQueriesWithPattern("testkey*"))
suite.redisConfig.client.Del(suite.redisConfig.ctx, "testkey1", "testkey2", "otherkey")
}