From a471bfdba1582724e1ec4c27a8bbca242be6feb5 Mon Sep 17 00:00:00 2001 From: bread Date: Sun, 7 Dec 2025 20:10:30 +0900 Subject: [PATCH] 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. Signed-off-by: bread --- .../data/redis/core/RedisTemplate.java | 33 ++---- .../data/redis/util/PrecisionApiHelper.java | 112 ++++++++++++++++++ 2 files changed, 122 insertions(+), 23 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/util/PrecisionApiHelper.java diff --git a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java index 8b6474fff6..98bad9e910 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java +++ b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java @@ -58,6 +58,8 @@ import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.SerializationUtils; import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.redis.util.PrecisionApiHelper; +import org.springframework.data.redis.util.PrecisionApiHelper.PrecisionCommand; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -692,14 +694,9 @@ public Boolean expire(K key, final long timeout, final TimeUnit unit) { byte[] rawKey = rawKey(key); long rawTimeout = TimeoutUtils.toMillis(timeout, unit); - return doWithKeys(connection -> { - try { - return connection.pExpire(rawKey, rawTimeout); - } catch (Exception ignore) { - // Driver may not support pExpire or we may be running on Redis 2.4 - return connection.expire(rawKey, TimeoutUtils.toSeconds(timeout, unit)); - } - }); + return doWithKeys(connection -> PrecisionApiHelper.withPrecisionFallback(PrecisionCommand.PEXPIRE, + () -> connection.pExpire(rawKey, rawTimeout), + () -> connection.expire(rawKey, TimeoutUtils.toSeconds(timeout, unit)))); } @Override @@ -707,13 +704,9 @@ public Boolean expireAt(K key, final Date date) { byte[] rawKey = rawKey(key); - return doWithKeys(connection -> { - try { - return connection.pExpireAt(rawKey, date.getTime()); - } catch (Exception ignore) { - return connection.expireAt(rawKey, date.getTime() / 1000); - } - }); + return doWithKeys(connection -> PrecisionApiHelper.withPrecisionFallback(PrecisionCommand.PEXPIREAT, + () -> connection.pExpireAt(rawKey, date.getTime()), + () -> connection.expireAt(rawKey, date.getTime() / 1000))); } @Override @@ -743,14 +736,8 @@ public Long getExpire(K key) { public Long getExpire(K key, TimeUnit timeUnit) { byte[] rawKey = rawKey(key); - return doWithKeys(connection -> { - try { - return connection.pTtl(rawKey, timeUnit); - } catch (Exception ignore) { - // Driver may not support pTtl or we may be running on Redis 2.4 - return connection.ttl(rawKey, timeUnit); - } - }); + return doWithKeys(connection -> PrecisionApiHelper.withPrecisionFallback(PrecisionCommand.PTTL, + () -> connection.pTtl(rawKey, timeUnit), () -> connection.ttl(rawKey, timeUnit))); } @Override diff --git a/src/main/java/org/springframework/data/redis/util/PrecisionApiHelper.java b/src/main/java/org/springframework/data/redis/util/PrecisionApiHelper.java new file mode 100644 index 0000000000..f368163a27 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/util/PrecisionApiHelper.java @@ -0,0 +1,112 @@ +/* + * 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 java.util.function.Supplier; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.redis.RedisConnectionFailureException; + +/** + * Helper utility for executing precision millisecond-based Redis commands with automatic fallback + * to legacy second-based commands when precision APIs are not supported. + *

+ * This utility is used internally to handle compatibility across different Redis versions and drivers. + * Redis 2.6+ introduced millisecond-precision commands (pExpire, pExpireAt, pTtl) as alternatives + * to the original second-precision commands (expire, expireAt, ttl). + * + * @author Youngsuk Kim + * @since 4.0.1 + */ +public final class PrecisionApiHelper { + + private static final Logger logger = LoggerFactory.getLogger(PrecisionApiHelper.class); + + private PrecisionApiHelper() {} + + /** + * Enum representing Redis precision commands for type-safe operation naming. + * + * @since 4.0.1 + */ + public enum PrecisionCommand { + + /** Precision millisecond-based expiration command (PEXPIRE) */ + PEXPIRE("pExpire"), + + /** Precision millisecond-based expiration at timestamp command (PEXPIREAT) */ + PEXPIREAT("pExpireAt"), + + /** Precision millisecond-based time-to-live command (PTTL) */ + PTTL("pTtl"); + + private final String commandName; + + PrecisionCommand(String commandName) { + this.commandName = commandName; + } + + @Override + public String toString() { + return commandName; + } + } + + /** + * Attempts to execute a precision millisecond-based Redis operation, falling back to the legacy + * second-based operation if the precision API is not supported. + *

+ * This method catches {@link UnsupportedOperationException} and {@link RedisConnectionFailureException} + * which typically indicate that the Redis server or driver does not support the precision API. + * In such cases, it logs a debug message and executes the legacy fallback operation. + * + * @param the return type of the operation + * @param command the precision command type (e.g., PEXPIRE, PTTL) + * @param precisionSupplier supplier that executes the precision millisecond-based operation + * @param legacySupplier supplier that executes the legacy second-based operation as fallback + * @return the result of either the precision or legacy operation + * @throws RuntimeException if both precision and legacy operations fail + * @since 4.0.1 + */ + public static T withPrecisionFallback(PrecisionCommand command, Supplier precisionSupplier, + Supplier legacySupplier) { + + try { + return precisionSupplier.get(); + } catch (UnsupportedOperationException e) { + if (logger.isTraceEnabled()) { + logger.trace("Precision command '{}' not implemented by driver, using legacy fallback", command, e); + } + return legacySupplier.get(); + } catch (RedisConnectionFailureException e) { + if (logger.isDebugEnabled()) { + logger.debug( + "Precision command '{}' not supported by Redis server (requires Redis 2.6+), " + + "falling back to legacy command. This may result in loss of sub-second precision.", + command, e); + } + return legacySupplier.get(); + } catch (Exception e) { + // Catch generic exceptions that might indicate unsupported operations + if (logger.isDebugEnabled()) { + logger.debug("Precision command '{}' failed, attempting legacy fallback", command, e); + } + return legacySupplier.get(); + } + } + +}