Skip to content

Commit

Permalink
link memcached
Browse files Browse the repository at this point in the history
  • Loading branch information
woutslakhorst committed Nov 11, 2024
1 parent 877531b commit 39aa9aa
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 25 deletions.
1 change: 1 addition & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ The following options can be configured on the server:
pki.maxupdatefailhours 4 Maximum number of hours that a denylist update can fail
pki.softfail true Do not reject certificates if their revocation status cannot be established when softfail is true
**Storage**
storage.session.memcached.address [] List of Memcached server addresses. These can be a simple 'host:port' or a Memcached connection URL with scheme, auth and other options.
storage.session.redis.address Redis session database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. If not set it, defaults to an in-memory database.
storage.session.redis.database Redis session database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance.
storage.session.redis.password Redis session database password. If set, it overrides the username in the connection URL.
Expand Down
1 change: 1 addition & 0 deletions docs/pages/deployment/server_options.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
pki.maxupdatefailhours 4 Maximum number of hours that a denylist update can fail
pki.softfail true Do not reject certificates if their revocation status cannot be established when softfail is true
**Storage**
storage.session.memcached.address [] List of Memcached server addresses. These can be a simple 'host:port' or a Memcached connection URL with scheme, auth and other options.
storage.session.redis.address Redis session database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. If not set it, defaults to an in-memory database.
storage.session.redis.database Redis session database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance.
storage.session.redis.password Redis session database password. If set, it overrides the username in the connection URL.
Expand Down
12 changes: 7 additions & 5 deletions storage/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,14 @@ func FlagSet() *pflag.FlagSet {
"Note: using SQLite is not recommended in production environments. "+
"If using SQLite anyways, remember to enable foreign keys ('_foreign_keys=on') and the write-ahead-log ('_journal_mode=WAL').")

