Support SET CAS operations IFEQ and IFNE#3304
Support SET CAS operations IFEQ and IFNE#3304Dgramada wants to merge 14 commits intospring-projects:mainfrom
SET CAS operations IFEQ and IFNE#3304Conversation
Introduce SetCondition class to RedisStringCommands with ifValueEqual() method that enables conditional SET operations based on current value comparison. This extends existing set() method overloads to accept SetCondition parameters alongside Expiration, supporting the IFEQ option introduced in Redis 8.4 for atomic compare-and-swap operations. The SetCondition class provides factory methods for upsert(), ifAbsent(), ifPresent(), and ifValueEqual() conditions, maintaining backward compatibility while enabling new CAS functionality. Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
…eactive and non-reactive abstractions. Extended testing to include IFNE logic. Added setGet logic for sync APIs. Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
SET CAS operations IFEQ and IFNE
The refactoring: - Converts SetOption from an enum to a class with cached instances for stateless conditions - Removes the separate SetCondition class and its duplicate API methods - Unifies the API surface by having a single SetOption type for all SET command variations - Maintains backward compatibility with existing SetOption.upsert(), ifAbsent(), and ifPresent() factory methods - Adds ifValueEqual() and ifValueNotEqual() factory methods for CAS operations Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
mp911de
left a comment
There was a problem hiding this comment.
Did a first review pass. The comments regarding version numbers and template API design apply also to ValueOperations and their reactive counterpart.
| class SetOption { | ||
|
|
||
| // Cached instances for stateless conditions | ||
| private static final SetOption UPSERT_INSTANCE = new SetOption(Type.UPSERT, null); |
There was a problem hiding this comment.
Let's keep these constants public to retain compile compatibility. No need to add _INSTANCE to the name.
| private static final SetOption IF_ABSENT_INSTANCE = new SetOption(Type.SET_IF_ABSENT, null); | ||
| private static final SetOption IF_PRESENT_INSTANCE = new SetOption(Type.SET_IF_PRESENT, null); | ||
|
|
||
| private final @NonNull Type type; |
There was a problem hiding this comment.
Please avoid adding @NonNull. Instead, please annotate the class with @NullMarked to opt in for non-null by default.
| */ | ||
| public static SetOption upsert() { | ||
| return UPSERT; | ||
| public static SetOption ifValueEqual(byte @NonNull [] value) { |
There was a problem hiding this comment.
Maybe ifEqual(…) to shorten the name, maybe good for consideration.
| if (this.compareValue == null) { | ||
| return null; | ||
| } | ||
| return Arrays.copyOf(this.compareValue, this.compareValue.length); |
There was a problem hiding this comment.
We typicall do not use copied instances. Depending on our Template API design, this property might be an internal piece anyway.
Also, looking at the calling code of this method, the method should be really non-nullable and throw an IllegalStateException if the value is null and the condition is not one of if-equal/if-not-equal for a clearer usage.
| * {@code SET} command options for {@code NX}, {@code XX}, {@code IFEQ}. | ||
| * | ||
| * @author Yordan Tsintsov | ||
| * @since 4.1.0 |
There was a problem hiding this comment.
Nit: For .0 versions, we would just use 4.1. Less noise.
| * @since 4.1.0 | ||
| * @see <a href="https://redis.io/commands/setnx">Redis Documentation: SET</a> | ||
| */ | ||
| Boolean setIfEqual(@NonNull V newValue, @NonNull V compareValue); |
There was a problem hiding this comment.
I wonder whether we should call these compareAndSet
There was a problem hiding this comment.
Wouldn't it be better to keep setIfNotEqual because it:
- is consistent with existing Spring Data Redis naming: setIfAbsent, setIfPresent
- directly maps to Redis command options: IFEQ, IFNE
| * @since 4.1.0 | ||
| * @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a> | ||
| */ | ||
| Boolean setIfEqual(@NonNull V newValue, @NonNull V compareValue, long timeout, @NonNull TimeUnit unit); |
There was a problem hiding this comment.
Let's use Expiration in new API to avoid repetitive overloads simplifying the overall usage experience.
As follow-up, it would make sense to retrofit existing methods accepting any durations/TimeUnit argument variants to evolve the API into a better model.
Since we have to do some cleanup, the Expiration revision would have to deprecate the other methods so that we evolve towards a more concise interface.
| * @since 4.1.0 | ||
| * @see <a href="https://redis.io/commands/setnx">Redis Documentation: SET</a> | ||
| */ | ||
| Boolean setIfNotEqual(@NonNull V newValue, @NonNull V compareValue); |
There was a problem hiding this comment.
Wouldn't it be better to keep setIfNotEqual because it:
- is consistent with existing Spring Data Redis naming:
setIfAbsent,setIfPresent - directly maps to Redis command options: IFEQ, IFNE
| byte[] cacheLockKey = createCacheLockKey(name); | ||
|
|
||
| while (!ObjectUtils.nullSafeEquals(commands.set(cacheLockKey, new byte[0], expiration, SetOption.SET_IF_ABSENT), | ||
| while (!ObjectUtils.nullSafeEquals(commands.set(cacheLockKey, new byte[0], expiration, SetOption.ifAbsent()), |
There was a problem hiding this comment.
These changes illustrate that preserving the enum constants in the refactored class is a good idea.
Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
…tive function parameter names. Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
viktoriya-kutsarova
left a comment
There was a problem hiding this comment.
To complete the Redis version update, also update Jenkinsfile and pipeline.properties, and move the Dockerfile to ci/openjdk25-redis-8.4/
| * @param condition must not be {@literal null}. | ||
| * @since 4.1 | ||
| */ | ||
| public static SetParams toSetCommandNxXxArgument(SetOption option) { |
There was a problem hiding this comment.
As this is a public method, we should probably first deprecate it and just add the new one.
| * @param condition must not be {@literal null}. | ||
| * @since 4.1 | ||
| */ | ||
| public static SetParams toSetCommandNxXxArgument(SetOption option, SetParams params) { |
There was a problem hiding this comment.
Leave this method, just deprecate it.
|
|
||
| private SetCommand(@Nullable ByteBuffer key, @Nullable ByteBuffer value, @Nullable Expiration expiration, | ||
| @Nullable SetOption option) { | ||
| private SetCommand(@Nullable ByteBuffer key, |
There was a problem hiding this comment.
nit: No need to put each parameter on a separate line.
| * @param condition must not be {@literal null}. | ||
| * @since 4.1 | ||
| */ | ||
| public static SetParams toSetCommandNxXxArgument(SetOption option, SetParams params) { |
There was a problem hiding this comment.
Please deprecate this method instead of deleting it to maintain backward compatibility.
| * @param condition must not be {@literal null}. | ||
| * @since 4.1 | ||
| */ | ||
| public static SetParams toSetCommandNxXxArgument(SetOption option) { |
There was a problem hiding this comment.
Please deprecate this method instead of deleting it to maintain backward compatibility.
| assertThat(result.get(3)).isEqualTo("foo"); | ||
| } | ||
|
|
||
| @Test // GH-3254 |
| assertThat(result.get(3)).isEqualTo("bar-bar"); | ||
| } | ||
|
|
||
| @Test // GH-3254 |
|
|
||
| @Override | ||
| public String toString() { | ||
| return "%s{type=%s, compareValue=%s}".formatted(getClass().getSimpleName(), type, compareValue != null ? "*****" : "<none>"); |
There was a problem hiding this comment.
Is compareValue sensitive here? If not, the ***** masking seems unnecessary - consider removing it from the output.
|
|
||
| private SetCommand(@Nullable ByteBuffer key, @Nullable ByteBuffer value, @Nullable Expiration expiration, | ||
| @Nullable SetOption option) { | ||
| private SetCommand(@Nullable ByteBuffer key, |
There was a problem hiding this comment.
nit: The parameters can stay on two lines.
Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
…perations.java methods Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
…hods in LettuceReactiveStringCommands. Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
Signed-off-by: Yordan Tsintsov <82151074+Dgramada@users.noreply.github.com>
…g. Polished. Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
|
Postponing to M2 (March milestone) |
…c/extend-set-command-options # Conflicts: # Jenkinsfile # ci/openjdk25-redis-8.4/Dockerfile # ci/pipeline.properties
Signed-off-by: Yordan Tsintsov <yordan.tsintsov@redis.com>
This PR introduces support for the Redis
SETcommand'sIFEQandIFNEoptions, which were added in Redis 8.4 to enable atomic compare-and-swap (CAS) operations.Summary
Introduces a new
SetConditionclass inRedisStringCommandsthat provides factory methods for conditional SET operations:upsert()- Always set the value (default behavior)ifAbsent()- Set only if key does not exist (NX)ifPresent()- Set only if key exists (XX)ifValueEqual(byte[])- Set only if current value equals the specified value (IFEQ)ifValueNotEqual(byte[])- Set only if current value does not equal the specified value (IFNE)Changes
Core API:
SetConditionclass with immutable, cached instances for stateless conditionsset()andsetGet()method overloads inRedisStringCommandsto acceptSetConditionStringRedisConnectionandDefaultStringRedisConnectionDriver Implementations:
JedisStringCommands,JedisClusterStringCommands,JedisConverters)LettuceStringCommands,LettuceConverters)LettuceReactiveStringCommands,ReactiveStringCommands)Template Operations:
ValueOperationsandBoundValueOperationswith newset()andsetGet()methodsReactiveValueOperationswith reactive equivalentsReactiveValueOperationsTests:
Compatibility
IFEQ/IFNEfunctionalitySetOptionAPI