Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
b7bb379
Introduce push handler
ggivo Jun 20, 2025
b0987a3
Introduce PushHandlerChain for composable push event handling
ggivo Jun 23, 2025
e373dfe
Handle relax timeout for maintenance events
ggivo Jun 25, 2025
4ee4893
Support custom Push listeners for Jedis client
ggivo Jul 1, 2025
835fca7
Add proactiveRebindEnabled configuration option
ggivo Jul 3, 2025
b0528f7
PushHandler is now provided through JedisClientConfig instead through…
ggivo Jul 4, 2025
0d1a431
Fix NPE in CacheConnection
ggivo Jul 4, 2025
645ff82
[cleanup] Use weak reference in AdaptiveTimeoutHandler to avoid memor…
ggivo Jul 7, 2025
d85eb17
[cleanup] Fix javadoc errors
ggivo Jul 7, 2025
71e0891
[cleanup] Fix TransactionCommandsTest mocked test
ggivo Jul 7, 2025
b982a77
Moving/Rebind initial support
ggivo Jul 9, 2025
774dec1
Mocked relaxed timeout test
ggivo Jul 9, 2025
c453a0f
Mocked rebind test
ggivo Jul 9, 2025
ba8542a
Fix : wrong order connection.rebind pool.clear
ggivo Jul 10, 2025
adc4b07
[clean up] Address review comments from a-TODO-rov
ggivo Jul 10, 2025
49dcd90
add more rebind tests
ggivo Jul 11, 2025
67a188a
clean up
ggivo Jul 11, 2025
5c5e135
clean up remove unused test method
ggivo Jul 11, 2025
927e94d
fix relaxed timeout on blocking command
ggivo Jul 21, 2025
478749e
format
ggivo Jul 21, 2025
cecb4cd
enforce code formating for new classes
ggivo Jul 28, 2025
27c32ac
reformat to fix java docs
ggivo Jul 28, 2025
719f5c7
force formating of TimeoutOptions.java
ggivo Jul 28, 2025
682f125
Address review comments
ggivo Jul 28, 2025
e3e5aca
Address review comments
ggivo Jul 29, 2025
3767002
Address review comments
ggivo Jul 29, 2025
98def08
format ConnectionTestHelper
ggivo Jul 29, 2025
c41cf49
fix merge errors after rebase
ggivo Mar 24, 2026
58d28a5
remove support for generic listeners for Push events
ggivo Mar 24, 2026
2f4b3b1
per connection maintenance event handler
ggivo Mar 25, 2026
04739ae
use Connection memberOf reference to its owning ConnectionPool to not…
ggivo Mar 25, 2026
2907008
PushConsumerContext rename and clean up
ggivo Mar 25, 2026
264cd35
drop readProtocolWithCheckingBroken(pushConsumer)
ggivo Mar 25, 2026
1fa9e5a
fix tests
ggivo Mar 25, 2026
96f306f
tcp mock clean up & improvements
ggivo Mar 26, 2026
89a534a
Merge branch 'master' into feature/hu-notifications-rebased-wo-generi…
ggivo Mar 26, 2026
1fbbe7a
Merge branch 'master' into feature/hu-notifications-rebased-wo-generi…
ggivo Apr 2, 2026
c753354
fix : Single-argument Protocol.read returns wrong type for pushes
ggivo Apr 2, 2026
3cd5667
fix : Builder constructor skips MaintenanceEventConsumer registration…
ggivo Apr 2, 2026
70afbf3
address review comments
ggivo Apr 2, 2026
3bd2919
fix: PushMessageNotificationTest
ggivo Apr 3, 2026
28a4814
format and fix moving target parsing
ggivo Apr 3, 2026
04e9661
Stub commands instead of predefined answers
ggivo Mar 27, 2026
64aae2a
pub/sub support
ggivo Apr 1, 2026
233082d
add basic RedisClient pub/sub tests
ggivo Apr 3, 2026
d360603
add pub/sub test with arbitrary push notifications
ggivo Apr 3, 2026
1d1be0c
send push notification to client by id
ggivo Apr 3, 2026
f87fa4d
fix compilation errors after merge
ggivo Apr 14, 2026
fd06a7e
clean up unused imports
ggivo Apr 14, 2026
91e8539
configurable RedisStubServer version
ggivo Apr 14, 2026
9b4a276
Partial support for CLIENT TRACKING in RedisStubServer
ggivo Apr 14, 2026
61a75bb
formating
ggivo Apr 14, 2026
6589268
formating
ggivo Apr 14, 2026
b7425ea
address Duplicate CommandContext interface and implementation across …
ggivo Apr 14, 2026
636b878
address : Test no longer verifies relaxed blocking timeout value
ggivo Apr 14, 2026
9b790db
shutdownNow command executor
ggivo Apr 14, 2026
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
13 changes: 12 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -570,8 +570,19 @@
<include>**/*TestUtil.java</include>
<include>**/executors/aggregators/*.java</include>
<include>**/*MapMatcher.java</include>
<include>**/Maintenance*.java</include>
<include>**/Push*.java</include>
<include>**/Rebind*.java</include>
<include>src/test/java/redis/clients/jedis/upgrade/*.java</include>
<include>src/test/java/redis/clients/jedis/util/server/*.java</include>
<include>**/TimeoutOptions.java</include>
<include>**/*Handler.java</include>
<include>**/ConnectionTestHelper.java</include>
<include>**/*MockTest.java</include>
<include>**/server/**/*.java</include>
<include>**/pubsub/**/*.java</include>
</includes>
</configuration>
</configuration>
<executions>
<execution>
<goals>
Expand Down
214 changes: 209 additions & 5 deletions src/main/java/redis/clients/jedis/Connection.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
import java.util.function.Supplier;
import java.util.concurrent.atomic.AtomicReference;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Protocol.Command;
import redis.clients.jedis.Protocol.Keyword;
import redis.clients.jedis.annots.Experimental;
Expand All @@ -28,10 +30,13 @@
import redis.clients.jedis.exceptions.JedisException;
import redis.clients.jedis.exceptions.JedisValidationException;
import redis.clients.jedis.util.IOUtils;
import redis.clients.jedis.util.NumberUtils;
import redis.clients.jedis.util.RedisInputStream;
import redis.clients.jedis.util.RedisOutputStream;
import redis.clients.jedis.util.SafeEncoder;

