xcache

package module
v1.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 8, 2023 License: MIT Imports: 17 Imported by: 0

README

Xcache

Build Status License Coverage Status Goreportcard Go Reference


Package xcache offers caching alternatives for an application like a local in memory cache, or distributed Redis cache, or combination of those two in a multi layered cache.

Installation
$ go get -u github.com/actforgood/xcache
Cache adapters
  • Memory - a local in memory cache, relies upon Freecache package.
  • Redis6 - Redis version 6 cache (single instance / sentinel failover / cluster).
  • Redis7 - Redis version 7 cache (single instance / sentinel failover / cluster).
  • Multi - A multi layer cache.
  • Nop - A no-operation cache.
  • Mock - A stub that can be used in Unit Tests.
The Cache contract

Looks like:

// Cache provides prototype a for storing and returning a key-value into/from cache.
type Cache interface {
	// Save stores the given key-value with expiration period into cache.
	// An expiration period equal to 0 (NoExpire) means no expiration.
	// A negative expiration period triggers deletion of key.
	// It returns an error if the key could not be saved.
	Save(ctx context.Context, key string, value []byte, expire time.Duration) error

	// Load returns a key's value from cache, or an error if something bad happened.
	// If the key is not found, ErrNotFound is returned.
	Load(ctx context.Context, key string) ([]byte, error)

	// TTL returns a key's remaining time to live, or an error if something bad happened.
	// If the key is not found, a negative TTL is returned.
	// If the key has no expiration, 0 (NoExpire) is returned.
	TTL(ctx context.Context, key string) (time.Duration, error)

	// Stats returns some statistics about cache's memory/keys.
	// It returns an error if something goes wrong.
	Stats(context.Context) (Stats, error)
}
Examples
Memory
func ExampleMemory() {
	cache := xcache.NewMemory(10 * 1024 * 1024) // 10 Mb

	ctx := context.Background()
	key := "example-memory"
	value := []byte("Hello Memory Cache")
	ttl := 10 * time.Minute

	// save a key for 10 minutes
	if err := cache.Save(ctx, key, value, ttl); err != nil {
		fmt.Println("could not save Memory cache key: " + err.Error())
	}

	// load the key's value
	if value, err := cache.Load(ctx, key); err != nil {
		fmt.Println("could not get Memory cache key: " + err.Error())
	} else {
		fmt.Println(string(value))
	}

	// Output:
	// Hello Memory Cache
}

Benchmarks

go test -run=^# -benchmem -benchtime=5s -bench BenchmarkMemory github.com/actforgood/xcache
goos: linux
goarch: amd64
pkg: github.com/actforgood/xcache
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkMemory_Save-4                  11463078               492.5 ns/op             2 B/op          0 allocs/op
BenchmarkMemory_Save_parallel-4         24244593               261.4 ns/op            25 B/op          1 allocs/op
BenchmarkMemory_Load-4                  30390457               194.0 ns/op            40 B/op          2 allocs/op
BenchmarkMemory_Load_parallel-4         25545855               220.0 ns/op            40 B/op          2 allocs/op
BenchmarkMemory_TTL-4                   55357045               110.5 ns/op             0 B/op          0 allocs/op
BenchmarkMemory_TTL_parallel-4          40464970               153.2 ns/op             0 B/op          0 allocs/op
BenchmarkMemory_Stats-4                  5760609               983.7 ns/op             0 B/op          0 allocs/op
BenchmarkMemory_Stats_parallel-4        23939924               254.5 ns/op             0 B/op          0 allocs/op
Redis
func ExampleRedis() {
	cache := xcache.NewRedis6(xcache.RedisConfig{ // or xcache.NewRedis7 if you're using ver. 7
		Addrs: []string{"127.0.0.1:6379"},
	})

	ctx := context.Background()
	key := "example-redis"
	value := []byte("Hello Redis Cache")
	ttl := 10 * time.Minute

	// save a key for 10 minutes
	if err := cache.Save(ctx, key, value, ttl); err != nil {
		fmt.Println("could not save Redis cache key: " + err.Error())
	}

	// load the key's value
	if value, err := cache.Load(ctx, key); err != nil {
		fmt.Println("could not get Redis cache key: " + err.Error())
	} else {
		fmt.Println(string(value))
	}

	// close the cache when no needed anymore/at your application shutdown.
	if err := cache.Close(); err != nil {
		fmt.Println("could not close Redis cache: " + err.Error())
	}

	// should output:
	// Hello Redis Cache
}

Benchmarks

