MaintenanceNotificationsConfig.java
package redis.clients.jedis;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.time.Duration;
import redis.clients.jedis.util.JedisAsserts;
/**
* Configuration of the {@code CLIENT MAINT_NOTIFICATIONS} feature: how the client reacts to server
* maintenance push notifications ��� timeout relaxation and proactive endpoint handoff.
* <p>
* Maintenance notifications require RESP3: on a RESP2 connection {@link Mode#AUTO} (the default)
* leaves the feature inactive and {@link Mode#ENABLED} fails the connection setup.
* @since 8.1
*/
public class MaintenanceNotificationsConfig {
/**
* Default upper bound on the relaxed-timeout window started by MIGRATING/FAILING_OVER when the
* matching closing notification is lost. MOVING windows end at their own grace period instead.
* @since 8.1
*/
public static final Duration DEFAULT_RELAXED_WINDOW_MAX_DURATION = Duration.ofSeconds(60);
/**
* Default relaxed socket timeout in milliseconds.
* @since 8.1
*/
public static final int DEFAULT_RELAXED_SOCKET_TIMEOUT_MS = 10_000;
/**
* Default relaxed blocking socket timeout in milliseconds; {@code 0} means infinite.
* @since 8.1
*/
public static final int DEFAULT_RELAXED_BLOCKING_SOCKET_TIMEOUT_MS = 0;
private MaintenanceNotificationsConfig(Builder builder) {
this.endpointTypeResolver = builder.endpointTypeResolver;
this.mode = builder.mode;
this.relaxedWindowMaxDuration = builder.relaxedWindowMaxDuration;
this.relaxedTimeout = builder.relaxedTimeout;
this.relaxedBlockingTimeout = builder.relaxedBlockingTimeout;
}
/**
* Endpoint types for maintenance event notifications.
* <p>
* Determines the format of endpoint addresses returned in MOVING notifications.
* @since 8.1
*/
public enum EndpointType {
/** Internal IP address (for private network connections) */
INTERNAL_IP,
/** Internal fully qualified domain name (for private network connections with TLS) */
INTERNAL_FQDN,
/** External IP address (for public network connections) */
EXTERNAL_IP,
/** External fully qualified domain name (for public network connections with TLS) */
EXTERNAL_FQDN,
/**
* No endpoint: MOVING carries a null target. The client does not remap; it reconnects to the
* currently-configured endpoint at half the grace period.
* @since 8.1
*/
NONE
}
/**
* Strategy determining the {@link EndpointType} to request in MOVING notifications, evaluated per
* connection at handshake time.
* @since 8.1
*/
public interface EndpointTypeResolver {
/**
* Determines the endpoint type based on connection characteristics.
* @param remoteAddress the remote socket address of the connection
* @param sslEnabled whether TLS/SSL is enabled for the connection
* @return the {@link EndpointType} to request
*/
EndpointType getEndpointType(SocketAddress remoteAddress, boolean sslEnabled);
}
/**
* Auto-resolves from connection characteristics: a private remote IP (loopback, link-local,
* site-local, or IPv6 unique-local) selects {@code INTERNAL_*}, a public one {@code EXTERNAL_*};
* TLS selects {@code *_FQDN}, plaintext {@code *_IP}.
*/
private static final class AutoEndpointTypeResolver implements EndpointTypeResolver {
static final AutoEndpointTypeResolver INSTANCE = new AutoEndpointTypeResolver();
@Override
public EndpointType getEndpointType(SocketAddress remoteAddress, boolean sslEnabled) {
if (isPrivateIp(remoteAddress)) {
return sslEnabled ? EndpointType.INTERNAL_FQDN : EndpointType.INTERNAL_IP;
}
return sslEnabled ? EndpointType.EXTERNAL_FQDN : EndpointType.EXTERNAL_IP;
}
@Override
public String toString() {
return "AutoEndpointTypeResolver";
}
}
/** Always requests the user-chosen endpoint type, ignoring connection characteristics. */
private static final class FixedEndpointTypeResolver implements EndpointTypeResolver {
private final EndpointType endpointType;
FixedEndpointTypeResolver(EndpointType endpointType) {
this.endpointType = endpointType;
}
@Override
public EndpointType getEndpointType(SocketAddress remoteAddress, boolean sslEnabled) {
return endpointType;
}
@Override
public String toString() {
return "FixedEndpointTypeResolver(" + endpointType + ")";
}
}
/**
* True if the address is a resolved private IP: loopback, link-local, site-local, or IPv6
* unique-local.
*/
private static boolean isPrivateIp(SocketAddress socketAddress) {
if (!(socketAddress instanceof InetSocketAddress)) {
return false;
}
InetAddress address = ((InetSocketAddress) socketAddress).getAddress();
if (address == null || address.isAnyLocalAddress()) {
return false;
}
return address.isLoopbackAddress() || address.isLinkLocalAddress()
|| address.isSiteLocalAddress() || isUniqueLocalAddress(address);
}
// https://datatracker.ietf.org/doc/html/rfc4193
private static boolean isUniqueLocalAddress(InetAddress address) {
if (!(address instanceof Inet6Address)) {
return false;
}
byte[] bytes = address.getAddress();
return (bytes[0] & (byte) 0xfe) == (byte) 0xfc; // fc00::/7
}
/**
* Mode for maintenance event notifications.
* <ul>
* <li>ENABLED - the handshake must succeed: connection setup fails if the server rejects
* {@code CLIENT MAINT_NOTIFICATIONS} or the connection is not RESP3.</li>
* <li>DISABLED - the handshake is not attempted; the feature is off.</li>
* <li>AUTO - the handshake is attempted on RESP3 connections and the feature is quietly disabled
* if the server rejects it (or the connection is not RESP3).</li>
* </ul>
* @since 8.1
*/
public enum Mode {
ENABLED, DISABLED, AUTO
}
private final EndpointTypeResolver endpointTypeResolver;
private final Mode mode;
private final Duration relaxedWindowMaxDuration;
private final int relaxedTimeout;
private final int relaxedBlockingTimeout;
/**
* The strategy that decides which endpoint type to request in MOVING notifications; defaults to
* auto-resolution from connection characteristics.
* @since 8.1
*/
public EndpointTypeResolver getEndpointTypeResolver() {
return endpointTypeResolver;
}
/**
* The configured mode.
* @since 8.1
*/
public Mode getMode() {
return mode;
}
/**
* Upper bound on the relaxed-timeout window started by MIGRATING/FAILING_OVER. The window reverts
* automatically after this duration even if the matching closing notification is never received.
* Safety net against missed events or misbehaving servers. MOVING windows end at their own grace
* period instead.
* @since 8.1
*/
public Duration getRelaxedWindowMaxDuration() {
return relaxedWindowMaxDuration;
}
/**
* True when the client will attempt the maintenance-notifications handshake (mode ENABLED or
* AUTO); with AUTO the feature still depends on server support.
* @since 8.1
*/
public boolean isEnabledOrAuto() {
return mode == Mode.ENABLED || mode == Mode.AUTO;
}
/**
* Relaxed socket timeout in milliseconds applied while a maintenance window is open. The
* connection uses the looser of this value and the configured socket timeout ({@code 0} =
* infinite is the loosest), so relaxation never tightens a timeout.
* @since 8.1
*/
public int getRelaxedTimeout() {
return relaxedTimeout;
}
/**
* Relaxed blocking socket timeout in milliseconds; {@code 0} means infinite. The connection uses
* the looser of this value and the configured blocking socket timeout, so relaxation never
* tightens a timeout.
* @since 8.1
*/
public int getRelaxedBlockingTimeout() {
return relaxedBlockingTimeout;
}
/**
* A builder preloaded with the defaults: {@link Mode#AUTO}, auto-resolved endpoint type, relaxed
* timeouts {@link #DEFAULT_RELAXED_SOCKET_TIMEOUT_MS} /
* {@link #DEFAULT_RELAXED_BLOCKING_SOCKET_TIMEOUT_MS}.
* @since 8.1
*/
public static Builder builder() {
return new Builder();
}
/**
* Maintenance notifications disabled. To enable ��� optionally with custom relaxed timeouts ��� use
* {@link #builder()} with mode {@link Mode#AUTO} or {@link Mode#ENABLED}.
* @since 8.1
*/
public static final MaintenanceNotificationsConfig DISABLED = builder().mode(Mode.DISABLED)
.build();
/**
* Builder for {@link MaintenanceNotificationsConfig}.
* @since 8.1
*/
public static class Builder {
private EndpointTypeResolver endpointTypeResolver = AutoEndpointTypeResolver.INSTANCE;
private Mode mode = Mode.AUTO;
private Duration relaxedWindowMaxDuration = DEFAULT_RELAXED_WINDOW_MAX_DURATION;
private int relaxedTimeout = DEFAULT_RELAXED_SOCKET_TIMEOUT_MS;
private int relaxedBlockingTimeout = DEFAULT_RELAXED_BLOCKING_SOCKET_TIMEOUT_MS;
/**
* Requests a fixed endpoint type for all MOVING notifications. Mutually exclusive with
* {@link #autoResolveEndpointType()}; the last call wins.
* @since 8.1
*/
public Builder endpointType(EndpointType endpointType) {
JedisAsserts.notNull(endpointType, "endpointType must not be null");
this.endpointTypeResolver = new FixedEndpointTypeResolver(endpointType);
return this;
}
/**
* Requests the endpoint type resolved per connection from its characteristics: private remote
* IP selects {@code INTERNAL_*}, public {@code EXTERNAL_*}; TLS selects {@code *_FQDN},
* plaintext {@code *_IP}. This is the default. Mutually exclusive with
* {@link #endpointType(EndpointType)}; the last call wins.
* @since 8.1
*/
public Builder autoResolveEndpointType() {
this.endpointTypeResolver = AutoEndpointTypeResolver.INSTANCE;
return this;
}
/**
* The notifications mode; defaults to {@link Mode#AUTO}.
* @since 8.1
*/
public Builder mode(Mode mode) {
this.mode = mode;
return this;
}
/**
* Socket timeout in milliseconds applied while a maintenance window is open; the connection
* uses the looser of this value and the configured socket timeout. Defaults to
* {@link MaintenanceNotificationsConfig#DEFAULT_RELAXED_SOCKET_TIMEOUT_MS}.
* @since 8.1
*/
public Builder relaxedTimeout(int millis) {
this.relaxedTimeout = millis;
return this;
}
/**
* Blocking-command socket timeout in milliseconds applied while a maintenance window is open;
* {@code 0} means infinite. The connection uses the looser of this value and the configured
* blocking socket timeout. Defaults to
* {@link MaintenanceNotificationsConfig#DEFAULT_RELAXED_BLOCKING_SOCKET_TIMEOUT_MS}.
* @since 8.1
*/
public Builder relaxedBlockingTimeout(int millis) {
this.relaxedBlockingTimeout = millis;
return this;
}
/**
* Upper bound on relaxation triggered by MIGRATING/FAILING_OVER. Acts as a safety net: the
* relaxed window reverts after this duration even if the matching closing notification is lost.
* Defaults to {@link MaintenanceNotificationsConfig#DEFAULT_RELAXED_WINDOW_MAX_DURATION}.
* @since 8.1
*/
public Builder relaxedWindowMaxDuration(Duration duration) {
JedisAsserts.notNull(duration, "duration must not be null");
this.relaxedWindowMaxDuration = duration;
return this;
}
/**
* Builds the immutable configuration.
* @since 8.1
*/
public MaintenanceNotificationsConfig build() {
return new MaintenanceNotificationsConfig(this);
}
}
}