public class Connection implements Closeable {
public static Logger logger = LoggerFactory.getLogger(Connection.class);

public static class Builder {
private JedisSocketFactory socketFactory;
Expand Down Expand Up @@ -72,6 +77,9 @@ public static Builder builder() {
private Socket socket;
private RedisOutputStream outputStream;
private RedisInputStream inputStream;
private boolean relaxedTimeoutEnabled = false;
private int relaxedTimeout = NumberUtils.safeToInt(TimeoutOptions.DISABLED_TIMEOUT.toMillis());
private int relaxedBlockingTimeout = NumberUtils.safeToInt(TimeoutOptions.DISABLED_TIMEOUT.toMillis());
private int soTimeout = 0;
private int infiniteSoTimeout = 0;
private boolean broken = false;
Expand All @@ -81,8 +89,12 @@ public static Builder builder() {
protected String version;
private AtomicReference<RedisCredentials> currentCredentials = new AtomicReference<>(null);
private AuthXManager authXManager;
private boolean isBlocking = false;
private boolean isRelaxed = false;
private JedisClientConfig clientConfig;
private boolean rebindRequested = false;

private PushConsumerChain pushConsumers;
public Connection() {
this(Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT);
}
Expand All @@ -101,6 +113,9 @@ public Connection(final HostAndPort hostAndPort, final JedisClientConfig clientC

public Connection(final JedisSocketFactory socketFactory) {
this.socketFactory = socketFactory;
this.authXManager = null;

initPushConsumers(null);
}

public Connection(final JedisSocketFactory socketFactory, JedisClientConfig clientConfig) {
Expand All @@ -114,6 +129,29 @@ protected Connection(Builder builder) {
this.clientConfig = builder.getClientConfig();
}

protected void initPushConsumers(JedisClientConfig config) {
/*
* Default consumers to process push messages.
* Marks all @{link PushMessages as processed, except for pub/sub.
* Pub/sub messages are propagated to the client.
*/
this.pushConsumers = PushConsumerChain.of(
PushConsumerChain.PUBSUB_ONLY_CONSUMER
);

if (config != null) {

/*
* Add consumer to handle server maintenance events.
* Per-connection concerns (timeout relaxation, rebind flag) are handled inline.
* Pool-level concerns (factory rebind, pool clear) are notified via memberOf.
*/
if (config.isProactiveRebindEnabled() || relaxedTimeoutEnabled) {
addPushConsumer(new MaintenanceEventConsumer(config.isProactiveRebindEnabled()));
}
}
}

@Override
public String toString() {
return getClass().getSimpleName() + "{" + socketFactory + "}";
Expand Down Expand Up @@ -194,7 +232,8 @@ public void setTimeoutInfinite() {

public void rollbackTimeout() {
try {
socket.setSoTimeout(this.soTimeout);
int timeout = getDesiredTimeout();
socket.setSoTimeout(timeout);
} catch (SocketException ex) {
setBroken();
throw new JedisConnectionException(ex);
Expand All @@ -217,9 +256,11 @@ public <T> T executeCommand(final CommandObject<T> commandObject) {
return commandObject.getBuilder().build(getOne());
} else {
try {
isBlocking = true;
setTimeoutInfinite();
return commandObject.getBuilder().build(getOne());
} finally {
isBlocking = false;
rollbackTimeout();
}
}
Expand Down Expand Up @@ -303,7 +344,7 @@ public void close() {
if (this.memberOf != null) {
ConnectionPool pool = this.memberOf;
this.memberOf = null;
if (isBroken()) {
if (isBroken() || isRebindRequested()) {
pool.returnBrokenResource(this);
} else {
pool.returnResource(this);
Expand All @@ -313,6 +354,10 @@ public void close() {
}
}

private boolean isRebindRequested() {
return rebindRequested;
}

/**
* Close the socket and disconnect the server.
*/
Expand Down Expand Up @@ -431,8 +476,8 @@ protected void flush() {
}

@Experimental
protected Object protocolRead(RedisInputStream is) {
return Protocol.read(is);
protected Object protocolRead(RedisInputStream is, PushConsumer handler) {
return Protocol.read(is, handler);
}

@Experimental
Expand All @@ -445,7 +490,7 @@ protected Object readProtocolWithCheckingBroken() {
}

try {
return protocolRead(inputStream);
return protocolRead(inputStream, pushConsumers);
} catch (JedisConnectionException exc) {
broken = true;
throw exc;
Expand Down Expand Up @@ -508,6 +553,13 @@ protected void initializeFromClientConfig(final JedisClientConfig config) {
try {
this.soTimeout = config.getSocketTimeoutMillis();
this.infiniteSoTimeout = config.getBlockingSocketTimeoutMillis();
// TODO : ggivo Align configuration properties to other clients
this.relaxedTimeout = NumberUtils.safeToInt(config.getTimeoutOptions().getRelaxedTimeout().toMillis());
this.relaxedBlockingTimeout = NumberUtils.safeToInt(config.getTimeoutOptions().getRelaxedBlockingTimeout().toMillis());
this.relaxedTimeoutEnabled = TimeoutOptions.isRelaxedTimeoutEnabled(relaxedTimeout) ||
TimeoutOptions.isRelaxedTimeoutEnabled(relaxedBlockingTimeout);

initPushConsumers(config);

connect();

Expand Down Expand Up @@ -666,4 +718,156 @@ protected boolean isTokenBasedAuthenticationEnabled() {
protected AuthXManager getAuthXManager() {
return authXManager;
}

/**
* Returns an unmodifiable view of the registered push consumers.
*
* @return
*/
List<PushConsumer> getPushConsumers() {
return pushConsumers.getConsumers();
}

@Experimental
protected void addPushConsumer(PushConsumer consumer) {
this.pushConsumers.add(consumer);
}

@Experimental
public boolean isRelaxedTimeoutActive() {
return isRelaxed;
}

/**
* Calculate the desired timeout based on current state (blocking/non-blocking and relaxed/normal).
* When relaxed timeouts are enabled, use configured relaxed timeout if available, otherwise fallback to default timeout.
*/
private int getDesiredTimeout() {
if (!isRelaxed) {
if (!isBlocking) {
return soTimeout;
} else {
return infiniteSoTimeout;
}
} else {
if (!isBlocking) {
return TimeoutOptions.isRelaxedTimeoutEnabled(relaxedTimeout) ? relaxedTimeout : soTimeout;
} else {
return TimeoutOptions.isRelaxedTimeoutEnabled(relaxedBlockingTimeout) ? relaxedBlockingTimeout : infiniteSoTimeout;
}
}
}

@Experimental
public void relaxTimeouts() {
if (!relaxedTimeoutEnabled) {
return;
}

if (!isRelaxed) {
isRelaxed = true;
try {
if (isConnected()) {
socket.setSoTimeout(getDesiredTimeout());
}
} catch (SocketException ex) {
setBroken();
throw new JedisConnectionException(ex);
}
}
}

@Experimental
public void disableRelaxedTimeout() {
if (isRelaxed) {
isRelaxed = false;
try {
if (isConnected()) {
socket.setSoTimeout(getDesiredTimeout());
}
} catch (SocketException ex) {
setBroken();
throw new JedisConnectionException(ex);
}
}
}

/**
* Push consumer that handles server maintenance events.
* <p>
* Handles per-connection concerns (timeout relaxation, rebind flag) inline.
* Notifies the owning {@link ConnectionPool} via {@code memberOf} for pool-level concerns.
* </p>
*/
class MaintenanceEventConsumer implements PushConsumer {
private final boolean proactiveRebindEnabled;

public MaintenanceEventConsumer(boolean proactiveRebindEnabled) {
this.proactiveRebindEnabled = proactiveRebindEnabled;
}

@Override
public void accept(PushConsumerContext context) {
PushMessage message = context.getMessage();

switch (message.getType()) {
case "MOVING":
onMoving(message);
break;
case "MIGRATING":
relaxTimeouts();
break;
case "MIGRATED":
disableRelaxedTimeout();
break;
case "FAILING_OVER":
relaxTimeouts();
break;
case "FAILED_OVER":
disableRelaxedTimeout();
break;
}
}

private void onMoving(PushMessage message) {
HostAndPort rebindTarget = getRebindTarget(message);

// per-connection: mark rebind requested and relax timeouts
if (proactiveRebindEnabled) {
rebindRequested = true;
}
relaxTimeouts();

// notify owning pool to rebind factory and clear idle connections
if (memberOf != null) {
memberOf.onMoving(rebindTarget);
}
}

private HostAndPort getRebindTarget(PushMessage message) {
// Extract domain/ip and port from the message
// MOVING push message format: ["MOVING", slot, "host:port"]
List<Object> content = message.getContent();

if (content.size() < 3) {
logger.warn("MOVING push message is malformed: {}", message);
return null;
}

Object addressObject = content.get(2); // Get the 3rd element (index 2)
if (!(addressObject instanceof byte[])) {
logger.warn("Invalid re-bind message format, expected 3rd element to be a byte[], got {}", addressObject);
return null;
}


try {
String addressAndPort = SafeEncoder.encode((byte[]) addressObject);
return HostAndPort.from(addressAndPort);
} catch (Exception e) {
logger.warn("Error parsing re-bind target from message: {}", message, e);
return null;
}
}
}
}
20 changes: 19 additions & 1 deletion src/main/java/redis/clients/jedis/ConnectionFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
/**
* PoolableObjectFactory custom impl.
*/
public class ConnectionFactory implements PooledObjectFactory<Connection> {
public class ConnectionFactory implements PooledObjectFactory<Connection> , RebindAware {

public static class Builder {
private JedisClientConfig clientConfig;
Expand Down Expand Up @@ -221,4 +221,22 @@ private void reAuthenticate(Connection jedis) throws Exception {
throw e;
}
}


@Override
public void rebind(HostAndPort newHostAndPort) {
JedisSocketFactory jedisSocketFactory = connectionBuilder.getSocketFactory();
if ((connectionBuilder.getSocketFactory() instanceof RebindAware)) {
RebindAware factory = (RebindAware) jedisSocketFactory;
logger.debug("Rebinding to {}", newHostAndPort);
factory.rebind(newHostAndPort);
} else {
if ( logger.isDebugEnabled()) {
logger.debug("Rebind not supported: {}", jedisSocketFactory.getClass().getName());
}
}


}

}
Loading
Loading