go test -tags=integration -run=^# -benchmem -benchtime=5s -bench BenchmarkRedis github.com/actforgood/xcache
goos: linux
goarch: amd64
pkg: github.com/actforgood/xcache
cpu: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
BenchmarkRedis6_Save_integration-4                 32479            161610 ns/op             272 B/op          7 allocs/op
BenchmarkRedis6_Save_parallel_integration-4       130350             43058 ns/op             296 B/op          8 allocs/op
BenchmarkRedis6_Load_integration-4                 39960            144686 ns/op             208 B/op          6 allocs/op
BenchmarkRedis6_Load_parallel_integration-4       103783             62320 ns/op             208 B/op          6 allocs/op
BenchmarkRedis6_TTL_integration-4                  43336            158656 ns/op             196 B/op          5 allocs/op
BenchmarkRedis6_TTL_parallel_integration-4        135324             43266 ns/op             196 B/op          5 allocs/op
BenchmarkRedis6_Stats-4                            23257            244013 ns/op            5052 B/op          6 allocs/op
BenchmarkRedis6_Stats_parallel-4                   63873             90896 ns/op            5052 B/op          6 allocs/op
BenchmarkRedis7_Save_integration-4                 38620            162062 ns/op             312 B/op         10 allocs/op
BenchmarkRedis7_Save_parallel_integration-4       129525             46068 ns/op             336 B/op         11 allocs/op
BenchmarkRedis7_Load_integration-4                 42074            153150 ns/op             248 B/op          9 allocs/op
BenchmarkRedis7_Load_parallel_integration-4       139232             43403 ns/op             248 B/op          9 allocs/op
BenchmarkRedis7_TTL_integration-4                  32029            163338 ns/op             236 B/op          8 allocs/op
BenchmarkRedis7_TTL_parallel_integration-4        117226             56544 ns/op             236 B/op          8 allocs/op
BenchmarkRedis7_Stats-4                            23600            254668 ns/op            5604 B/op          9 allocs/op
BenchmarkRedis7_Stats_parallel-4                   59908            100755 ns/op            5604 B/op          9 allocs/op
Multi
func ExampleMulti() {
	// create a frontend - backend multi cache.
	frontCache := xcache.NewMemory(10 * 1024 * 1024) // 10 Mb
	backCache := xcache.NewRedis6(xcache.RedisConfig{
		Addrs: []string{"127.0.0.1:6379"},
	})
	defer backCache.Close()
	cache := xcache.NewMulti(frontCache, backCache)

	ctx := context.Background()
	key := "example-multi"
	value := []byte("Hello Multi Cache")
	ttl := 10 * time.Minute

	// save a key for 10 minutes
	if err := cache.Save(ctx, key, value, ttl); err != nil {
		fmt.Println("could not save Multi cache key: " + err.Error())
	}

	// load the key's value
	if value, err := cache.Load(ctx, key); err != nil {
		fmt.Println("could not get Multi cache key: " + err.Error())
	} else {
		fmt.Println(string(value))
	}

	// should output:
	// Hello Multi Cache
}
Reconfiguring on the fly the caches

If you need to change caches' configs without redeploying your application, you can use the xconf pkg adapter to initialize the caches: NewMemoryWithConfig / NewRedis6WithConfig / NewRedis7WithConfig.

Monitoring your cache stats

If you need to monitor your cache's statistics, you can check StatsWatcher which can help you in this matter. It executes periodically a provided callback upon cache's Stats, thus, you can log them / sent them to a metrics system.

Running tests / benchmarks

in scripts folder there is a shell script that sets up a Redis docker based environment with desired configuration and runs integration tests / benchmarks.

cd /path/to/xcache
./scripts/run_local.sh cluster  // example of running tests in Redis cluster setup
./scripts/run_local.sh single bench // example of running benchmarks in Redis single instance setup.
TODOs:

Things that can be added to pkg, extended:

  • Support also Memcached.
License

This package is released under a MIT license. See LICENSE.
Other 3rd party packages directly used by this package are released under their own licenses.

Documentation

Overview

Package xcache offers caching alternatives for an application like a local in memory cache, or distributed Redis cache, or combination of those two in a multi layered cache.

Index

Examples

Constants

View Source
const (
	// RedisCfgKeyAddrs is the key under which xconf.Config expects Redis server(s).
	// Value should be a slice of string(s).
	RedisCfgKeyAddrs = "xcache.redis.addrs"
	// RedisCfgKeyDB is the key under which xconf.Config expects Redis DB.
	RedisCfgKeyDB = "xcache.redis.db"
	// RedisCfgKeyAuthUsername is the key under which xconf.Config expects auth username.
	RedisCfgKeyAuthUsername = "xcache.redis.auth.username"
	// RedisCfgKeyAuthPassword is the key under which xconf.Config expects auth password.
	RedisCfgKeyAuthPassword = "xcache.redis.auth.password"
	// RedisCfgKeyDialTimeout is the key under which xconf.Config expects dial timeout.
	RedisCfgKeyDialTimeout = "xcache.redis.timeout.dial"
	// RedisCfgKeyReadTimeout is the key under which xconf.Config expects read timeout.
	RedisCfgKeyReadTimeout = "xcache.redis.timeout.read"
	// RedisCfgKeyWriteTimeout is the key under which xconf.Config expects write timeout.
	RedisCfgKeyWriteTimeout = "xcache.redis.timeout.write"
	// RedisCfgKeyClusterReadonly is the key under which xconf.Config expects readonly flag.
	RedisCfgKeyClusterReadonly = "xcache.redis.cluster.readonly"
	// RedisCfgKeyFailoverMasterName is the key under which xconf.Config expects master name.
	RedisCfgKeyFailoverMasterName = "xcache.redis.failover.mastername"
	// RedisCfgKeyFailoverAuthUsername is the key under which xconf.Config expects sentinel auth username.
	RedisCfgKeyFailoverAuthUsername = "xcache.redis.failover.auth.usernmae"
	// RedisCfgKeyFailoverAuthPassword is the key under which xconf.Config expects sentinel auth password.
	RedisCfgKeyFailoverAuthPassword = "xcache.redis.failover.auth.password"
)
View Source
const (
	// MemoryCfgKeyMemorySize is the key under which xconf.Config expects memory size in bytes.
	MemoryCfgKeyMemorySize = "xcache.memory.memsizebytes"
)
View Source
const NoExpire time.Duration = 0

