Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for Redis setGet command #3017

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>3.4.0-SNAPSHOT</version>
<version>3.4.0-GH-2853-SNAPSHOT</version>

<name>Spring Data Redis</name>
<description>Spring Data module for Redis</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,11 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op
return convertAndReturn(delegate.set(key, value, expiration, option), Converters.identityConverter());
}

@Override
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
return convertAndReturn(delegate.setGet(key, value, expiration, option), Converters.identityConverter());
}

@Override
public Boolean setBit(byte[] key, long offset, boolean value) {
return convertAndReturn(delegate.setBit(key, offset, value), Converters.identityConverter());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,13 @@ default Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption o
return stringCommands().set(key, value, expiration, option);
}

/** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */
@Override
@Deprecated
default byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
return stringCommands().setGet(key, value, expiration, option);
}

/** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */
@Override
@Deprecated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
*
* @author Christoph Strobl
* @author Mark Paluch
* @author Marcin Grzejszczak
* @since 2.0
*/
public interface ReactiveStringCommands {
Expand All @@ -62,15 +63,23 @@ class SetCommand extends KeyCommand {
private @Nullable ByteBuffer value;
private Expiration expiration;
private SetOption option;
private boolean get;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That isn't going to work as the set API returns a boolean response.


private SetCommand(ByteBuffer key, @Nullable ByteBuffer value, @Nullable Expiration expiration,
@Nullable SetOption option) {

this(key, value, expiration, option, false);
}

private SetCommand(ByteBuffer key, @Nullable ByteBuffer value, @Nullable Expiration expiration,
@Nullable SetOption option, boolean get) {

super(key);

this.value = value;
this.expiration = expiration;
this.option = option;
this.get = get;
}

/**
Expand Down Expand Up @@ -125,6 +134,16 @@ public SetCommand withSetOption(SetOption option) {
return new SetCommand(getKey(), value, expiration, option);
}

/**
* Applies GET option. Return the old string stored at key, or {@code null} if key did not exist.
* An error is returned and SET aborted if the value stored at key is not a string.
*
* @return a new {@link SetCommand} with {@link SetOption} applied.
*/
public SetCommand withGet() {
return new SetCommand(getKey(), value, expiration, option, true);
}

/**
* @return
*/
Expand All @@ -146,6 +165,13 @@ public Optional<Expiration> getExpiration() {
public Optional<SetOption> getOption() {
return Optional.ofNullable(option);
}

/**
* @return
*/
public boolean isGet() {
return get;
}
}

/**
Expand Down Expand Up @@ -184,6 +210,22 @@ default Mono<Boolean> set(ByteBuffer key, ByteBuffer value, Expiration expiratio
.map(BooleanResponse::getOutput);
}

/**
* Set {@literal value} for {@literal key} with {@literal expiration} and {@literal options}. Return the old
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
* stored at key is not a string.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
* @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} for no expiration time or
* {@link Expiration#keepTtl()} to keep the existing.
* @param option must not be {@literal null}.
* @return
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
* @since 3.4
*/
Flux<ByteBufferResponse<SetCommand>> setGet(ByteBuffer key, ByteBuffer value, Expiration expiration, SetOption option);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we need both signatures, Mono<ByteBuffer> setGet(ByteBuffer key, ByteBuffer value, Expiration expiration, SetOption option) { and Flux<ByteBufferResponse<SetCommand>> set(Publisher<SetCommand> commands);

The first one delegates to the latter.


/**
* Set each and every item separately by invoking {@link SetCommand}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,22 @@ enum BitOperation {
@Nullable
Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption option);

/**
* Set {@code value} for {@code key}. Return the old string stored at key, or nil if key did not exist.
* An error is returned and SET aborted if the value stored at key is not a string.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
* @param expiration must not be {@literal null}. Use {@link Expiration#persistent()} to not set any ttl or
* {@link Expiration#keepTtl()} to keep the existing expiration.
* @param option must not be {@literal null}. Use {@link SetOption#upsert()} to add non existing.
* @return {@literal null} when used in pipeline / transaction.
* @since 3.4
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
*/
@Nullable
byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option);

/**
* Set {@code value} for {@code key}, only if {@code key} does not exist.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,24 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op
}
}

@Override
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {

Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
Assert.notNull(expiration, "Expiration must not be null");
Assert.notNull(option, "Option must not be null");

SetParams setParams = JedisConverters.toSetCommandExPxArgument(expiration,
JedisConverters.toSetCommandNxXxArgument(option));

try {
return connection.getCluster().setGet(key, value, setParams);
} catch (Exception ex) {
throw convertJedisAccessException(ex);
}
}

@Override
public Boolean setNX(byte[] key, byte[] value) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op
.getOrElse(Converters.stringToBooleanConverter(), () -> false);
}

@Override
@Nullable
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
Assert.notNull(expiration, "Expiration must not be null");
Assert.notNull(option, "Option must not be null");

SetParams params = JedisConverters.toSetCommandExPxArgument(expiration,
JedisConverters.toSetCommandNxXxArgument(option));

return connection.invoke().just(Jedis::setGet, PipelineBinaryCommands::setGet, key, value, params);
}

@Override
public Boolean setNX(byte[] key, byte[] value) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,20 @@ public Flux<BooleanResponse<SetCommand>> set(Publisher<SetCommand> commands) {
}));
}

@Override
public Flux<ByteBufferResponse<SetCommand>> setGet(ByteBuffer key, ByteBuffer value, Expiration exp,
RedisStringCommands.SetOption option) {
return this.connection.execute(reactiveCommands -> Flux.from(Mono.just(SetCommand.set(key).value(value).withGet().expiring(exp).withSetOption(option))).concatMap((command) -> {

Assert.notNull(command.getKey(), "Key must not be null");
Assert.notNull(command.getValue(), "Value must not be null");

return reactiveCommands.setGet(command.getKey(), command.getValue())
.map(v -> new ByteBufferResponse<>(command, v))
.defaultIfEmpty(new AbsentByteBufferResponse<>(command));
}));
}

@Override
public Flux<ByteBufferResponse<SetCommand>> getSet(Publisher<SetCommand> commands) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op
.orElse(LettuceConverters.stringToBooleanConverter(), false);
}

@Override
@Nullable
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
Assert.notNull(key, "Key must not be null");
Assert.notNull(value, "Value must not be null");
Assert.notNull(expiration, "Expiration must not be null");
Assert.notNull(option, "Option must not be null");

return connection.invoke()
.just(RedisStringAsyncCommands::setGet, key, value, LettuceConverters.toSetArgs(expiration, option));
}

@Override
public Boolean setNX(byte[] key, byte[] value) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,27 @@ private boolean failsafeInvokePsetEx(RedisConnection connection) {
});
}

@Override
public V setGet(K key, V value, long timeout, TimeUnit unit) {
return doSetGet(key, value, Expiration.from(timeout, unit));
}

@Override
public V setGet(K key, V value, Duration duration) {
return doSetGet(key, value, Expiration.from(duration));
}

private V doSetGet(K key, V value, Expiration duration) {
byte[] rawValue = rawValue(value);
return execute( new ValueDeserializingRedisCallback(key) {

@Override
protected byte[] inRedis(byte[] rawKey, RedisConnection connection) {
return connection.stringCommands().setGet(rawKey, rawValue, duration, SetOption.UPSERT);
}
});
}

@Override
public Boolean setIfAbsent(K key, V value) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
* @author Christoph Strobl
* @author Mark Paluch
* @author Jiahe Cai
* @author Marcin Grzejszczak
*/
public interface ValueOperations<K, V> {

Expand All @@ -44,6 +45,33 @@ public interface ValueOperations<K, V> {
*/
void set(K key, V value);

/**
* Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
* stored at key is not a string.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
* @param timeout the key expiration timeout.
* @param unit must not be {@literal null}.
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
* @since 3.4
*/
V setGet(K key, V value, long timeout, TimeUnit unit);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want to update BoundValueOperations (the interface) and ReactiveValueOperations as well.


/**
* Set the {@code value} and expiration {@code timeout} for {@code key}. Return the old
* string stored at key, or nil if key did not exist. An error is returned and SET aborted if the value
* stored at key is not a string.
*
* @param key must not be {@literal null}.
* @param value must not be {@literal null}.
* @param duration expiration duration
* @see <a href="https://redis.io/commands/set">Redis Documentation: SET</a>
* @since 3.4
*/
V setGet(K key, V value, Duration duration);

/**
* Set the {@code value} and expiration {@code timeout} for {@code key}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,11 @@ public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption op
return delegate.set(key, value, expiration, options);
}

@Override
public byte[] setGet(byte[] key, byte[] value, Expiration expiration, SetOption option) {
return delegate.setGet(key, value, expiration, option);
}

@Override
public List<Long> bitField(byte[] key, BitFieldSubCommands subCommands) {
return delegate.bitField(key, subCommands);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,4 +556,17 @@ void setKeepTTL() {
assertThat(nativeBinaryCommands.ttl(KEY_1_BBUFFER)).isCloseTo(expireSeconds, Offset.offset(5L));
assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2);
}

@ParameterizedRedisTest // GH-2853
void setWithGetOption() {
nativeCommands.set(KEY_1, VALUE_1);

connection.stringCommands().setGet(KEY_1_BBUFFER, VALUE_2_BBUFFER, Expiration.keepTtl(), SetOption.upsert())
.map(CommandResponse::getOutput)
.as(StepVerifier::create) //
.expectNext(VALUE_1_BBUFFER) //
.verifyComplete();

assertThat(nativeCommands.get(KEY_1)).isEqualTo(VALUE_2);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,32 @@ void testSetWithExpirationWithTimeUnitMilliseconds() {
await().atMost(Duration.ofMillis(500L)).until(() -> !redisTemplate.hasKey(key));
}

@ParameterizedRedisTest
void testSetGetWithExpiration() {

K key = keyFactory.instance();
V value1 = valueFactory.instance();
V value2 = valueFactory.instance();

valueOps.set(key, value1);

assertThat(valueOps.setGet(key, value2, 1, TimeUnit.SECONDS)).isEqualTo(value1);
assertThat(valueOps.get(key)).isEqualTo(value2);
}

@ParameterizedRedisTest
void testSetGetWithExpirationDuration() {

K key = keyFactory.instance();
V value1 = valueFactory.instance();
V value2 = valueFactory.instance();

valueOps.set(key, value1);

assertThat(valueOps.setGet(key, value2, Duration.ofMillis(1000))).isEqualTo(value1);
assertThat(valueOps.get(key)).isEqualTo(value2);
}

@ParameterizedRedisTest
void testAppend() {

Expand Down