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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.springframework.data.redis.connection.jedis;

import redis.clients.jedis.ClientSetInfoConfig;
import redis.clients.jedis.Connection;
import redis.clients.jedis.DefaultJedisClientConfig;
import redis.clients.jedis.HostAndPort;
Expand Down Expand Up @@ -59,10 +60,12 @@
import org.springframework.data.redis.connection.RedisConfiguration.WithDatabaseIndex;
import org.springframework.data.redis.connection.RedisConfiguration.WithPassword;
import org.springframework.data.redis.connection.jedis.JedisClusterConnection.JedisClusterTopologyProvider;
import org.springframework.data.redis.util.RedisClientLibraryInfo;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
* Connection factory creating <a href="https://github.com/redis/jedis">Jedis</a> based connections.
Expand Down Expand Up @@ -130,6 +133,13 @@ public class JedisConnectionFactory
private RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration("localhost",
Protocol.DEFAULT_PORT);

/**
* Upstream framework library suffixes (without the Spring Data Redis entry).
* Values are collected from calls to {@link #addUpstreamLibNameSuffix(String)} and combined
* into the final CLIENT SETINFO LIB-NAME when configuring the client.
*/
private @Nullable String upstreamLibNameSuffix;

/**
* Lifecycle state of this factory.
*/
Expand Down Expand Up @@ -654,6 +664,32 @@ public void setConvertPipelineAndTxResults(boolean convertPipelineAndTxResults)
this.convertPipelineAndTxResults = convertPipelineAndTxResults;
}

/**
* Add a library name suffix used for CLIENT SETINFO.
* This method is primarily intended for upstream framework integrations (for example,
* Spring Session Data Redis or Spring Security) to contribute their identifiers to the
* CLIENT SETINFO library name chain.
* <p>
* The given value should contain framework identifiers without the core driver name,
* for example {@code "spring-session-data-redis_v3.0.0"}. Multiple calls will
* accumulate values; the final CLIENT SETINFO suffix is assembled by appending the
* Spring Data Redis entry via {@link RedisClientLibraryInfo#getLibNameSuffix(String)}.
*
* @param libNameSuffix the additional library name suffix to add; can be {@code null}.
* @since 4.0
*/
public void addUpstreamLibNameSuffix(@Nullable String libNameSuffix) {
if (!StringUtils.hasText(libNameSuffix)) {
return;
}
if (!StringUtils.hasText(this.upstreamLibNameSuffix)) {
this.upstreamLibNameSuffix = libNameSuffix;
}
else if (!this.upstreamLibNameSuffix.contains(libNameSuffix)) {
this.upstreamLibNameSuffix = this.upstreamLibNameSuffix + ";" + libNameSuffix;
}
}

/**
* @return true when {@link RedisSentinelConfiguration} is present.
* @since 1.4
Expand Down Expand Up @@ -688,6 +724,9 @@ private JedisClientConfig createClientConfig(int database, @Nullable String user
builder.connectionTimeoutMillis(getConnectTimeout());
builder.socketTimeoutMillis(getReadTimeout());

String suffix = RedisClientLibraryInfo.getLibNameSuffix(this.upstreamLibNameSuffix);
builder.clientSetInfoConfig(new ClientSetInfoConfig(suffix));

builder.database(database);

if (!ObjectUtils.isEmpty(username)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import org.springframework.data.redis.connection.RedisConfiguration.ClusterConfiguration;
import org.springframework.data.redis.connection.RedisConfiguration.WithDatabaseIndex;
import org.springframework.data.redis.connection.RedisConfiguration.WithPassword;
import org.springframework.data.redis.util.RedisClientLibraryInfo;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
Expand Down Expand Up @@ -154,6 +155,13 @@ public class LettuceConnectionFactory implements RedisConnectionFactory, Reactiv

private RedisStandaloneConfiguration standaloneConfig = new RedisStandaloneConfiguration("localhost", 6379);

/**
* Upstream framework library suffixes (without the Spring Data Redis entry).
* Values are collected from calls to {@link #addUpstreamLibNameSuffix(String)} and combined
* into the final CLIENT SETINFO LIB-NAME when configuring the client.
*/
private @Nullable String upstreamLibNameSuffix;

private @Nullable SharedConnection<byte[]> connection;
private @Nullable SharedConnection<ByteBuffer> reactiveConnection;

Expand Down Expand Up @@ -637,6 +645,32 @@ public void setClientName(@Nullable String clientName) {
this.getMutableConfiguration().setClientName(clientName);
}

/**
* Add a library name suffix used for CLIENT SETINFO.
* This method is primarily intended for upstream framework integrations (for example,
* Spring Session Data Redis) to contribute their identifiers to the
* CLIENT SETINFO library name chain.
* <p>
* The given value should contain framework identifiers without the core driver name,
* for example {@code "spring-session-data-redis_v3.0.0"}. Multiple calls will
* accumulate values; the final CLIENT SETINFO suffix is assembled by appending the
* Spring Data Redis entry via {@link RedisClientLibraryInfo#getLibNameSuffix(String)}.
*
* @param libNameSuffix the additional library name suffix to add; can be {@code null}.
* @since 4.0
*/
public void addUpstreamLibNameSuffix(@Nullable String libNameSuffix) {
if (!StringUtils.hasText(libNameSuffix)) {
return;
}
if (!StringUtils.hasText(this.upstreamLibNameSuffix)) {
this.upstreamLibNameSuffix = libNameSuffix;
}
else if (!this.upstreamLibNameSuffix.contains(libNameSuffix)) {
this.upstreamLibNameSuffix = this.upstreamLibNameSuffix + ";" + libNameSuffix;
}
}