NoExpire is the value for no expiration.

Variables

View Source
var ErrNotFound = errors.New("key not found")

ErrNotFound is an error returned by a cache Load operation if a key does not exist.

Functions

func SetRedis6Logger

func SetRedis6Logger(redisXLogger RedisXLogger)

SetRedis6Logger sets given xlog logger for a Redis6 client.

func SetRedis7Logger

func SetRedis7Logger(redisXLogger RedisXLogger)

SetRedis7Logger sets given xlog logger for a Redis7 client.

Types

type Cache

type Cache interface {
	// Save stores the given key-value with expiration period into cache.
	// An expiration period equal to 0 (NoExpire) means no expiration.
	// A negative expiration period triggers deletion of key.
	// It returns an error if the key could not be saved.
	Save(ctx context.Context, key string, value []byte, expire time.Duration) error

	// Load returns a key's value from cache, or an error if something bad happened.
	// If the key is not found, ErrNotFound is returned.
	Load(ctx context.Context, key string) ([]byte, error)

	// TTL returns a key's remaining time to live, or an error if something bad happened.
	// If the key is not found, a negative TTL is returned.
	// If the key has no expiration, 0 (NoExpire) is returned.
	TTL(ctx context.Context, key string) (time.Duration, error)

	// Stats returns some statistics about cache's memory/keys.
	// It returns an error if something goes wrong.
	Stats(context.Context) (Stats, error)
}

Cache provides prototype a for storing and returning a key-value into/from cache.

type Memory

type Memory struct {
	// contains filtered or unexported fields
}

Memory is an in memory implementation for Cache. It is not distributed, keys are stored in memory, only for current instance. It relies upon Freecache package.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/actforgood/xcache"
)

func main() {
	cache := xcache.NewMemory(10 * 1024 * 1024) // 10 Mb

	ctx := context.Background()
	key := "example-memory"
	value := []byte("Hello Memory Cache")
	ttl := 10 * time.Minute

	// save a key for 10 minutes
	if err := cache.Save(ctx, key, value, ttl); err != nil {
		fmt.Println("could not save Memory cache key: " + err.Error())
	}

	// load the key's value
	if value, err := cache.Load(ctx, key); err != nil {
		fmt.Println("could not get Memory cache key: " + err.Error())
	} else {
		fmt.Println(string(value))
	}

}
Output:

Hello Memory Cache
Example (WithXConf)
package main

import (
	"context"
	"fmt"
	"os"
	"time"

	"github.com/actforgood/xcache"
	"github.com/actforgood/xconf"
)

func main() {
	// Setup an env (assuming your application configuration comes from env,
	// it's not mandatory to be env, you can use any source loader you want)
	_ = os.Setenv("MY_APP_CACHE_MEM_SIZE", "1048576")
	defer os.Unsetenv("MY_APP_CACHE_MEM_SIZE")

	// Initialize config, we set an alias, as example, as our config key is custom ("MY_APP_CACHE_MEM_SIZE").
	config, err := xconf.NewDefaultConfig(
		xconf.AliasLoader(
			xconf.EnvLoader(),
			xcache.MemoryCfgKeyMemorySize, "MY_APP_CACHE_MEM_SIZE",
		),
		xconf.DefaultConfigWithReloadInterval(2*time.Second),
	)
	if err != nil {
		panic(err)
	}
	defer config.Close()

	// Initialize the cache our application will use.
	cache := xcache.NewMemoryWithConfig(config)

	// From this point forward you can use the cache object however you want.

	// Let's print some stats to see the memory size.
	stats, err := cache.Stats(context.Background())
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(stats)
	}

	// Let's assume we monitor our cache - see StatsWatcher for that,
	// and we notice existing keys count is kind of constant and an increase in evictions,
	// meaning our memory cache is probably full.

	// We decide to increase the memory size.
	_ = os.Setenv("MY_APP_CACHE_MEM_SIZE", "5242880")
	time.Sleep(2500 * time.Millisecond) // wait for config to reload

	// Print again the stats, we can see that memory was changed,
	// without the need of restarting/redeploying our application.
	stats, err = cache.Stats(context.Background())
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println(stats)
	}

}
Output:

mem=1M maxMem=1M memUsage=100.00% hits=0 misses=0 hitRate=100.00% keys=0 expired=0 evicted=0
mem=5M maxMem=5M memUsage=100.00% hits=0 misses=0 hitRate=100.00% keys=0 expired=0 evicted=0

func NewMemory

func NewMemory(memSize int) *Memory

NewMemory initializes a new Memory instance.

Relaying package additional notes: The cache size will be set to 512KB at minimum. If the size is set relatively large, you should call runtime/debug.SetGCPercent, set it to a much smaller value to limit the memory consumption and GC pause time.

func NewMemoryWithConfig

func NewMemoryWithConfig(config xconf.Config) *Memory

NewMemoryWithConfig initializes a Memory Cache with memory size taken from a xconf.Config.

