Detailed changes
@@ -30,6 +30,7 @@ require (
github.com/muesli/mango-cobra v1.2.0
github.com/muesli/roff v0.1.0
github.com/prometheus/client_golang v1.15.1
+ github.com/redis/go-redis/v9 v9.0.4
github.com/robfig/cron/v3 v3.0.1
github.com/spf13/cobra v1.7.0
go.uber.org/automaxprocs v1.5.2
@@ -49,6 +50,7 @@ require (
github.com/caarlos0/sshmarshal v0.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
+ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
@@ -15,6 +15,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=
+github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/caarlos0/env/v8 v8.0.0 h1:POhxHhSpuxrLMIdvTGARuZqR4Jjm8AYmoi/JKlcScs0=
github.com/caarlos0/env/v8 v8.0.0/go.mod h1:7K4wMY9bH0esiXSSHlfHLX5xKGQMnkH5Fk4TDSSSzfo=
@@ -50,6 +52,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
@@ -170,6 +174,8 @@ github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
+github.com/redis/go-redis/v9 v9.0.4 h1:FC82T+CHJ/Q/PdyLW++GeCO+Ol59Y4T7R4jbgjvktgc=
+github.com/redis/go-redis/v9 v9.0.4/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -4,9 +4,11 @@ import (
"github.com/charmbracelet/soft-serve/server/cache"
"github.com/charmbracelet/soft-serve/server/cache/lru"
"github.com/charmbracelet/soft-serve/server/cache/noop"
+ "github.com/charmbracelet/soft-serve/server/cache/redis"
)
func init() {
cache.Register("lru", lru.NewCache)
cache.Register("noop", noop.NewCache)
+ cache.Register("redis", redis.NewCache)
}
@@ -0,0 +1,47 @@
+package redis
+
+import (
+ "github.com/charmbracelet/soft-serve/server/config"
+)
+
+// Config is the configuration for the Redis cache.
+type Config struct {
+ // Addr is the Redis address [host][:port].
+ Addr string `env:"ADDR" yaml:"addr"`
+ // Username is the Redis username.
+ Username string `env:"USERNAME" yaml:"username"`
+ // Password is the Redis password.
+ Password string `env:"PASSWORD" yaml:"password"`
+ // DB is the Redis database.
+ DB int `env:"DB" yaml:"db"`
+}
+
+// NewConfig returns a new Redis cache configuration.
+// If path is empty, the default config path will be used.
+//
+// TODO: add support for TLS and other Redis options.
+func NewConfig(path string) (*Config, error) {
+ if path == "" {
+ path = config.DefaultConfig().FilePath()
+ }
+
+ cfg := DefaultConfig()
+ wrapper := &struct {
+ Redis *Config `envPrefix:"REDIS_" yaml:"redis"`
+ }{
+ Redis: cfg,
+ }
+
+ if err := config.ParseConfig(wrapper, path); err != nil {
+ return cfg, err
+ }
+
+ return cfg, nil
+}
+
+// DefaultConfig returns the default configuration for the Redis cache.
+func DefaultConfig() *Config {
+ return &Config{
+ Addr: "localhost:6379",
+ }
+}
@@ -0,0 +1,88 @@
+package redis
+
+import (
+ "context"
+ "time"
+
+ "github.com/charmbracelet/soft-serve/server/cache"
+ "github.com/redis/go-redis/v9"
+)
+
+// Cache is a Redis cache.
+type Cache struct {
+ client *redis.Client
+}
+
+// NewCache returns a new Redis cache.
+// It converts non-string types to JSON before storing/retrieving them.
+func NewCache(ctx context.Context, _ ...cache.Option) (cache.Cache, error) {
+ cfg, err := NewConfig("")
+ if err != nil {
+ return nil, err
+ }
+
+ client := redis.NewClient(&redis.Options{
+ Addr: cfg.Addr,
+ Username: cfg.Username,
+ Password: cfg.Password,
+ DB: cfg.DB,
+ })
+
+ return &Cache{
+ client: client,
+ }, client.Ping(ctx).Err()
+}
+
+type option struct {
+ cache.Item
+ ttl time.Duration
+}
+
+func (*option) item() {}
+
+// WithTTL sets the TTL for the cache item.
+func WithTTL(ttl time.Duration) cache.ItemOption {
+ return func(io cache.Item) {
+ i := io.(*option)
+ i.ttl = ttl
+ }
+}
+
+// Contains implements cache.Cache.
+func (r *Cache) Contains(ctx context.Context, key string) bool {
+ return r.client.Exists(ctx, key).Val() == 1
+}
+
+// Delete implements cache.Cache.
+func (r *Cache) Delete(ctx context.Context, key string) {
+ r.client.Del(ctx, key)
+}
+
+// Get implements cache.Cache.
+func (r *Cache) Get(ctx context.Context, key string) (value any, ok bool) {
+ val := r.client.Get(ctx, key)
+ if val.Err() != nil {
+ return nil, false
+ }
+
+ return val.Val(), true
+}
+
+// Keys implements cache.Cache.
+func (r *Cache) Keys(ctx context.Context) []string {
+ return r.client.Keys(ctx, "*").Val()
+}
+
+// Len implements cache.Cache.
+func (r *Cache) Len(ctx context.Context) int64 {
+ return r.client.DBSize(ctx).Val()
+}
+
+// Set implements cache.Cache.
+func (r *Cache) Set(ctx context.Context, key string, val any, opts ...cache.ItemOption) {
+ var opt option
+ for _, o := range opts {
+ o(&opt)
+ }
+ r.client.Set(ctx, key, val, opt.ttl)
+}