/**
* Returns the native {@link AbstractRedisClient} used by this instance. The client is initialized as part of
* {@link #afterPropertiesSet() the bean initialization lifecycle} and only available when this connection factory is
Expand Down Expand Up @@ -1482,6 +1516,10 @@ private RedisURI createRedisURIAndApplySettings(String host, int port) {
builder.withStartTls(clientConfiguration.isStartTls());
builder.withTimeout(clientConfiguration.getCommandTimeout());

String libName = RedisClientLibraryInfo.getLibName(RedisClientLibraryInfo.DRIVER_LETTUCE,
this.upstreamLibNameSuffix);
builder.withLibraryName(libName);

return builder.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright 2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.util;

import org.jspecify.annotations.Nullable;

import org.springframework.util.StringUtils;

/**
* Utility class for building Spring Data Redis client library identification
* strings for Redis CLIENT SETINFO.
* <p>
* Supports the Redis CLIENT SETINFO custom suffix pattern:
* {@code (?<custom-name>[ -~]+) v(?<custom-version>[\d\.]+)}.
* Multiple suffixes can be delimited with semicolons. The recommended format
* for individual suffixes is {@code <custom-name>_v<custom-version>}.
*
* @author Viktoriya Kutsarova
* @since 4.0
*/
public final class RedisClientLibraryInfo {

/**
* Lettuce driver name constant for CLIENT SETINFO.
*/
public static final String DRIVER_LETTUCE = "lettuce";

/**
* Spring Data Redis framework name constant for CLIENT SETINFO.
*/
public static final String FRAMEWORK_NAME = "spring-data-redis";

private static final String SUFFIX_DELIMITER = ";";

private static final String VERSION_SEPARATOR = "_v";

private static final String UNKNOWN_VERSION = "unknown";

/**
* Get the Spring Data Redis version from the package manifest.
* Returns "unknown" if the version cannot be determined (for example when
* running from an IDE or tests without a populated Implementation-Version).
*
* @return the Spring Data Redis version, or "unknown" if not available
*/
public static String getVersion() {
Package pkg = RedisClientLibraryInfo.class.getPackage();
String version = (pkg != null ? pkg.getImplementationVersion() : null);
return (version != null ? version : UNKNOWN_VERSION);
}

private RedisClientLibraryInfo() {
}

Comment on lines +65 to +67
Copy link
Contributor

Choose a reason for hiding this comment

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

Formatting seems off here

/**
* Build a library name suffix for CLIENT SETINFO in the format:
* {@code spring-data-redis_v<version>}
* <p>
* Note: The underscore before 'v' follows the Redis CLIENT SETINFO pattern recommendation.
*
* @return the library name suffix
*/
public static String getLibNameSuffix() {
return FRAMEWORK_NAME + VERSION_SEPARATOR + getVersion();
}

/**
* Build a library name suffix with additional framework suffix(es) for CLIENT SETINFO.
* This allows multiple higher-level frameworks to identify themselves in a chain.
* <p>
* The {@code additionalSuffix} parameter should already be formatted according to the pattern
* and can contain multiple frameworks separated by semicolons.
* <p>
* Format: {@code <additionalSuffix>;spring-data-redis_v<version>}
* <p>
* Example with multiple frameworks:
* <pre>
* String suffix = RedisClientInfo.getLibNameSuffix(
* "spring-security_v6.0.0;spring-session-data-redis_v3.0.0"
* );
* // Returns: "spring-security_v6.0.0;spring-session-data-redis_v3.0.0;spring-data-redis_v4.0.0"
* </pre>
*
* @param additionalSuffix pre-formatted suffix string containing one or more framework identifiers,
* already in the format "name_version" and separated by semicolons if multiple
* @return the combined library name suffix with all frameworks and Spring Data Redis info
*/
public static String getLibNameSuffix(@Nullable String additionalSuffix) {
if (!StringUtils.hasText(additionalSuffix)) {
return getLibNameSuffix();
}
return additionalSuffix + SUFFIX_DELIMITER + getLibNameSuffix();
}

/**
* Build a complete library name for CLIENT SETINFO by wrapping the suffix with the core driver name.
* This allows multiple higher-level frameworks to identify themselves in a chain.
* <p>
* Format: {@code <driverName>(<additionalSuffix>;spring-data-redis_v<version>)}
* <p>
* Example:
* <pre>
* String libName = RedisClientInfo.getLibName("lettuce",
* "spring-security_v6.0.0;spring-session-data-redis_v3.0.0");
* // Returns: "lettuce(spring-security_v6.0.0;spring-session-data-redis_v3.0.0;spring-data-redis_v4.0.0)"
* </pre>
*
* @param driverName the core Redis driver name (e.g., "lettuce", "jedis")
* @param additionalSuffix pre-formatted suffix string containing one or more framework identifiers,
* already in the format "name_version" and separated by semicolons if multiple
* @return the complete library name in the format "driverName(additionalSuffix;spring-data-redis_version)"
*/
public static String getLibName(String driverName, @Nullable String additionalSuffix) {
return driverName + "(" + getLibNameSuffix(additionalSuffix) + ")";
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.util.List;
import org.springframework.data.redis.core.types.RedisClientInfo;

import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -74,6 +77,64 @@ void connectionAppliesClientName() {
assertThat(connection.getClientName()).isEqualTo("clientName");
}

@Test // CLIENT SETINFO
void clientListReportsJedisLibNameWithSpringDataSuffix() {

factory = new JedisConnectionFactory(
new RedisStandaloneConfiguration(SettingsUtils.getHost(), SettingsUtils.getPort()),
JedisClientConfiguration.builder().clientName("clientName").build());
factory.afterPropertiesSet();
factory.start();

try (RedisConnection connection = factory.getConnection()) {

List<RedisClientInfo> clients = connection.serverCommands().getClientList();

RedisClientInfo self = clients.stream()
.filter(info -> "clientName".equals(info.getName()))
.findFirst()
.orElseThrow();

String libName = self.get("lib-name");

assertThat(libName).isNotNull();
assertThat(libName).contains("jedis(");
assertThat(libName).contains("spring-data-redis_v");
}
}

@Test // CLIENT SETINFO
void clientListReportsJedisLibNameWithUpstreamSuffix() {

String upstreamLibNameSuffix = "spring-session-data-redis_v3.0.0";

factory = new JedisConnectionFactory(
new RedisStandaloneConfiguration(SettingsUtils.getHost(), SettingsUtils.getPort()),
JedisClientConfiguration.builder().clientName("clientName").build());
factory.addUpstreamLibNameSuffix(upstreamLibNameSuffix);
Copy link
Contributor

Choose a reason for hiding this comment

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

I was thinking we should hide the specifics of building up the suffix from the upstream libraries. Actually I am not sure SDR should do it either, but rather the drivers themselves

factory.afterPropertiesSet();
factory.start();

try (RedisConnection connection = factory.getConnection()) {

List<RedisClientInfo> clients = connection.serverCommands().getClientList();

RedisClientInfo self = clients.stream()
.filter(info -> "clientName".equals(info.getName()))
.findFirst()
.orElseThrow();

String libName = self.get("lib-name");

assertThat(libName).isNotNull();
assertThat(libName).contains("jedis(");
assertThat(libName).contains("spring-data-redis_v");
assertThat(libName).contains(upstreamLibNameSuffix);
}
}



@Test // GH-2503
void startStopStartConnectionFactory() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,47 @@ void earlyStartupDoesNotStartConnectionFactory() {
assertThat(ReflectionTestUtils.getField(connectionFactory, "pool")).isNull();
}

@Test // CLIENT SETINFO
void addUpstreamLibNameSuffixShouldIgnoreNullAndBlankValues() {

JedisConnectionFactory connectionFactory = new JedisConnectionFactory();

connectionFactory.addUpstreamLibNameSuffix(null);
connectionFactory.addUpstreamLibNameSuffix("");
connectionFactory.addUpstreamLibNameSuffix(" ");

Object upstreamLibNameSuffix = ReflectionTestUtils.getField(connectionFactory, "upstreamLibNameSuffix");

assertThat(upstreamLibNameSuffix).isNull();
}

@Test // CLIENT SETINFO
void addUpstreamLibNameSuffixShouldAccumulateValuesInOrder() {

JedisConnectionFactory connectionFactory = new JedisConnectionFactory();

connectionFactory.addUpstreamLibNameSuffix("spring-session-data-redis_v3.0.0");
connectionFactory.addUpstreamLibNameSuffix("spring-security_v6.0.0");

Object upstreamLibNameSuffix = ReflectionTestUtils.getField(connectionFactory, "upstreamLibNameSuffix");

assertThat(upstreamLibNameSuffix).isEqualTo("spring-session-data-redis_v3.0.0;spring-security_v6.0.0");
}

@Test // CLIENT SETINFO
void addUpstreamLibNameSuffixShouldNotAddDuplicates() {

JedisConnectionFactory connectionFactory = new JedisConnectionFactory();

connectionFactory.addUpstreamLibNameSuffix("spring-session-data-redis_v3.0.0");
connectionFactory.addUpstreamLibNameSuffix("spring-session-data-redis_v3.0.0");

Object upstreamLibNameSuffix = ReflectionTestUtils.getField(connectionFactory, "upstreamLibNameSuffix");

assertThat(upstreamLibNameSuffix).isEqualTo("spring-session-data-redis_v3.0.0");
}


private JedisConnectionFactory initSpyedConnectionFactory(RedisSentinelConfiguration sentinelConfiguration,
@Nullable JedisPoolConfig poolConfig) {

Expand Down
Loading
Loading