Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ type RedisConfig struct {
TLS bool `conf:"REDIS_TLS"`
Username string `conf:"REDIS_USERNAME"`
Password string `conf:"REDIS_PASSWORD"`
Cluster bool `conf:"REDIS_CLUSTER"`
}

// ConsulConfig configures the optional Consul integration.
Expand Down
5 changes: 5 additions & 0 deletions config/config_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ func validateConfigDatabases(result *ct.ValidationResult, c *Config, loggers ldl
return // no point doing further database config validation if it's in this state
}

if c.Redis.URL.IsDefined() && c.Redis.Cluster {
loggers.Warn("Redis cluster mode (REDIS_CLUSTER) applies only to the feature data store; " +
"the Redis Big Segments store is not cluster-aware and should not be relied upon in this configuration")
}

if c.Consul.Host != "" {
if c.Consul.Token != "" && c.Consul.TokenFile != "" {
result.AddError(nil, errConsulTokenAndTokenFile)
Expand Down
27 changes: 27 additions & 0 deletions config/test_data_configs_valid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ func makeValidConfigs() []testDataValidConfig {
makeValidConfigOfflineModeWithMonitoringInterval("5m"),
makeValidConfigRedisMinimal(),
makeValidConfigRedisAll(),
makeValidConfigRedisCluster(),
makeValidConfigRedisURL(),
makeValidConfigRedisPortOnly(),
makeValidConfigRedisDockerPort(),
Expand Down Expand Up @@ -472,6 +473,32 @@ LocalTTL = 3s
return c
}

func makeValidConfigRedisCluster() testDataValidConfig {
c := testDataValidConfig{name: "Redis - cluster mode"}
c.makeConfig = func(c *Config) {
c.Redis = RedisConfig{
URL: newOptURLAbsoluteMustBeValid("redis://redishost:6400"),
TLS: true,
Cluster: true,
}
}
c.envVars = map[string]string{
"USE_REDIS": "1",
"REDIS_HOST": "redishost",
"REDIS_PORT": "6400",
"REDIS_TLS": "1",
"REDIS_CLUSTER": "1",
}
c.fileContent = `
[Redis]
Host = "redishost"
Port = 6400
TLS = 1
Cluster = 1
`
return c
}

func makeValidConfigRedisURL() testDataValidConfig {
c := testDataValidConfig{name: "Redis - URL instead of host/port"}
c.makeConfig = func(c *Config) {
Expand Down
8 changes: 8 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ To learn more, read [Persistent storage](./persistent-storage.md).
| `tls` | `REDIS_TLS` | Boolean | `false` | If `true`, will use a secure connection to Redis (not all Redis servers support this). If you specified a `redis://` URL, setting `tls` to `true` will change it to `rediss://`. |
| `password` | `REDIS_PASSWORD` | String | | Optional password if Redis requires authentication. |
| `username` | `REDIS_USERNAME` | String | | Optional username if Redis requires authentication. |
| `cluster` | `REDIS_CLUSTER` | Boolean | `false` | If `true`, connect in Redis cluster mode. Use this with a managed cluster reached through a single configuration endpoint, such as AWS MemoryDB or ElastiCache (cluster mode enabled). See below. |
| `localTtl` | `CACHE_TTL` | Duration | `30s` | Length of time that database items can be cached in memory. |

Note that the TLS and password options can also be specified as part of the URL: `rediss://` instead of `redis://`
Expand All @@ -213,6 +214,13 @@ username and password.
You may want to use the separate options instead if, for instance, you want your configuration file to contain the basic
Redis configuration, but for security reasons you would rather set the password in an environment variable (`REDIS_PASSWORD`).

Setting `cluster` (`REDIS_CLUSTER`) to `true` connects using a cluster-aware client, seeded from the single
configured `host`/`url` (the cluster's configuration endpoint), and stores all keys under a `{ld}.` hash-tag prefix so
that the multi-key operations the data store relies on stay within one hash slot. This targets managed cluster stores such
as AWS MemoryDB and ElastiCache with cluster mode enabled. Note that this makes the Relay Proxy _compatible with_ cluster
Redis; it does not shard the Relay Proxy's data across nodes (its data set is small). Cluster mode currently applies to the
feature data store only — the Big Segments store is not cluster-aware.


### File section: `[DynamoDB]`

Expand Down
2 changes: 1 addition & 1 deletion docs/persistent-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ You can configure Relay Proxy nodes to persist feature flag settings in Redis, D

To learn more, read [Using a persistent feature store](https://docs.launchdarkly.com/sdk/concepts/data-stores), and the Relay Proxy documentation on [Configuration](./configuration.md).

The Relay Proxy does not support clustered Redis or Redis Sentinel.
The Relay Proxy supports clustered Redis for the feature data store by setting `cluster` / `REDIS_CLUSTER` to `true` (see [Configuration](./configuration.md)); this works with managed cluster stores such as AWS MemoryDB and ElastiCache (cluster mode enabled). Cluster mode currently covers the feature data store only, not the Big Segments store. Redis Sentinel is not supported.

**Note:** The Redis configurations should also work with [Valkey](https://valkey.io/), as Valkey maintains Redis compatibility.

Expand Down
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@ require (
github.com/launchdarkly/go-server-sdk-consul/v3 v3.0.2
github.com/launchdarkly/go-server-sdk-dynamodb/v4 v4.0.3
github.com/launchdarkly/go-server-sdk-evaluation/v3 v3.0.1
github.com/launchdarkly/go-server-sdk-redis-go-redis v1.1.2
github.com/launchdarkly/go-server-sdk-redis-redigo/v3 v3.0.4
github.com/launchdarkly/go-server-sdk/v7 v7.15.3
github.com/launchdarkly/go-test-helpers/v3 v3.1.0
github.com/launchdarkly/opencensus-go-exporter-stackdriver v0.14.7
github.com/pborman/uuid v1.2.1
github.com/prometheus/client_golang v1.23.2 // indirect; override to address CVE-2022-21698
github.com/redis/go-redis/v9 v9.1.0
github.com/stretchr/testify v1.11.1
go.opencensus.io v0.24.0
golang.org/x/sync v0.21.0
Expand Down Expand Up @@ -145,3 +147,8 @@ require (
)

retract v8.19.4 // Introduced unintentional breaking changes; use version v8.19.5 or later.

// TEMPORARY (fork build — do not merge upstream): build against the actblue fork of the go-redis
// integration that adds ForceClusterMode (branch feat/force-cluster-mode). Remove once that change
// is released upstream and pin the released version above.
replace github.com/launchdarkly/go-server-sdk-redis-go-redis => github.com/actblue/go-server-sdk-redis-go-redis v0.0.0-20260724162236-4deaf9f9f8b2
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ github.com/DataDog/opencensus-go-exporter-datadog v0.0.0-20220622145613-731d59e8
github.com/DataDog/opencensus-go-exporter-datadog v0.0.0-20220622145613-731d59e8b567/go.mod h1:/VV3EFO/hTNQZHAqaj+CPGy2+ioFrP4EX3iRwozubhQ=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/actblue/go-server-sdk-redis-go-redis v0.0.0-20260724162236-4deaf9f9f8b2 h1:lobdskQ/ftiI1F9PzBwCwDTZDpYbVNQeeIpSyUZKGbo=
github.com/actblue/go-server-sdk-redis-go-redis v0.0.0-20260724162236-4deaf9f9f8b2/go.mod h1:MCHcQhcFu7VS4J4tP6Z3qP8JvP9j4f59r2oQx9WFONI=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
Expand Down Expand Up @@ -104,6 +106,10 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0=
github.com/bsm/ginkgo/v2 v2.9.5/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw=
Expand Down Expand Up @@ -468,6 +474,8 @@ github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlT
github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI=
github.com/prometheus/statsd_exporter v0.23.1 h1:TiNAE1XevlZZrpSbmf51l/Ryl2Eek9rYh//KlvcNvKw=
github.com/prometheus/statsd_exporter v0.23.1/go.mod h1:FFmnBRWf+HxX+PR+2fnc0ciBIONVAPJ6k4lqIbdqVxo=
github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY=
github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
Expand Down
63 changes: 63 additions & 0 deletions internal/sdks/data_stores.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package sdks

import (
"context"
"crypto/tls"
"errors"
"net/url"
"strings"

"github.com/launchdarkly/ld-relay/v8/config"
Expand All @@ -11,6 +13,7 @@ import (
"github.com/launchdarkly/go-sdk-common/v3/ldlog"
ldconsul "github.com/launchdarkly/go-server-sdk-consul/v3"
lddynamodb "github.com/launchdarkly/go-server-sdk-dynamodb/v4"
ldredisgoredis "github.com/launchdarkly/go-server-sdk-redis-go-redis"
ldredis "github.com/launchdarkly/go-server-sdk-redis-redigo/v3"
"github.com/launchdarkly/go-server-sdk/v7/ldcomponents"
"github.com/launchdarkly/go-server-sdk/v7/subsystems"
Expand All @@ -20,6 +23,7 @@ import (
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
redigo "github.com/gomodule/redigo/redis"
consul "github.com/hashicorp/consul/api"
goredis "github.com/redis/go-redis/v9"
)

var (
Expand Down Expand Up @@ -57,6 +61,33 @@ func ConfigureDataStore(
if allConfig.Redis.URL.IsDefined() {
// Our config validation already takes care of normalizing the Redis parameters so that if a
// host & port were specified, they are transformed into a URL.

if allConfig.Redis.Cluster {
// Cluster mode uses the go-redis integration (the redigo integration is not
// cluster-aware). This supports managed cluster stores such as AWS MemoryDB or
// ElastiCache (cluster mode enabled), which are reached through a single configuration
// endpoint.
clusterBuilder, redisURL, err := makeRedisClusterDataStoreBuilder(allConfig, envConfig)
if err != nil {
return nil, DataStoreEnvironmentInfo{}, err
}
redactedURL := util.RedactURL(redisURL)

loggers.Infof("Using Redis data store (cluster mode): %s with prefix: %s", redactedURL, envConfig.Prefix)

storeInfo := DataStoreEnvironmentInfo{
DBType: "redis",
DBServer: redactedURL,
DBPrefix: envConfig.Prefix,
}
if storeInfo.DBPrefix == "" {
storeInfo.DBPrefix = ldredisgoredis.DefaultPrefix
}

return ldcomponents.PersistentDataStore(clusterBuilder).
CacheTime(allConfig.Redis.LocalTTL.GetOrElse(config.DefaultDatabaseCacheTTL)), storeInfo, nil
}

redisBuilder, redisURL := makeRedisDataStoreBuilder(ldredis.DataStore, allConfig, envConfig)
redactedURL := util.RedactURL(redisURL)

Expand Down Expand Up @@ -169,6 +200,38 @@ func makeRedisDataStoreBuilder[T any](
return b, redisURL
}

// makeRedisClusterDataStoreBuilder builds a go-redis-based data store configured for Redis cluster
// mode. Unlike the redigo integration used by makeRedisDataStoreBuilder, the go-redis integration is
// cluster-aware; ForceClusterMode lets a single configuration endpoint (e.g. AWS MemoryDB) be used
// as a cluster seed, and applies the {ld}. hash-tag prefix so the store's multi-key operations stay
// within one hash slot.
func makeRedisClusterDataStoreBuilder(
allConfig config.Config,
envConfig config.EnvConfig,
) (builder *ldredisgoredis.DataStoreBuilder, redisURL string, err error) {
redisURL, prefix := GetRedisBasicProperties(allConfig.Redis, envConfig)

parsed, err := url.Parse(redisURL)
if err != nil {
return nil, redisURL, err
}

opts := goredis.UniversalOptions{
Addrs: []string{parsed.Host},
Username: allConfig.Redis.Username,
Password: allConfig.Redis.Password,
}
if allConfig.Redis.TLS {
opts.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12}
}

builder = ldredisgoredis.DataStore().
Options(opts).
Prefix(prefix).
ForceClusterMode(true)
return builder, redisURL, nil
}

// GetDynamoDBBasicProperties transforms the configuration properties to the standard parameters
// used for DynamoDB. This function is exported to ensure consistency between the SDK
// configuration and the internal big segment store for DynamoDB.
Expand Down
Loading