The key under which memory size is expected to be found is "xcache.memory.memsizebytes" (note, you can have a different config key defined in your project, you'll have to create an alias for it to expected "xcache.memory.memsizebytes"). If "xcache.memory.memsizebytes" config key is not found, a default value of 10M is used.

An observer is registered to xconf.DefaultConfig (which knows to reload configuration). In case "xcache.memory.memsizebytes" config is changed, the Memory is reinitialized with the new memory size, and all items from old freecache instance are copied to the new one. Note: host machine/container needs to have additional to current occupied memory, the new memory size available (until old memory is garbage collected, old memory size is still occupied).

func (*Memory) Load

func (cache *Memory) Load(_ context.Context, key string) ([]byte, error)

Load returns a key's value from cache, or an error if something bad happened. If the key is not found, ErrNotFound is returned.

func (*Memory) Save

func (cache *Memory) Save(
	_ context.Context,
	key string,
	value []byte,
	expire time.Duration,
) error

Save stores the given key-value with expiration period into cache. An expiration period equal to 0 (NoExpire) means no expiration. A negative expiration period triggers deletion of key. It returns an error if the key could not be saved.

Additional relaying package notes: If the key is larger than 65535 or value is larger than 1/1024 of the cache size, the entry will not be written to the cache. Items can be evicted when cache is full.

func (*Memory) Stats

func (cache *Memory) Stats(_ context.Context) (Stats, error)

Stats returns statistics about memory cache. Returned error is always nil and can be safely disregarded.

func (*Memory) TTL

func (cache *Memory) TTL(_ context.Context, key string) (time.Duration, error)

TTL returns a key's remaining time to live. Error is always nil. If the key is not found, a negative TTL is returned. If the key has no expiration, 0 (NoExpire) is returned.

type Mock

type Mock struct {
	// contains filtered or unexported fields
}

Mock is a mock to be used in UT.

func (*Mock) Load

func (mock *Mock) Load(ctx context.Context, key string) ([]byte, error)

Load mock logic...

func (*Mock) LoadCallsCount

func (mock *Mock) LoadCallsCount() int

LoadCallsCount returns the no. of times Load() method was called.

func (*Mock) Save

func (mock *Mock) Save(
	ctx context.Context,
	key string,
	value []byte,
	expire time.Duration,
) error

Save mock logic...

func (*Mock) SaveCallsCount

func (mock *Mock) SaveCallsCount() int

SaveCallsCount returns the no. of times Save() method was called.

func (*Mock) SetLoadCallback

func (mock *Mock) SetLoadCallback(callback func(context.Context, string) ([]byte, error))

SetLoadCallback sets the given callback to be executed inside Load() method. You can inject yourself to make assertions upon passed parameter(s) this way and/or control the returned value.

Usage example:

mock.SetLoadCallback(func(ctx context.Context, k string) ([]byte, error) {
	if k != "expected-key" {
		t.Error("expected ...")
	}

	return []byte("expected value"), nil
})

func (*Mock) SetSaveCallback

func (mock *Mock) SetSaveCallback(callback func(context.Context, string, []byte, time.Duration) error)

SetSaveCallback sets the given callback to be executed inside Save() method. You can inject yourself to make assertions upon passed parameter(s) this way and/or control the returned value.

Usage example:

mock.SetSaveCallback(func(ctx context.Context, k string, v []byte, exp time.Duration) {
	if k != "expected-key" {
		t.Error("expected ...")
	}
	if !reflect.DeepEqual(v, []byte("expected value")) {
		t.Error("expected ...")
	}
	if exp != 10 * time.Minute {
		t.Error("expected ...")
	}

	return nil
})

func (*Mock) SetStatsCallback

func (mock *Mock) SetStatsCallback(callback func(context.Context) (Stats, error))

SetStatsCallback sets the given callback to be executed inside Stats() method. You can inject yourself to make assertions upon passed parameter(s) this way and/or control the returned value.

Usage example:

mock.SetStatsCallback(func(ctx context.Context) (xcache.Stats, error) {
	if ctx != context.Background() {
		t.Error("expected ...")
	}

	return xcache.Stats{Memory: 1024}, nil
})

func (*Mock) SetTTLCallback

func (mock *Mock) SetTTLCallback(callback func(context.Context, string) (time.Duration, error))

SetTTLCallback sets the given callback to be executed inside TTL() method. You can inject yourself to make assertions upon passed parameter(s) this way and/or control the returned value.

Usage example:

mock.SetTTLCallback(func(ctx context.Context, k string) (time.Duration, error) {
	if k != "expected-key" {
		t.Error("expected ...")
	}

	return 123 * time.Second, nil
})

func (*Mock) Stats

func (mock *Mock) Stats(ctx context.Context) (Stats, error)

Stats mock logic...

func (*Mock) StatsCallsCount

func (mock *Mock) StatsCallsCount() int

StatsCallsCount returns the no. of times Stats() method was called.

func (*Mock) TTL

func (mock *Mock) TTL(ctx context.Context, key string) (time.Duration, error)

TTL mock logic...

func (*Mock) TTLCallsCount

func (mock *Mock) TTLCallsCount() int

TTLCallsCount returns the no. of times TTL() method was called.

type Multi

type Multi struct {
	// contains filtered or unexported fields
}

Multi is a composite Cache. Saving a key triggers saving in all contained caches. A key is loaded from the first cache it is found in (in the order caches were provided in the constructor).

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/actforgood/xcache"
)

