Skip to content
Open
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
42 changes: 40 additions & 2 deletions lib/java-server-sdk-redis-store/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ This assumes that you have already installed the LaunchDarkly Java SDK.
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
<version>7.1.0</version>
</dependency>

This library is compatible with Jedis 2.x versions greater than or equal to 2.9.0, and also with Jedis 3.x.
This library uses Jedis 7.1.0 by default and requires Jedis 3.6.0 or later for username/password (ACL) authentication support.

3. Import the LaunchDarkly package and the package for this library:

Expand All @@ -45,6 +45,44 @@ This assumes that you have already installed the LaunchDarkly Java SDK.

By default, the store will try to connect to a local Redis instance on port 6379.

## Authentication

### Password-only authentication (legacy)

For Redis servers using simple password authentication:

LDConfig config = new LDConfig.Builder()
.dataStore(
Components.persistentDataStore(
Redis.dataStore().password("my-redis-password")
)
)
.build();

Or include it in the URI:

Redis.dataStore().url("redis://:my-redis-password@my-redis-host:6379")

### Username/password authentication (Redis 6.0+ ACL)

For Redis 6.0+ servers using ACL with username and password:

LDConfig config = new LDConfig.Builder()
.dataStore(
Components.persistentDataStore(
Redis.dataStore()
.username("my-username")
.password("my-password")
)
)
.build();

Or include both in the URI:

Redis.dataStore().url("redis://my-username:my-password@my-redis-host:6379")

**Note:** Username/password authentication requires Jedis 3.6.0 or later.

## Caching behavior

The LaunchDarkly SDK has a standard caching mechanism for any persistent data store, to reduce database traffic. This is configured through the SDK's `PersistentDataStoreBuilder` class as described the SDK documentation. For instance, to specify a cache TTL of 5 minutes:
Expand Down
2 changes: 1 addition & 1 deletion lib/java-server-sdk-redis-store/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ ext {

ext.versions = [
"sdk": "6.3.0", // the *lowest* version we're compatible with
"jedis": "2.9.0"
"jedis": "7.1.0" // 3.6.0+ required for username/password (ACL) authentication
]

ext.libraries = [:]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public abstract class RedisStoreBuilder<T> implements ComponentConfigurer<T>, Di
Duration connectTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);
Duration socketTimeout = Duration.ofMillis(Protocol.DEFAULT_TIMEOUT);
Integer database = null;
String username = null;
String password = null;
boolean tls = false;
JedisPoolConfig poolConfig = null;
Expand All @@ -98,11 +99,30 @@ public RedisStoreBuilder<T> database(Integer database) {
return this;
}

/**
* Specifies a username for Redis ACL authentication.
* <p>
* Redis 6.0+ supports Access Control Lists (ACL) with username/password authentication.
* It is also possible to include a username in the Redis URI, in the form {@code redis://USERNAME:PASSWORD@host:port}.
* Any username that you set with {@link #username(String)} will override the URI.
* <p>
* Note: Using this feature requires Jedis 3.6.0 or later.
*
* @param username the username for ACL authentication
* @return the builder
* @since 2.2.0
*/
public RedisStoreBuilder<T> username(String username) {
this.username = username;
return this;
}

/**
* Specifies a password that will be sent to Redis in an AUTH command.
* <p>
* It is also possible to include a password in the Redis URI, in the form {@code redis://:PASSWORD@host:port}. Any
* password that you set with {@link #password(String)} will override the URI.
* It is also possible to include a password in the Redis URI, in the form {@code redis://:PASSWORD@host:port}
* or {@code redis://USERNAME:PASSWORD@host:port} for ACL authentication. Any password that you set with
* {@link #password(String)} will override the URI.
*
* @param password the password
* @return the builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ protected RedisStoreImplBase(RedisStoreBuilder<?> builder, LDLogger logger) {
// to decompose the URI.
String host = builder.uri.getHost();
int port = builder.uri.getPort();
String username = builder.username == null ? RedisURIComponents.getUsername(builder.uri) : builder.username;
String password = builder.password == null ? RedisURIComponents.getPassword(builder.uri) : builder.password;
int database = builder.database == null ? RedisURIComponents.getDBIndex(builder.uri) : builder.database;
boolean tls = builder.tls || builder.uri.getScheme().equals("rediss");

String extra = tls ? " with TLS" : "";
if (username != null) {
extra = extra + (extra.isEmpty() ? " with" : " and") + " username";
}
if (password != null) {
extra = extra + (extra.isEmpty() ? " with" : " and") + " password";
}
Expand All @@ -41,6 +45,7 @@ protected RedisStoreImplBase(RedisStoreBuilder<?> builder, LDLogger logger) {
port,
(int) builder.connectTimeout.toMillis(),
(int) builder.socketTimeout.toMillis(),
username,
password,
database,
null, // clientName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,39 @@
* that class doesn't exist in the same location in both versions.
*/
abstract class RedisURIComponents {
/**
* Extracts the username from a Redis URI.
* <p>
* Supports both formats:
* <ul>
* <li>{@code redis://USERNAME:PASSWORD@host:port} - returns USERNAME</li>
* <li>{@code redis://:PASSWORD@host:port} - returns null (password-only, legacy format)</li>
* </ul>
*
* @param uri the Redis URI
* @return the username, or null if not specified or empty
*/
static String getUsername(URI uri) {
if (uri.getUserInfo() == null) {
return null;
}
String[] parts = uri.getUserInfo().split(":", 2);
// If the username part is empty (e.g., ":password"), return null
return (parts.length > 0 && !parts[0].isEmpty()) ? parts[0] : null;
}

/**
* Extracts the password from a Redis URI.
* <p>
* Supports both formats:
* <ul>
* <li>{@code redis://USERNAME:PASSWORD@host:port} - returns PASSWORD</li>
* <li>{@code redis://:PASSWORD@host:port} - returns PASSWORD (legacy format)</li>
* </ul>
*
* @param uri the Redis URI
* @return the password, or null if not specified
*/
static String getPassword(URI uri) {
if (uri.getUserInfo() == null) {
return null;
Expand All @@ -16,6 +49,12 @@ static String getPassword(URI uri) {
return parts.length < 2 ? null : parts[1];
}

/**
* Extracts the database index from a Redis URI.
*
* @param uri the Redis URI (e.g., {@code redis://host:port/2})
* @return the database index, or 0 if not specified
*/
static int getDBIndex(URI uri) {
String[] parts = uri.getPath().split("/", 2);
if (parts.length < 2 || parts[1].isEmpty()) {
Expand Down
Loading