flagSet.String("storage.session.redis.address", defs.Redis.Address, "Redis session database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. "+
flagSet.StringSlice("storage.session.memcached.address", defs.Session.Memcached.Address, "List of Memcached server addresses. These can be a simple 'host:port' or a Memcached connection URL with scheme, auth and other options.")

flagSet.String("storage.session.redis.address", defs.Session.Redis.Address, "Redis session database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. "+
"If not set it, defaults to an in-memory database.")
flagSet.String("storage.session.redis.username", defs.Redis.Username, "Redis session database username. If set, it overrides the username in the connection URL.")
flagSet.String("storage.session.redis.password", defs.Redis.Password, "Redis session database password. If set, it overrides the username in the connection URL.")
flagSet.String("storage.session.redis.database", defs.Redis.Database, "Redis session database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance.")
flagSet.String("storage.session.redis.tls.truststorefile", defs.Redis.TLS.TrustStoreFile, "PEM file containing the trusted CA certificate(s) for authenticating remote Redis session servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address).")
flagSet.String("storage.session.redis.username", defs.Session.Redis.Username, "Redis session database username. If set, it overrides the username in the connection URL.")
flagSet.String("storage.session.redis.password", defs.Session.Redis.Password, "Redis session database password. If set, it overrides the username in the connection URL.")
flagSet.String("storage.session.redis.database", defs.Session.Redis.Database, "Redis session database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance.")
flagSet.String("storage.session.redis.tls.truststorefile", defs.Session.Redis.TLS.TrustStoreFile, "PEM file containing the trusted CA certificate(s) for authenticating remote Redis session servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address).")

return flagSet
}
4 changes: 3 additions & 1 deletion storage/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type SQLConfig struct {

// SessionConfig specifies config for the session storage engine.
type SessionConfig struct {
// Type is the type of session storage engine to use.
// Memcached specifies config for the Memcached session storage engine.
Memcached MemcachedConfig `koanf:"memcached"`
// Redis specifies config for the Redis session storage engine.
Redis RedisConfig `koanf:"redis"`
}
12 changes: 12 additions & 0 deletions storage/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ func (e *engine) Configure(config core.ServerConfig) error {

// session storage
redisConfig := e.config.Session.Redis
memcachedConfig := e.config.Session.Memcached
if redisConfig.isConfigured() && memcachedConfig.isConfigured() {
return errors.New("only one of 'storage.session.redis' and 'storage.session.memcached' can be configured")
}
if redisConfig.isConfigured() {
redisDB, err := createRedisDatabase(redisConfig)
if err != nil {
Expand All @@ -188,6 +192,14 @@ func (e *engine) Configure(config core.ServerConfig) error {
return fmt.Errorf("unable to configure redis client: %w", err)
}
e.sessionDatabase = NewRedisSessionDatabase(client, redisConfig.Database)
log.Logger().Info("Redis session storage support enabled.")
} else if memcachedConfig.isConfigured() {
memcachedClient, err := newMemcachedClient(memcachedConfig)
if err != nil {
return fmt.Errorf("unable to initialize memcached client: %w", err)
}
e.sessionDatabase = NewMemcachedSessionDatabase(memcachedClient)
log.Logger().Info("Memcached session storage support enabled.")
} else {
e.sessionDatabase = NewInMemorySessionDatabase()
}
Expand Down
41 changes: 35 additions & 6 deletions storage/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,20 @@ package storage

import (
"errors"
"fmt"
"os"
"path"
"strings"
"testing"
"time"

"github.com/alicebob/miniredis/v2"
"github.com/nuts-foundation/go-stoabs"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/test/io"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"os"
"path"
"strings"
"testing"
"time"
)

func Test_New(t *testing.T) {
Expand Down Expand Up @@ -261,7 +263,7 @@ func TestEngine_CheckHealth(t *testing.T) {
})
}

func Test_engine_redisSessionDatabase(t *testing.T) {
func Test_engine_sessionDatabase(t *testing.T) {
t.Run("redis", func(t *testing.T) {
redis := miniredis.RunT(t)
e := New().(*engine)
Expand All @@ -278,4 +280,31 @@ func Test_engine_redisSessionDatabase(t *testing.T) {
})
assert.IsType(t, redisSessionDatabase{}, e.GetSessionDatabase())
})
t.Run("memcached", func(t *testing.T) {
memcached := memcachedTestServer(t)
e := New().(*engine)
e.config = Config{
Session: SessionConfig{
Memcached: MemcachedConfig{Address: []string{fmt.Sprintf("localhost:%d", memcached.Port())}},
},
}
dataDir := io.TestDirectory(t)
require.NoError(t, e.Configure(core.ServerConfig{Datadir: dataDir}))
require.NoError(t, e.Start())
t.Cleanup(func() {
_ = e.Shutdown()
})
assert.IsType(t, &MemcachedSessionDatabase{}, e.GetSessionDatabase())
})
t.Run("error on both redis and memcached", func(t *testing.T) {
e := New().(*engine)
e.config = Config{
Session: SessionConfig{
Memcached: MemcachedConfig{Address: []string{"localhost:1111"}},
Redis: RedisConfig{Address: "localhost:1111"},
},
}
dataDir := io.TestDirectory(t)
require.Error(t, e.Configure(core.ServerConfig{Datadir: dataDir}))
})
}
42 changes: 42 additions & 0 deletions storage/memcached.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2024 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package storage

import "github.com/bradfitz/gomemcache/memcache"

// MemcachedConfig holds the configuration for the memcached storage.
type MemcachedConfig struct {
Address []string `koanf:"address"`
}

// isConfigured returns true if config the indicates Redis support should be enabled.
func (r MemcachedConfig) isConfigured() bool {
return len(r.Address) > 0
}

// newMemcachedClient creates a memcache.Client and performs a Ping()
func newMemcachedClient(config MemcachedConfig) (*memcache.Client, error) {
client := memcache.New(config.Address...)
err := client.Ping()
if err != nil {
_ = client.Close()
return nil, err
}
return client, err
}
51 changes: 51 additions & 0 deletions storage/memcached_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2024 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package storage

import (
"fmt"
"github.com/daangn/minimemcached"
"github.com/stretchr/testify/require"
"testing"
)

func Test_newMemcachedClient(t *testing.T) {
port, err := getRandomAvailablePort()
if err != nil {
t.Fatal(err)
}

cfg := &minimemcached.Config{
Port: uint16(port),
}
m, err := minimemcached.Run(cfg)
if err != nil {
t.Fatal(err)
}

client, err := newMemcachedClient(MemcachedConfig{Address: []string{
fmt.Sprintf("localhost:%d", m.Port()),
}})

defer client.Close()
defer m.Close()

require.NoError(t, err)
require.NotNil(t, client)
}
2 changes: 1 addition & 1 deletion storage/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ type RedisConfig struct {

// isConfigured returns true if config the indicates Redis support should be enabled.
func (r RedisConfig) isConfigured() bool {
return len(r.Address) > 0
return r.Sentinel.enabled() || len(r.Address) > 0
}

func (r RedisConfig) parse() (*redis.Options, error) {
Expand Down
13 changes: 9 additions & 4 deletions storage/session_memcached.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ import (
)

type MemcachedSessionDatabase struct {
client *memcache.Client
underlying *cache.Cache[[]byte]
}

func NewMemcachedSessionDatabase(addresses ...string) *MemcachedSessionDatabase {
redisStore := memcachestore.NewMemcache(memcache.New(addresses...), store.WithExpiration(5*time.Minute))
// NewMemcachedSessionDatabase creates a new MemcachedSessionDatabase using an initialized memcache.Client.
func NewMemcachedSessionDatabase(client *memcache.Client) *MemcachedSessionDatabase {
memcachedStore := memcachestore.NewMemcache(client, store.WithExpiration(5*time.Minute))
return &MemcachedSessionDatabase{
underlying: cache.New[[]byte](redisStore),
underlying: cache.New[[]byte](memcachedStore),
}
}

Expand All @@ -49,7 +51,10 @@ func (s MemcachedSessionDatabase) GetStore(ttl time.Duration, keys ...string) Se
}

func (s MemcachedSessionDatabase) close() {
// nop
// noop
if s.client != nil {
_ = s.client.Close()
}
}

func (s MemcachedSessionDatabase) getFullKey(prefixes []string, key string) string {
Expand Down
Loading

0 comments on commit 39aa9aa

Please sign in to comment.