diff --git a/go.mod b/go.mod index 641587058abaf37463193762bb60084b0368cccd..4db01719428602afc2da900226f304f70a409bfa 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 8928e0cb9f894b9fa890b465dc026b9f2d8def78..72f31daac594737df1d9dc61f274411a77445247 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/init/cache.go b/internal/init/cache.go index c64aeb93894119a80242123823c7b8ab945102d9..d733b98b09a9c5b4d8366fae99e3d38e20958807 100644 --- a/internal/init/cache.go +++ b/internal/init/cache.go @@ -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) } diff --git a/server/cache/redis/config.go b/server/cache/redis/config.go new file mode 100644 index 0000000000000000000000000000000000000000..cd4b27bfebbf20579252bf9565fc1b16df6b150e --- /dev/null +++ b/server/cache/redis/config.go @@ -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", + } +} diff --git a/server/cache/redis/redis.go b/server/cache/redis/redis.go new file mode 100644 index 0000000000000000000000000000000000000000..1e80e74a782e808c4106be22c8dadb26b0fec80c --- /dev/null +++ b/server/cache/redis/redis.go @@ -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) +}