func main() {
	// create a frontend - backend multi cache.
	frontCache := xcache.NewMemory(10 * 1024 * 1024) // 10 Mb
	backCache := xcache.NewRedis6(xcache.RedisConfig{
		Addrs: []string{"127.0.0.1:6379"},
	})
	defer backCache.Close()
	cache := xcache.NewMulti(frontCache, backCache)

	ctx := context.Background()
	key := "example-multi"
	value := []byte("Hello Multi Cache")
	ttl := 10 * time.Minute

	// save a key for 10 minutes
	if err := cache.Save(ctx, key, value, ttl); err != nil {
		fmt.Println("could not save Multi cache key: " + err.Error())
	}

	// load the key's value
	if value, err := cache.Load(ctx, key); err != nil {
		fmt.Println("could not get Multi cache key: " + err.Error())
	} else {
		fmt.Println(string(value))
	}

	// should output:
	// Hello Multi Cache
}
Output:

func NewMulti

func NewMulti(caches ...Cache) Multi

NewMulti initializes a new Multi instance.

func (Multi) Load

func (cache Multi) Load(ctx context.Context, key string) ([]byte, error)

Load returns a key's value from the first cache it finds it. If the key is found in a deeper cache, key is tried to be saved also in upfront cache(s). Note: if a cache returns an error, but the next cache returns the value, the value and nil error will be returned (method aims to be successful). If the key is not found in any of the caches, ErrNotFound is returned. If the key is not found in any of the caches, and any cache gave an error, that error will be returned.

func (Multi) Save

func (cache Multi) Save(
	ctx context.Context,
	key string,
	value []byte,
	expire time.Duration,
) error

Save stores the given key-value with expiration period into all caches. An expiration period equal to 0 (NoExpire) means no expiration. A negative expiration period triggers deletion of key. It returns an error if the key could not be saved (in any of the caches - note, that the key can end up being saved in other cache(s)).

func (Multi) Stats

func (cache Multi) Stats(ctx context.Context) (Stats, error)

Stats returns statistics about memory cache, or an error if something bad happens within any of the caches. Returned statistics are just summed up for all contained caches.

func (Multi) TTL

func (cache Multi) TTL(ctx context.Context, key string) (time.Duration, error)

TTL returns a key's remaining time to live from the first cache it finds it. If the key is not found (in any of the caches), a negative TTL is returned. If the key has no expiration, 0 (NoExpire) is returned. Note: if a cache returns an error, but the next cache returns the ttl, the ttl and nil error will be returned (method aims to be successful). If the key is not found in any of the caches, and any cache gave an error, that error will be returned.

type Nop

type Nop struct{}

Nop is a no-operation Cache which does nothing. It simply ignores saves and returns ErrNotFound.

func (Nop) Load

func (Nop) Load(context.Context, string) ([]byte, error)

Load returns ErrNotFound.

func (Nop) Save

Save does nothing.

func (Nop) Stats

func (Nop) Stats(context.Context) (Stats, error)

Stats returns empty Stats object.

func (Nop) TTL

TTL returns negative TTL.

type Redis6

type Redis6 struct {
	// contains filtered or unexported fields
}

Redis6 is Redis (distributed, ver.6) based implementation for Cache. It implements io.Closer, and thus it should be closed at your application shutdown.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/actforgood/xcache"
)

func main() {
	cache := xcache.NewRedis6(xcache.RedisConfig{
		Addrs: []string{"127.0.0.1:6379"},
	})

	ctx := context.Background()
	key := "example-redis"
	value := []byte("Hello Redis Cache")
	ttl := 10 * time.Minute

	// save a key for 10 minutes
	if err := cache.Save(ctx, key, value, ttl); err != nil {
		fmt.Println("could not save Redis cache key: " + err.Error())
	}

	// load the key's value
	if value, err := cache.Load(ctx, key); err != nil {
		fmt.Println("could not get Redis cache key: " + err.Error())
	} else {
		fmt.Println(string(value))
	}

	// close the cache when no needed anymore/at your application shutdown.
	if err := cache.Close(); err != nil {
		fmt.Println("could not close Redis cache: " + err.Error())
	}

	// should output:
	// Hello Redis Cache
}
Output:

Example (WithXConf)
package main

import (
	"bytes"
	"time"

	"github.com/actforgood/xcache"
	"github.com/actforgood/xconf"
)

func main() {
	// Setup the config our application will use (here used a NewFlattenLoader over a json source)
	// You can use whatever config loader suits you as long as needed xcache keys are present.
	config, err := xconf.NewDefaultConfig(
		xconf.NewFlattenLoader(xconf.JSONReaderLoader(bytes.NewReader([]byte(`{
			"xcache": {
			  "redis": {
				"addrs": [
				  "127..0.0.1:6379"
				],
				"db": 0,
				"auth": {
				  "username": "",
				  "password": ""
				},
				"timeout": {
				  "dial": "5s",
				  "read": "6s",
				  "write": "10s"
				},
				"cluster": {
				  "readonly": true
				},
				"failover": {
				  "mastername": "mymaster",
				  "auth": {
					"username": "",
					"password": ""
				  }
				}
			  }
			}
		  }`)))),
		xconf.DefaultConfigWithReloadInterval(5*time.Minute),
	)
	if err != nil {
		panic(err)
	}
	defer config.Close()

	// Initialize the cache our application will use.
	cache := xcache.NewRedis6WithConfig(config)
	defer cache.Close()

	// From this point forward you can do whatever you want with the cache.
	// Any config that gets changed, cache will reconfigure itself in a time up to reload interval (5 mins here)
	// without the need of restarting/redeploying our application.
	// For example, let's assume we notice a lot of timeout errors,
	// until we figure it out what's happening with our Redis server,
	// we can increase read/write timeouts.
}
Output:

