-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This new backend allows using a Redis Sentinel cluster to reliably store objects in the cache. With the redundancy and automatic master switching in a Redis Sentinel cluster, it ensures that the service and data stays available even if some of the storage nodes go down. Please note that due to the delay in Redis replication, some of the most recent data inserted in the current master can be lost if this host goes down before having a chance to replicate the change. It also takes some time for Sentinel to detect a host going down and elect a new master, so a short outage may occur when the current master fails. Redis Sentinel documentation (https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/) recommends a minimum of 3 nodes and gives several architecture examples.
- Loading branch information
1 parent
84a0690
commit 0f2eca0
Showing
9 changed files
with
381 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
package backends | ||
|
||
import ( | ||
"context" | ||
"crypto/tls" | ||
"time" | ||
|
||
"github.com/prebid/prebid-cache/config" | ||
"github.com/prebid/prebid-cache/utils" | ||
"github.com/redis/go-redis/v9" | ||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
// RedisSentinelDB is an interface that helps us communicate with an instance of a | ||
// Redis Sentinel database. Its implementation is intended to use the "github.com/redis/go-redis" | ||
// client | ||
type RedisSentinelDB interface { | ||
Get(ctx context.Context, key string) (string, error) | ||
Put(ctx context.Context, key string, value string, ttlSeconds int) (bool, error) | ||
} | ||
|
||
// RedisSentinelDBClient is a wrapper for the Redis client that implements the RedisSentinelDB interface | ||
type RedisSentinelDBClient struct { | ||
client *redis.Client | ||
} | ||
|
||
// Get returns the value associated with the provided `key` parameter | ||
func (db RedisSentinelDBClient) Get(ctx context.Context, key string) (string, error) { | ||
return db.client.Get(ctx, key).Result() | ||
} | ||
|
||
// Put will set 'key' to hold string 'value' if 'key' does not exist in the redis storage. | ||
// When key already holds a value, no operation is performed. That's the reason this adapter | ||
// uses the 'github.com/go-redis/redis's library SetNX. SetNX is short for "SET if Not eXists". | ||
func (db RedisSentinelDBClient) Put(ctx context.Context, key, value string, ttlSeconds int) (bool, error) { | ||
return db.client.SetNX(ctx, key, value, time.Duration(ttlSeconds)*time.Second).Result() | ||
} | ||
|
||
// RedisSentinelBackend when initialized will instantiate and configure the Redis client. It implements | ||
// the Backend interface. | ||
type RedisSentinelBackend struct { | ||
cfg config.RedisSentinel | ||
client RedisDB | ||
} | ||
|
||
// NewRedisSentinelBackend initializes the Redis Sentinel client and pings to make sure connection was successful | ||
func NewRedisSentinelBackend(cfg config.RedisSentinel, ctx context.Context) *RedisSentinelBackend { | ||
options := &redis.FailoverOptions{ | ||
MasterName: cfg.MasterName, | ||
SentinelAddrs: cfg.SentinelAddrs, | ||
Password: cfg.Password, | ||
DB: cfg.Db, | ||
} | ||
|
||
if cfg.TLS.Enabled { | ||
options.TLSConfig = &tls.Config{InsecureSkipVerify: cfg.TLS.InsecureSkipVerify} | ||
} | ||
|
||
client := RedisSentinelDBClient{client: redis.NewFailoverClient(options)} | ||
|
||
_, err := client.client.Ping(ctx).Result() | ||
if err != nil { | ||
log.Fatalf("Error creating Redis Sentinel backend: %v", err) | ||
} | ||
log.Infof("Connected to Redis Sentinels at %v", cfg.SentinelAddrs) | ||
|
||
return &RedisSentinelBackend{ | ||
cfg: cfg, | ||
client: client, | ||
} | ||
} | ||
|
||
// Get calls the Redis Sentinel client to return the value associated with the provided `key` | ||
// parameter and interprets its response. A `Nil` error reply of the Redis client means | ||
// the `key` does not exist. | ||
func (b *RedisSentinelBackend) Get(ctx context.Context, key string) (string, error) { | ||
res, err := b.client.Get(ctx, key) | ||
if err == redis.Nil { | ||
err = utils.NewPBCError(utils.KEY_NOT_FOUND) | ||
} | ||
|
||
return res, err | ||
} | ||
|
||
// Put writes the `value` under the provided `key` in the Redis Sentinel storage server. Because the backend | ||
// implementation of Put calls SetNX(item *Item), a `false` return value is interpreted as the data | ||
// not being written because the `key` already holds a value, and a RecordExistsError is returned | ||
func (b *RedisSentinelBackend) Put(ctx context.Context, key string, value string, ttlSeconds int) error { | ||
success, err := b.client.Put(ctx, key, value, ttlSeconds) | ||
if err != nil && err != redis.Nil { | ||
return err | ||
} | ||
if !success { | ||
return utils.NewPBCError(utils.RECORD_EXISTS) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
package backends | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"testing" | ||
|
||
"github.com/prebid/prebid-cache/utils" | ||
"github.com/redis/go-redis/v9" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestRedisSentinelClientGet(t *testing.T) { | ||
redisSentinelBackend := &RedisSentinelBackend{} | ||
|
||
type testInput struct { | ||
client RedisDB | ||
key string | ||
} | ||
|
||
type testExpectedValues struct { | ||
value string | ||
err error | ||
} | ||
|
||
testCases := []struct { | ||
desc string | ||
in testInput | ||
expected testExpectedValues | ||
}{ | ||
{ | ||
desc: "RedisSentinelBackend.Get() throws a redis.Nil error", | ||
in: testInput{ | ||
client: FakeRedisClient{ | ||
Success: false, | ||
ServerError: redis.Nil, | ||
}, | ||
key: "someKeyThatWontBeFound", | ||
}, | ||
expected: testExpectedValues{ | ||
value: "", | ||
err: utils.NewPBCError(utils.KEY_NOT_FOUND), | ||
}, | ||
}, | ||
{ | ||
desc: "RedisBackend.Get() throws an error different from redis.Nil", | ||
in: testInput{ | ||
client: FakeRedisClient{ | ||
Success: false, | ||
ServerError: errors.New("some other get error"), | ||
}, | ||
key: "someKey", | ||
}, | ||
expected: testExpectedValues{ | ||
value: "", | ||
err: errors.New("some other get error"), | ||
}, | ||
}, | ||
{ | ||
desc: "RedisBackend.Get() doesn't throw an error", | ||
in: testInput{ | ||
client: FakeRedisClient{ | ||
Success: true, | ||
StoredData: map[string]string{"defaultKey": "aValue"}, | ||
}, | ||
key: "defaultKey", | ||
}, | ||
expected: testExpectedValues{ | ||
value: "aValue", | ||
err: nil, | ||
}, | ||
}, | ||
} | ||
|
||
for _, tt := range testCases { | ||
redisSentinelBackend.client = tt.in.client | ||
|
||
// Run test | ||
actualValue, actualErr := redisSentinelBackend.Get(context.Background(), tt.in.key) | ||
|
||
// Assertions | ||
assert.Equal(t, tt.expected.value, actualValue, tt.desc) | ||
assert.Equal(t, tt.expected.err, actualErr, tt.desc) | ||
} | ||
} | ||
|
||
func TestRedisSentinelClientPut(t *testing.T) { | ||
redisSentinelBackend := &RedisSentinelBackend{} | ||
|
||
type testInput struct { | ||
redisSentinelClient RedisDB | ||
key string | ||
valueToStore string | ||
ttl int | ||
} | ||
|
||
type testExpectedValues struct { | ||
writtenValue string | ||
redisClientErr error | ||
} | ||
|
||
testCases := []struct { | ||
desc string | ||
in testInput | ||
expected testExpectedValues | ||
}{ | ||
{ | ||
desc: "Try to overwrite already existing key. From redis client documentation, SetNX returns 'false' because no operation is performed", | ||
in: testInput{ | ||
redisSentinelClient: FakeRedisClient{ | ||
Success: false, | ||
StoredData: map[string]string{"key": "original value"}, | ||
ServerError: redis.Nil, | ||
}, | ||
key: "key", | ||
valueToStore: "overwrite value", | ||
ttl: 10, | ||
}, | ||
expected: testExpectedValues{ | ||
redisClientErr: utils.NewPBCError(utils.RECORD_EXISTS), | ||
writtenValue: "original value", | ||
}, | ||
}, | ||
{ | ||
desc: "When key does not exist, redis.Nil is returned. Other errors should be interpreted as a server side error. Expect error.", | ||
in: testInput{ | ||
redisSentinelClient: FakeRedisClient{ | ||
Success: true, | ||
StoredData: map[string]string{}, | ||
ServerError: errors.New("A Redis client side error"), | ||
}, | ||
key: "someKey", | ||
valueToStore: "someValue", | ||
ttl: 10, | ||
}, | ||
expected: testExpectedValues{ | ||
redisClientErr: errors.New("A Redis client side error"), | ||
}, | ||
}, | ||
{ | ||
desc: "In Redis, a zero ttl value means no expiration. Expect value to be successfully set", | ||
in: testInput{ | ||
redisSentinelClient: FakeRedisClient{ | ||
StoredData: map[string]string{}, | ||
Success: true, | ||
ServerError: redis.Nil, | ||
}, | ||
key: "defaultKey", | ||
valueToStore: "aValue", | ||
ttl: 0, | ||
}, | ||
expected: testExpectedValues{ | ||
writtenValue: "aValue", | ||
}, | ||
}, | ||
{ | ||
desc: "RedisBackend.Put() successful, no need to set defaultTTL because ttl is greater than zero", | ||
in: testInput{ | ||
redisSentinelClient: FakeRedisClient{ | ||
StoredData: map[string]string{}, | ||
Success: true, | ||
ServerError: redis.Nil, | ||
}, | ||
key: "defaultKey", | ||
valueToStore: "aValue", | ||
ttl: 1, | ||
}, | ||
expected: testExpectedValues{ | ||
writtenValue: "aValue", | ||
}, | ||
}, | ||
} | ||
|
||
for _, tt := range testCases { | ||
// Assign redis backend client | ||
redisSentinelBackend.client = tt.in.redisSentinelClient | ||
|
||
// Run test | ||
actualErr := redisSentinelBackend.Put(context.Background(), tt.in.key, tt.in.valueToStore, tt.in.ttl) | ||
|
||
// Assertions | ||
assert.Equal(t, tt.expected.redisClientErr, actualErr, tt.desc) | ||
|
||
// Put error | ||
assert.Equal(t, tt.expected.redisClientErr, actualErr, tt.desc) | ||
|
||
if actualErr == nil || actualErr == utils.NewPBCError(utils.RECORD_EXISTS) { | ||
// Either a value was inserted successfully or the record already existed. | ||
// Assert data in the backend | ||
storage, ok := tt.in.redisSentinelClient.(FakeRedisClient) | ||
assert.True(t, ok, tt.desc) | ||
assert.Equal(t, tt.expected.writtenValue, storage.StoredData[tt.in.key], tt.desc) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.