Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import io.lettuce.core.AbstractRedisClient;
import io.lettuce.core.ClientOptions;
import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisCredentials;
import io.lettuce.core.RedisURI;
import io.lettuce.core.TimeoutOptions;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.RedisClusterClient;
Expand All @@ -24,6 +26,7 @@
import org.springframework.data.redis.connection.RedisConfiguration;
import org.springframework.data.redis.connection.RedisNode;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
Expand Down Expand Up @@ -59,6 +62,9 @@ public class RedisConfig {
@Value("${appsmith.redis.url:}")
private String redisURL;

@Value("${appsmith.redis.sentinel.master:}")
private String redisSentinelMaster;

/**
* This is the topic to which we will publish & subscribe to. We can have multiple topics based on the messages
* that we wish to broadcast. Starting with a single one for now.
Expand All @@ -78,13 +84,15 @@ public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {

switch (scheme) {
case "redis" -> {
log.info("Creating Redis standalone connection configuration");
final RedisStandaloneConfiguration config =
new RedisStandaloneConfiguration(redisUri.getHost(), redisUri.getPort());
fillAuthentication(redisUri, config);
return new LettuceConnectionFactory(config);
}

case "rediss" -> {
log.info("Creating Redis standalone SSL connection configuration");
final RedisStandaloneConfiguration config =
new RedisStandaloneConfiguration(redisUri.getHost(), redisUri.getPort());
fillAuthentication(redisUri, config);
Expand All @@ -93,7 +101,20 @@ public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() {
return new LettuceConnectionFactory(config, clientConfig);
}

case "redis-sentinel" -> {
log.info("Creating Redis Sentinel connection configuration");
validateSentinelMaster();
final RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration()
.master(redisSentinelMaster)
.sentinel(redisUri.getHost(), redisUri.getPort());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation only supports a single sentinel node. In production, Redis Sentinel deployments typically have 3+ sentinel nodes for quorum. A single sentinel node defeats the purpose of Sentinel (if that one sentinel goes down, the client can't discover the master).

fillAuthentication(redisUri, sentinelConfig);
final LettuceClientConfiguration clientConfig =
LettuceClientConfiguration.builder().build();
return new LettuceConnectionFactory(sentinelConfig, clientConfig);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QQ: Does it provide connection pooling out of the box? Or is it not covered in the current implementation?

}

case "redis-cluster" -> {
log.info("Creating Redis Cluster connection configuration");
// For ElastiCache Redis with cluster mode enabled, with the configuration endpoint.
final RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration();
fillAuthentication(redisUri, clusterConfig);
Expand All @@ -112,16 +133,13 @@ public AbstractRedisClient redisClient() {
String redisurl = redisURL;
final URI redisUri = URI.create(redisURL);
String scheme = redisUri.getScheme();
boolean isCluster = false;

if ("redis-cluster".equalsIgnoreCase(scheme)) {
isCluster = true;
// java clients do not support redis-cluster scheme
if (redisurl.startsWith("redis-cluster://")) {
redisurl = "redis://" + redisurl.substring("redis-cluster://".length());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing code has rediss:// for standalone SSL connections. There's no equivalent rediss-sentinel:// or TLS option for the sentinel connection. In cloud environments (AWS ElastiCache, Azure Cache), sentinel with TLS is common.

}
}

if (isCluster) {
RedisClusterClient redisClusterClient = RedisClusterClient.create(redisurl);
redisClusterClient.setOptions(ClusterClientOptions.builder()
.timeoutOptions(TimeoutOptions.builder()
Expand All @@ -132,6 +150,24 @@ public AbstractRedisClient redisClient() {
return redisClusterClient;
}

if ("redis-sentinel".equalsIgnoreCase(scheme)) {
validateSentinelMaster();
// Create new RedisURI with Sentinel configuration
RedisURI.Builder uriBuilder = RedisURI.builder()
.withSentinelMasterId(redisSentinelMaster)
.withSentinel(redisUri.getHost(), redisUri.getPort());

fillAuthentication(redisUri, uriBuilder);
RedisClient redisClient = RedisClient.create(uriBuilder.build());
redisClient.setOptions(ClientOptions.builder()
.timeoutOptions(TimeoutOptions.builder()
.timeoutCommands(true)
.fixedTimeout(Duration.ofMillis(2000))
.build())
.build());
return redisClient;
}

RedisClient redisClient = RedisClient.create(redisurl);
redisClient.setOptions(ClientOptions.builder()
.timeoutOptions(TimeoutOptions.builder()
Expand All @@ -144,11 +180,32 @@ public AbstractRedisClient redisClient() {
}

private void fillAuthentication(URI redisUri, RedisConfiguration.WithAuthentication config) {
RedisCredentials credentials = extractCredentials(redisUri);
if (credentials != null) {
config.setUsername(credentials.getUsername());
config.setPassword(RedisPassword.of(credentials.getPassword()));
}
}

private void fillAuthentication(URI redisUri, RedisURI.Builder uriBuilder) {
RedisCredentials credentials = extractCredentials(redisUri);
if (credentials != null) {
uriBuilder.withAuthentication(credentials.getUsername(), credentials.getPassword());
}
}

private RedisCredentials extractCredentials(URI redisUri) {
final String userInfo = redisUri.getUserInfo();
if (StringUtils.isNotEmpty(userInfo)) {
final String[] parts = userInfo.split(":", 2);
config.setUsername(parts[0]);
config.setPassword(RedisPassword.of(parts.length > 1 ? parts[1] : null));
return RedisCredentials.just(parts[0], parts.length > 1 ? parts[1] : null);
}
return null;
}

private void validateSentinelMaster() {
if (StringUtils.isEmpty(redisSentinelMaster)) {
throw new IllegalStateException("Redis Sentinel Master is not configured");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ sentry.environment=${APPSMITH_SERVER_SENTRY_ENVIRONMENT:}
# Redis Properties
appsmith.redis.url=${APPSMITH_REDIS_URL}
appsmith.redis.git.url=${APPSMITH_REDIS_GIT_URL:${APPSMITH_REDIS_URL}}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concern: If a user sets APPSMITH_REDIS_GIT_URL to a sentinel URL, it will be passed to bash/RTS code that likely doesn't understand the redis-sentinel:// scheme. @sondermanish @wyattwalter What do you think?

appsmith.redis.sentinel.master=${APPSMITH_REDIS_SENTINEL_MASTER:}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No Helm/Docker documentation update: I thinkdeploy/helm/values.yaml and deploy/helm/templates/configMap.yaml need updates to support the new APPSMITH_REDIS_SENTINEL_MASTER env var. @wyattwalter Could you please confirm?


# Mail Properties
# Email defaults to false, because, when true and the other SMTP properties are not set, Spring will try to use a
Expand Down
Loading
Loading