Skip to content
This repository has been archived by the owner on Apr 2, 2024. It is now read-only.

Commit

Permalink
Added new cache alternative solution: FreeCache
Browse files Browse the repository at this point in the history
  • Loading branch information
mrz1836 committed Apr 3, 2022
1 parent 8610e58 commit 0962fe1
Show file tree
Hide file tree
Showing 14 changed files with 158 additions and 138 deletions.
25 changes: 24 additions & 1 deletion cachestore/cachestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

"github.com/OrlovEvgeny/go-mcache"
"github.com/coocood/freecache"
"github.com/gomodule/redigo/redis"
"github.com/mrz1836/go-cache"
)
Expand All @@ -31,6 +32,8 @@ func (c *Client) Set(ctx context.Context, key string, value interface{}, depende
// Redis
if c.Engine() == Redis {
return cache.Set(ctx, c.options.redis, key, value, dependencies...)
} else if c.Engine() == FreeCache {
return c.options.freecache.Set([]byte(key), []byte(value.(string)), 0)
} else if c.Engine() == Ristretto {
if !c.options.ristretto.Set(key, value, baseCostPerKey) {
return ErrFailedToSet
Expand Down Expand Up @@ -61,7 +64,7 @@ func (c *Client) Get(ctx context.Context, key string) (interface{}, error) {
if c.Engine() == Redis {
str, err := cache.Get(ctx, c.options.redis, key)
if err != nil {
return "", err
return nil, err
}
return str, nil
} else if c.Engine() == Ristretto {
Expand All @@ -75,6 +78,14 @@ func (c *Client) Get(ctx context.Context, key string) (interface{}, error) {
return data, nil
}
return nil, nil
} else if c.Engine() == FreeCache {
data, err := c.options.freecache.Get([]byte(key))
if err != nil && errors.Is(err, freecache.ErrNotFound) { // Ignore this error
return nil, nil
} else if err != nil { // Real error getting the cache value
return nil, err
}
return string(data), nil
}

// Not found
Expand Down Expand Up @@ -112,6 +123,8 @@ func (c *Client) SetModel(ctx context.Context, key string, model interface{}, tt
ttl = mcache.TTL_FOREVER
}
return c.options.mCache.Set(key, responseBytes, ttl)
} else if c.Engine() == FreeCache {
return c.options.freecache.Set([]byte(key), responseBytes, int(ttl.Seconds()))
}

// Ristretto (store the bytes)
Expand Down Expand Up @@ -176,6 +189,16 @@ func (c *Client) GetModel(ctx context.Context, key string, model interface{}) er

return json.Unmarshal(by, &model)
}
} else if c.Engine() == FreeCache {
if b, err := c.options.freecache.Get([]byte(key)); err == nil {

// Sanity check to make sure there is a value to unmarshal
if len(b) == 0 {
return ErrKeyNotFound
}

return json.Unmarshal(b, &model)
}
}

// Not found
Expand Down
11 changes: 6 additions & 5 deletions cachestore/cachestore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ type cacheTestCase struct {

// Test cases for all in-memory cachestore engines
var cacheTestCases = []cacheTestCase{
{name: "[mcache] [in-memory]", engine: MCache, opts: WithMcache()},
{name: "[ristretto] [in-memory]", engine: Ristretto, opts: WithRistretto(DefaultRistrettoConfig())},
// {name: "[mcache] [in-memory]", engine: MCache, opts: WithMcache()},
// {name: "[ristretto] [in-memory]", engine: Ristretto, opts: WithRistretto(DefaultRistrettoConfig())},
{name: "[freecache] [in-memory]", engine: FreeCache, opts: WithFreeCache()},
}

func TestCachestore_Interface(t *testing.T) {
t.Run("mocked - valid datastore config", func(t *testing.T) {
t.Run("mocked - valid get/set using redis", func(t *testing.T) {
ctx := context.Background()
client, conn := newMockRedisClient(t)

Expand All @@ -85,7 +86,7 @@ func TestCachestore_Interface(t *testing.T) {
assert.Equal(t, fees, getFees)
})

t.Run("valid datastore config", func(t *testing.T) {
t.Run("valid get/set using redis", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping test: redis is required")
}
Expand Down Expand Up @@ -116,7 +117,7 @@ func TestCachestore_Interface(t *testing.T) {
}

func TestCachestore_Models(t *testing.T) {
t.Run("valid datastore config", func(t *testing.T) {
t.Run("valid set/get model struct", func(t *testing.T) {
ctx := context.Background()
if testing.Short() {
t.Skip("skipping test: redis is required")
Expand Down
18 changes: 18 additions & 0 deletions cachestore/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"

"github.com/OrlovEvgeny/go-mcache"
"github.com/coocood/freecache"
"github.com/dgraph-io/ristretto"
"github.com/mrz1836/go-cache"
"github.com/newrelic/go-agent/v3/newrelic"
Expand All @@ -20,6 +21,7 @@ type (
clientOptions struct {
debug bool // For extra logs and additional debug information
engine Engine // Cachestore engine (redis or mcache)
freecache *freecache.Cache // Driver (client) for local in-memory storage
mCache *mcache.CacheDriver // Driver (client) for local in-memory storage
newRelicEnabled bool // If NewRelic is enabled (parent application)
redis *cache.Client // Current redis client (read & write)
Expand Down Expand Up @@ -74,6 +76,12 @@ func NewClient(ctx context.Context, opts ...ClientOps) (ClientInterface, error)
return nil, err
}
}
} else if client.Engine() == FreeCache {

// Only if we don't already have an existing client
if client.options.freecache == nil {
client.options.freecache = loadFreeCache()
}
}

// Return the client
Expand Down Expand Up @@ -101,6 +109,11 @@ func (c *Client) Close(ctx context.Context) {
c.options.ristretto.Close()
}
c.options.ristretto = nil
} else if c.Engine() == FreeCache {
if c.options.freecache != nil {
c.options.freecache.Clear()
}
c.options.freecache = nil
}
c.options.engine = Empty
}
Expand Down Expand Up @@ -150,3 +163,8 @@ func (c *Client) Redis() *cache.Client {
func (c *Client) RedisConfig() *RedisConfig {
return c.options.redisConfig
}

// FreeCache will return the FreeCache client if found
func (c *Client) FreeCache() *freecache.Cache {
return c.options.freecache
}
7 changes: 7 additions & 0 deletions cachestore/client_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ func WithMcache() ClientOps {
}
}

// WithFreeCache will set the cache to local memory using FreeCache
func WithFreeCache() ClientOps {
return func(c *clientOptions) {
c.engine = FreeCache
}
}

// WithRistretto will set the cache to local in-memory using Ristretto
func WithRistretto(config *ristretto.Config) ClientOps {
return func(c *clientOptions) {
Expand Down
107 changes: 7 additions & 100 deletions cachestore/client_options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import (
"time"

"github.com/BuxOrg/bux/tester"
"github.com/OrlovEvgeny/go-mcache"
"github.com/dgraph-io/ristretto"
"github.com/coocood/freecache"
"github.com/mrz1836/go-cache"
"github.com/newrelic/go-agent/v3/newrelic"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -193,113 +192,21 @@ func TestWithRedisConnection(t *testing.T) {
})
}

// TestWithMcache will test the method WithMcache()
func TestWithMcache(t *testing.T) {
// TestWithFreeCache will test the method WithFreeCache()
func TestWithFreeCache(t *testing.T) {
t.Run("get opts", func(t *testing.T) {
opt := WithMcache()
opt := WithFreeCache()
assert.IsType(t, *new(ClientOps), opt)
})

t.Run("apply basic config", func(t *testing.T) {

opts := []ClientOps{WithDebugging(), WithMcache()}
c, err := NewClient(context.Background(), opts...)
require.NotNil(t, c)
require.NoError(t, err)

assert.Equal(t, MCache, c.Engine())
assert.IsType(t, &mcache.CacheDriver{}, c.MCache())
})
}

// TestWithRistretto will test the method WithRistretto()
func TestWithRistretto(t *testing.T) {
t.Run("get opts", func(t *testing.T) {
opt := WithRistretto(&ristretto.Config{
NumCounters: 100,
MaxCost: 100,
BufferItems: 1,
})
assert.IsType(t, *new(ClientOps), opt)
})

t.Run("apply basic config", func(t *testing.T) {

opts := []ClientOps{WithDebugging(), WithRistretto(&ristretto.Config{
NumCounters: 100,
MaxCost: 100,
BufferItems: 1,
})}
c, err := NewClient(context.Background(), opts...)
require.NotNil(t, c)
require.NoError(t, err)

assert.Equal(t, Ristretto, c.Engine())
assert.IsType(t, &ristretto.Cache{}, c.Ristretto())
})
}

// TestWithMcacheConnection will test the method WithMcacheConnection()
func TestWithMcacheConnection(t *testing.T) {
t.Run("get opts", func(t *testing.T) {
opt := WithMcacheConnection(nil)
assert.IsType(t, *new(ClientOps), opt)
})

t.Run("apply empty connection", func(t *testing.T) {
opts := []ClientOps{WithDebugging(), WithMcacheConnection(nil)}
c, err := NewClient(context.Background(), opts...)
assert.Nil(t, c)
assert.Error(t, err)
})

t.Run("apply existing external connection", func(t *testing.T) {
newClient := mcache.New()
require.NotNil(t, newClient)

opts := []ClientOps{WithDebugging(), WithMcacheConnection(newClient)}

c, err := NewClient(context.Background(), opts...)
require.NotNil(t, c)
require.NoError(t, err)

assert.Equal(t, MCache, c.Engine())
assert.IsType(t, &mcache.CacheDriver{}, c.MCache())
})
}

// TestWithRistrettoConnection will test the method WithRistrettoConnection()
func TestWithRistrettoConnection(t *testing.T) {
t.Run("get opts", func(t *testing.T) {
opt := WithRistretto(nil)
assert.IsType(t, *new(ClientOps), opt)
})

t.Run("apply empty connection", func(t *testing.T) {
opts := []ClientOps{WithDebugging(), WithRistretto(nil)}
opts := []ClientOps{WithDebugging(), WithFreeCache()}
c, err := NewClient(context.Background(), opts...)
assert.Nil(t, c)
assert.Error(t, err)
})

t.Run("apply existing external connection", func(t *testing.T) {
newClient, err := ristretto.NewCache(&ristretto.Config{
NumCounters: 100,
MaxCost: 1,
BufferItems: 10,
})
require.NotNil(t, newClient)
require.NoError(t, err)

opts := []ClientOps{WithDebugging(), WithRistrettoConnection(newClient)}

var c ClientInterface
c, err = NewClient(context.Background(), opts...)
require.NotNil(t, c)
require.NoError(t, err)

assert.Equal(t, Ristretto, c.Engine())
assert.IsType(t, &ristretto.Cache{}, c.Ristretto())
assert.Nil(t, c.RistrettoConfig())
assert.Equal(t, FreeCache, c.Engine())
assert.IsType(t, &freecache.Cache{}, c.FreeCache())
})
}
1 change: 1 addition & 0 deletions cachestore/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Engine string
// Supported engines
const (
Empty Engine = "empty"
FreeCache Engine = "freecache"
MCache Engine = "mcache"
Redis Engine = "redis"
Ristretto Engine = "ristretto"
Expand Down
1 change: 1 addition & 0 deletions cachestore/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ func TestEngine_String(t *testing.T) {
assert.Equal(t, "mcache", MCache.String())
assert.Equal(t, "redis", Redis.String())
assert.Equal(t, "ristretto", Ristretto.String())
assert.Equal(t, "freecache", FreeCache.String())
})
}

Expand Down
78 changes: 78 additions & 0 deletions cachestore/freecache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package cachestore

import (
"errors"
"runtime/debug"

"github.com/coocood/freecache"
"github.com/mrz1836/go-cache"
)

// loadFreeCache will load the FreeCache client
func loadFreeCache() *freecache.Cache {
// In bytes, where 1024 * 1024 represents a single Megabyte, and 100 * 1024*1024 represents 100 Megabytes.
cacheSize := 100 * 1024 * 1024
c := freecache.NewCache(cacheSize)
debug.SetGCPercent(20)
return c
}

// writeLockFreeCache will write a lock record into memory using a secret and expiration
//
// ttl is in seconds
func writeLockFreeCache(freeCacheClient *freecache.Cache, lockKey, secret string, ttl int64) (bool, error) {

// Test the key and secret
if err := validateLockValues(lockKey, secret); err != nil {
return false, err
}

// Try to get an existing lock (if it fails, make a new lock)
lockKeyBytes := []byte(lockKey)
secretBytes := []byte(secret)
data, err := freeCacheClient.Get(lockKeyBytes)
if err != nil && errors.Is(err, freecache.ErrNotFound) {
return true, freeCacheClient.Set(lockKeyBytes, secretBytes, int(ttl))
} else if err != nil {
return false, err
} else if err == nil && len(data) == 0 { // No lock found
return true, freeCacheClient.Set(lockKeyBytes, secretBytes, int(ttl))
}

// Check secret
if string(data) != secret { // Secret mismatch (lock exists with different secret)
return false, cache.ErrLockMismatch
}

// Same secret / lock again?
return true, freeCacheClient.Set(lockKeyBytes, secretBytes, int(ttl))
}

// releaseLockFreeCache will attempt to release a lock if it exists and matches the given secret
func releaseLockFreeCache(freeCacheClient *freecache.Cache, lockKey, secret string) (bool, error) {

// Test the key and secret
if err := validateLockValues(lockKey, secret); err != nil {
return false, err
}

// Try to get an existing lock (if it fails, lock does not exist)
lockKeyBytes := []byte(lockKey)
data, err := freeCacheClient.Get(lockKeyBytes)
if err != nil && errors.Is(err, freecache.ErrNotFound) {
return true, nil
} else if err != nil {
return false, err
} else if err == nil && len(data) == 0 { // No lock found
return true, nil
}

// Check secret if found
if string(data) == secret { // If it matches, remove the key
freeCacheClient.Del(lockKeyBytes)
return true, nil
}

// Key found does not match the secret, do not remove
return false, cache.ErrLockMismatch
}
Loading

0 comments on commit 0962fe1

Please sign in to comment.