func NewRedis6

func NewRedis6(config RedisConfig) *Redis6

NewRedis6 instantiates a new Redis6 Cache instance (compatible with Redis ver.6).

1. If the MasterName option is specified, a sentinel-backed FailoverClient is used behind. 2. If the number of Addrs is two or more, a ClusterClient is used behind. 3. Otherwise, a single-node Client is used.

func NewRedis6WithConfig

func NewRedis6WithConfig(config xconf.Config) *Redis6

NewRedis6WithConfig initializes a Redis6 Cache with configuration taken from a xconf.Config.

Keys under which configuration is expected are defined in RedisCfgKey* constants (note, you can have different config keys defined in your project, you'll have to create an alias for them to expected values by this package).

An observer is registered to xconf.DefaultConfig (which knows to reload configuration). In case any config value requested by Redis6 is changed, the Redis6 is reinitialized with the new config.

func (*Redis6) Close

func (cache *Redis6) Close() (err error)

Close closes the underlying Redis client.

func (*Redis6) Load

func (cache *Redis6) Load(ctx context.Context, key string) ([]byte, error)

Load returns a key's value from cache, or an error if something bad happened. If the key is not found, ErrNotFound is returned.

func (*Redis6) Save

func (cache *Redis6) Save(
	ctx context.Context,
	key string,
	value []byte,
	expire time.Duration,
) error

Save stores the given key-value with expiration period into cache. An expiration period equal to 0 (NoExpire) means no expiration. A negative expiration period triggers deletion of key. It returns an error if the key could not be saved.

func (*Redis6) Stats

func (cache *Redis6) Stats(ctx context.Context) (Stats, error)

Stats returns some statistics about cache memory/keys. It returns an error if something goes wrong (for example, client might not be able to connect to Redis server).

func (*Redis6) TTL

func (cache *Redis6) TTL(ctx context.Context, key string) (time.Duration, error)

TTL returns a key's expiration from cache, or an error if something bad happened. If the key is not found, a negative TTL is returned. If the key has no expiration, 0 (NoExpire) is returned.

type Redis7

type Redis7 struct {
	// contains filtered or unexported fields
}

Redis7 is Redis (distributed, ver.7) based implementation for Cache. It implements io.Closer, and thus it should be closed at your application shutdown.

Example
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/actforgood/xcache"
)

func main() {
	cache := xcache.NewRedis7(xcache.RedisConfig{
		Addrs: []string{"127.0.0.1:6379"},
	})

	ctx := context.Background()
	key := "example-redis"
	value := []byte("Hello Redis Cache")
	ttl := 10 * time.Minute

	// save a key for 10 minutes
	if err := cache.Save(ctx, key, value, ttl); err != nil {
		fmt.Println("could not save Redis cache key: " + err.Error())
	}

	// load the key's value
	if value, err := cache.Load(ctx, key); err != nil {
		fmt.Println("could not get Redis cache key: " + err.Error())
	} else {
		fmt.Println(string(value))
	}

	// close the cache when no needed anymore/at your application shutdown.
	if err := cache.Close(); err != nil {
		fmt.Println("could not close Redis cache: " + err.Error())
	}

	// should output:
	// Hello Redis Cache
}
Output:

Example (WithXConf)
package main

import (
	"bytes"
	"time"

	"github.com/actforgood/xcache"
	"github.com/actforgood/xconf"
)

func main() {
	// Setup the config our application will use (here used a NewFlattenLoader over a json source)
	// You can use whatever config loader suits you as long as needed xcache keys are present.
	config, err := xconf.NewDefaultConfig(
		xconf.NewFlattenLoader(xconf.JSONReaderLoader(bytes.NewReader([]byte(`{
			"xcache": {
			  "redis": {
				"addrs": [
				  "127..0.0.1:6379"
				],
				"db": 0,
				"auth": {
				  "username": "",
				  "password": ""
				},
				"timeout": {
				  "dial": "5s",
				  "read": "6s",
				  "write": "10s"
				},
				"cluster": {
				  "readonly": true
				},
				"failover": {
				  "mastername": "mymaster",
				  "auth": {
					"username": "",
					"password": ""
				  }
				}
			  }
			}
		  }`)))),
		xconf.DefaultConfigWithReloadInterval(5*time.Minute),
	)
	if err != nil {
		panic(err)
	}
	defer config.Close()

	// Initialize the cache our application will use.
	cache := xcache.NewRedis7WithConfig(config)
	defer cache.Close()

	// From this point forward you can do whatever you want with the cache.
	// Any config that gets changed, cache will reconfigure itself in a time up to reload interval (5 mins here)
	// without the need of restarting/redeploying our application.
	// For example, let's assume we notice a lot of timeout errors,
	// until we figure it out what's happening with our Redis server,
	// we can increase read/write timeouts.
}
Output:

func NewRedis7

func NewRedis7(config RedisConfig) *Redis7

NewRedis7 instantiates a new Redis7 Cache instance (compatible with Redis ver.7).

1. If the MasterName option is specified, a sentinel-backed FailoverClient is used behind. 2. If the number of Addrs is two or more, a ClusterClient is used behind. 3. Otherwise, a single-node Client is used.

func NewRedis7WithConfig

