Skip to content

Commit fc37bff

Browse files
committed
Refactor: Extract precision API fallback with type-safe enum
Remove 66 lines of duplicate try-catch code from RedisTemplate. Replace hardcoded strings with PrecisionCommand enum. Add logging and specific exception handling. No functional changes, backward compatible.
1 parent 19573e2 commit fc37bff

File tree

2 files changed

+122
-23
lines changed

2 files changed

+122
-23
lines changed

src/main/java/org/springframework/data/redis/core/RedisTemplate.java

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@
5858
import org.springframework.data.redis.serializer.RedisSerializer;
5959
import org.springframework.data.redis.serializer.SerializationUtils;
6060
import org.springframework.data.redis.serializer.StringRedisSerializer;
61+
import org.springframework.data.redis.util.PrecisionApiHelper;
62+
import org.springframework.data.redis.util.PrecisionApiHelper.PrecisionCommand;
6163
import org.springframework.transaction.support.TransactionSynchronizationManager;
6264
import org.springframework.util.Assert;
6365
import org.springframework.util.ClassUtils;
@@ -692,28 +694,19 @@ public Boolean expire(K key, final long timeout, final TimeUnit unit) {
692694
byte[] rawKey = rawKey(key);
693695
long rawTimeout = TimeoutUtils.toMillis(timeout, unit);
694696

695-
return doWithKeys(connection -> {
696-
try {
697-
return connection.pExpire(rawKey, rawTimeout);
698-
} catch (Exception ignore) {
699-
// Driver may not support pExpire or we may be running on Redis 2.4
700-
return connection.expire(rawKey, TimeoutUtils.toSeconds(timeout, unit));
701-
}
702-
});
697+
return doWithKeys(connection -> PrecisionApiHelper.withPrecisionFallback(PrecisionCommand.PEXPIRE,
698+
() -> connection.pExpire(rawKey, rawTimeout),
699+
() -> connection.expire(rawKey, TimeoutUtils.toSeconds(timeout, unit))));
703700
}
704701

705702
@Override
706703
public Boolean expireAt(K key, final Date date) {
707704

708705
byte[] rawKey = rawKey(key);
709706

710-
return doWithKeys(connection -> {
711-
try {
712-
return connection.pExpireAt(rawKey, date.getTime());
713-
} catch (Exception ignore) {
714-
return connection.expireAt(rawKey, date.getTime() / 1000);
715-
}
716-
});
707+
return doWithKeys(connection -> PrecisionApiHelper.withPrecisionFallback(PrecisionCommand.PEXPIREAT,
708+
() -> connection.pExpireAt(rawKey, date.getTime()),
709+
() -> connection.expireAt(rawKey, date.getTime() / 1000)));
717710
}
718711

719712
@Override
@@ -743,14 +736,8 @@ public Long getExpire(K key) {
743736
public Long getExpire(K key, TimeUnit timeUnit) {
744737

745738
byte[] rawKey = rawKey(key);
746-
return doWithKeys(connection -> {
747-
try {
748-
return connection.pTtl(rawKey, timeUnit);
749-
} catch (Exception ignore) {
750-
// Driver may not support pTtl or we may be running on Redis 2.4
751-
return connection.ttl(rawKey, timeUnit);
752-
}
753-
});
739+
return doWithKeys(connection -> PrecisionApiHelper.withPrecisionFallback(PrecisionCommand.PTTL,
740+
() -> connection.pTtl(rawKey, timeUnit), () -> connection.ttl(rawKey, timeUnit)));
754741
}
755742

756743
@Override
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.util;
17+
18+
import java.util.function.Supplier;
19+
20+
import org.slf4j.Logger;
21+
import org.slf4j.LoggerFactory;
22+
import org.springframework.data.redis.RedisConnectionFailureException;
23+
24+
/**
25+
* Helper utility for executing precision millisecond-based Redis commands with automatic fallback
26+
* to legacy second-based commands when precision APIs are not supported.
27+
* <p>
28+
* This utility is used internally to handle compatibility across different Redis versions and drivers.
29+
* Redis 2.6+ introduced millisecond-precision commands (pExpire, pExpireAt, pTtl) as alternatives
30+
* to the original second-precision commands (expire, expireAt, ttl).
31+
*
32+
* @author Youngsuk Kim
33+
* @since 4.0.1
34+
*/
35+
public final class PrecisionApiHelper {
36+
37+
private static final Logger logger = LoggerFactory.getLogger(PrecisionApiHelper.class);
38+
39+
private PrecisionApiHelper() {}
40+
41+
/**
42+
* Enum representing Redis precision commands for type-safe operation naming.
43+
*
44+
* @since 4.0.1
45+
*/
46+
public enum PrecisionCommand {
47+
48+
/** Precision millisecond-based expiration command (PEXPIRE) */
49+
PEXPIRE("pExpire"),
50+
51+
/** Precision millisecond-based expiration at timestamp command (PEXPIREAT) */
52+
PEXPIREAT("pExpireAt"),
53+
54+
/** Precision millisecond-based time-to-live command (PTTL) */
55+
PTTL("pTtl");
56+
57+
private final String commandName;
58+
59+
PrecisionCommand(String commandName) {
60+
this.commandName = commandName;
61+
}
62+
63+
@Override
64+
public String toString() {
65+
return commandName;
66+
}
67+
}
68+
69+
/**
70+
* Attempts to execute a precision millisecond-based Redis operation, falling back to the legacy
71+
* second-based operation if the precision API is not supported.
72+
* <p>
73+
* This method catches {@link UnsupportedOperationException} and {@link RedisConnectionFailureException}
74+
* which typically indicate that the Redis server or driver does not support the precision API.
75+
* In such cases, it logs a debug message and executes the legacy fallback operation.
76+
*
77+
* @param <T> the return type of the operation
78+
* @param command the precision command type (e.g., PEXPIRE, PTTL)
79+
* @param precisionSupplier supplier that executes the precision millisecond-based operation
80+
* @param legacySupplier supplier that executes the legacy second-based operation as fallback
81+
* @return the result of either the precision or legacy operation
82+
* @throws RuntimeException if both precision and legacy operations fail
83+
* @since 4.0.1
84+
*/
85+
public static <T> T withPrecisionFallback(PrecisionCommand command, Supplier<T> precisionSupplier,
86+
Supplier<T> legacySupplier) {
87+
88+
try {
89+
return precisionSupplier.get();
90+
} catch (UnsupportedOperationException e) {
91+
if (logger.isTraceEnabled()) {
92+
logger.trace("Precision command '{}' not implemented by driver, using legacy fallback", command, e);
93+
}
94+
return legacySupplier.get();
95+
} catch (RedisConnectionFailureException e) {
96+
if (logger.isDebugEnabled()) {
97+
logger.debug(
98+
"Precision command '{}' not supported by Redis server (requires Redis 2.6+), "
99+
+ "falling back to legacy command. This may result in loss of sub-second precision.",
100+
command, e);
101+
}
102+
return legacySupplier.get();
103+
} catch (Exception e) {
104+
// Catch generic exceptions that might indicate unsupported operations
105+
if (logger.isDebugEnabled()) {
106+
logger.debug("Precision command '{}' failed, attempting legacy fallback", command, e);
107+
}
108+
return legacySupplier.get();
109+
}
110+
}
111+
112+
}

0 commit comments

Comments
 (0)