func NewRedis7WithConfig(config xconf.Config) *Redis7

NewRedis7WithConfig initializes a Redis7 Cache with configuration taken from a xconf.Config.

Keys under which configuration is expected are defined in RedisCfgKey* constants (note, you can have different config keys defined in your project, you'll have to create an alias for them to expected values by this package).

An observer is registered to xconf.DefaultConfig (which knows to reload configuration). In case any config value requested by Redis7 is changed, the Redis7 is reinitialized with the new config.

func (*Redis7) Close

func (cache *Redis7) Close() (err error)

Close closes the underlying Redis client.

func (*Redis7) Load

func (cache *Redis7) Load(ctx context.Context, key string) ([]byte, error)

Load returns a key's value from cache, or an error if something bad happened. If the key is not found, ErrNotFound is returned.

func (*Redis7) Save

func (cache *Redis7) Save(
	ctx context.Context,
	key string,
	value []byte,
	expire time.Duration,
) error

Save stores the given key-value with expiration period into cache. An expiration period equal to 0 (NoExpire) means no expiration. A negative expiration period triggers deletion of key. It returns an error if the key could not be saved.

func (*Redis7) Stats

func (cache *Redis7) Stats(ctx context.Context) (Stats, error)

Stats returns some statistics about cache memory/keys. It returns an error if something goes wrong (for example, client might not be able to connect to Redis server).

func (*Redis7) TTL

func (cache *Redis7) TTL(ctx context.Context, key string) (time.Duration, error)

TTL returns a key's expiration from cache, or an error if something bad happened. If the key is not found, a negative TTL is returned. If the key has no expiration, 0 (NoExpire) is returned.

type RedisAuth

type RedisAuth struct {
	// Username to authenticate with.
	Username string
	// Password to authenticate with.
	Password string
}

RedisAuth contains user/password authentication info.

type RedisConfig

type RedisConfig struct {
	// Addrs contains either a single address or a seed list of host:port addresses
	// of cluster/sentinel nodes.
	// Example:
	//	Addrs: []string{"redis-single-node:6379"}
	//	Addrs: []string{"redis-sentinel-node-1:26379", "redis-sentinel-node-2:26379", "redis-sentinel-node-3:26379"}
	// 	Addrs: []string{"redis-cluster-node-1:7000", "redis-cluster-node-2:7001", "redis-cluster-node-3:7002"}
	Addrs []string

	// DB is the database to be selected after connecting to the server.
	// Only for single-node and failover clients.
	DB int

	// Auth represents the auth user/pwd of redis instances.
	Auth RedisAuth

	// DialTimeout is the timeout for dial op.
	DialTimeout time.Duration
	// ReadTimeout is the timeout for read ops.
	ReadTimeout time.Duration
	// WriteTimeout is the timeout for write ops.
	WriteTimeout time.Duration

	// Enables read-only commands on slave nodes. [cluster only]
	ReadOnly bool

	// MasterName represents the sentinel master name. [failover only]
	MasterName string
	// SentinelAuth represents the auth user/pwd of redis sentinel instances. [failover only]
	SentinelAuth RedisAuth
}

RedisConfig contains commonly used information for Redis connection.

func (RedisConfig) IsCluster

func (rc RedisConfig) IsCluster() bool

IsCluster returns true if config is for a cluster configuration.

type RedisXLogger

type RedisXLogger struct {
	// contains filtered or unexported fields
}

RedisXLogger is a XLog adapter for Redis internal logging contract. Redis default logger has an unstructured format (and relies upon standard Go Logger). Through this adapter, you can achieve a structured output of the log as a whole, but the message inside will still be unstructured. See also Printf method doc.

Example
package main

import (
	"os"

	"github.com/actforgood/xcache"
	"github.com/actforgood/xlog"
)

func main() {
	// somewhere in your bootstrap process...

	// initialize an xlog.Logger
	loggerOpts := xlog.NewCommonOpts()
	loggerOpts.MinLevel = xlog.FixedLevelProvider(xlog.LevelInfo)
	loggerOpts.Source = xlog.SourceProvider(5, 1)
	logger := xlog.NewSyncLogger(os.Stdout, xlog.SyncLoggerWithOptions(loggerOpts))
	// set the xlog.Logger Redis adapter
	redisLogger := xcache.NewRedisXLogger(logger)
	xcache.SetRedis6Logger(redisLogger) // or xcache.SetRedis7Logger(redisLogger),
	// depending which ver. of Redis you're using.

	// somewhere in your shutdown process ...
	_ = logger.Close()
}
Output:

func NewRedisXLogger

func NewRedisXLogger(logger xlog.Logger) RedisXLogger

NewRedisXLogger instantiates a new RedisXLogger object.

func (RedisXLogger) Printf

func (l RedisXLogger) Printf(_ context.Context, format string, v ...any)

Printf implements redis pkg internal.Logging contract, see also https://github.com/redis/go-redis/blob/v8.11.5/internal/log.go .

Example of default redis logger output (which goes to StdErr):

redis: 2022/07/29 07:16:34 sentinel.go:661: sentinel: new master="xcacheMaster" addr="some-redis-master:6380"

Example of RedisXLogger output:

{"date":"2022-07-29T09:07:54.915902723Z","lvl":"INFO","msg":"sentinel: new master=\"xcacheMaster\" addr=\"some-redis-master:6380\"","pkg":"redis","src":"/sentinel.go:661"}

Method categorizes the message as error/info based on presence of some words like "failed"/"error". nolint:lll

type Stats

type Stats struct {
	// Memory represents the in use memory.
	// Notes:
	// - for Memory Cache it's equal to the memory size used to initialize the cache,
	// as Freecache allocates that amount of memory from the start. Thus, Memory is always equal to MaxMemory.
	// To figure out that the memory is effectively full, a raise in Evicted number of keys should be considered.
	// - for Redis Cache it's the used memory.
	Memory int64
	// MaxMemory represents the maximum memory.
	// Notes:
	// - for Memory Cache it's equal to the memory size used to initialize the cache.
	// - for Redis Cache it's the max memory Redis was configured with, or system total memory, if max memory is 0.
	// On a Redis Cluster configuration, it's calculated as the sum of max memory or system total memory of all masters.
	MaxMemory int64
	// Hits represents the number of successful accesses of keys.
	// Notes:
	// - for Redis Cache, also TTL calls to a key are reported, for Memory Cache this does not happen
	// (if you need this consistency, you can make your own Memory Cache and use Freecache's GetWithExpiration api
	// for TTL implementation - not used here as it's more costly than TTL api).
	Hits int64
	// Misses represents the number of times keys were not found.
	// Notes:
	// - for Redis Cache, also TTL calls to a not found key are reported, for Memory Cache this does not happen
	// (if you need this consistency, you can make your own Memory Cache and use Freecache's GetWithExpiration api
	// for TTL implementation - not used here as it's more costly than TTL api).
	Misses int64
	// Keys represents the current number of keys in cache.
	// Notes:
	// - for Redis Cache, if you have a Redis Cluster, this will be 0.
	Keys int64
	// Expired represents the number of expired keys reported by cache.
	Expired int64
	// Evicted represents the number of evicted keys reported by cache.
	Evicted int64
}

Stats holds memory and keys statistics.

They can be useful to be reported to metrics systems like Prometheus / DataDog, or they can just be used for debug purposes.

Note: As Redis is distributed, stats information is shared (and affected) between/by all services that use it.

func (Stats) String

func (s Stats) String() string

String implements fmt.Stringer. Returns a human friendly stats representation.

Example:

mem=1.25M maxMem=7.77G memPerc=0.02% hits=101701 misses=0 hitRate=100.00% keys=1 expired=14473 evicted=0

type StatsWatcher

type StatsWatcher struct {
	// contains filtered or unexported fields
}

StatsWatcher can be used to execute a given callback upon stats, interval based. It implements io.Closer and should be closed at your application shutdown.

Example
package main

import (
	"context"
	"fmt"
	"math/rand"
	"strconv"
	"sync"
	"time"

	"github.com/actforgood/xcache"
)

func main() {
	// initialize our application cache...
	cache := xcache.NewMemory(10 * 1024 * 1024) // 10 Mb
	ctx, cancelCtx := context.WithCancel(context.Background())

	// perform some operations upon cache to have some data...
	var wg sync.WaitGroup
	wg.Add(1)
	go generateRandomStats(ctx, cache, &wg)

	// initialize our stats watcher, which will execute a logging callback every second.
	subject := xcache.NewStatsWatcher(cache, time.Second)
	defer subject.Close() // close your watcher! (at your app shutdown eventually)

	// start watching
	subject.Watch(
		context.Background(),
		func(_ context.Context, stats xcache.Stats, err error) {
			if err != nil {
				fmt.Println("could not get cache stats:" + err.Error())
			} else { // do something useful with stats, log / sent it to a metrics system...
				fmt.Println(stats)
			}
		},
	)

	time.Sleep(3 * time.Second)
	cancelCtx() // cancel data generator goroutine
	wg.Wait()   // wait for data generator goroutine to finish

	// should output periodically something like:
	// mem=10M maxMem=10M memUsage=100.00% hits=10 misses=1 hitRate=90.91% keys=10 expired=0 evicted=0
}

func generateRandomStats(ctx context.Context, cache xcache.Cache, wg *sync.WaitGroup) {
	defer wg.Done()

	keyPrefix := "example-stats-watcher-"
	value := []byte("Hello Memory Cache")
	ttl := 5 * time.Minute

	for {
		select {
		case <-ctx.Done():
			return
		default:
			randLoop := rand.Intn(10)
			for i := 0; i <= randLoop; i++ {
				key := keyPrefix + strconv.FormatInt(time.Now().UnixNano(), 10)
				_ = cache.Save(ctx, key, value, ttl)
				_, _ = cache.Load(ctx, key)
			}
			_, _ = cache.Load(ctx, keyPrefix+"miss")
			time.Sleep(100 * time.Millisecond)
		}
	}
}
Output:

func NewStatsWatcher

func NewStatsWatcher(cache Cache, interval time.Duration) *StatsWatcher

NewStatsWatcher instantiates a new StatsWatcher object.

func (*StatsWatcher) Close

func (sw *StatsWatcher) Close() error

Close stops the underlying ticker used to execute the callback, interval based, avoiding memory leaks. It should be called at your application shutdown. It implements io.Closer interface, and the returned error can be disregarded (is nil all the time).

func (*StatsWatcher) Watch

func (sw *StatsWatcher) Watch(ctx context.Context, fn func(context.Context, Stats, error))

Watch executes fn asynchronously, interval based. Calling Watch multiple times has no effect.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL