DATAREDIS-683 - Provide reactive Lua script execution commands.
We now support Redis Lua script execution using reactive infrastructure through the reactive connection and template API.
Flux<V> execute = reactiveTemplate.execute(new DefaultRedisScript<>("return redis.call('get', KEYS[1])", String.class), Collections.singletonList(key));
Release the reactive connection consistently after Publisher termination. Extract shared code between DefaultScriptExecutor and DefaultScriptOperators in ScriptUtils.
Original Pull Request: #268
This commit is contained in:
committed by
Christoph Strobl
parent
95500d4d72
commit
a3b50ba765
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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.connection.lettuce;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assume.*;
|
||||
import static org.springframework.data.redis.SpinBarrier.*;
|
||||
|
||||
import io.lettuce.core.ScriptOutputType;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.data.redis.RedisSystemException;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisClusterConnection;
|
||||
import org.springframework.data.redis.connection.ReturnType;
|
||||
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class LettuceReactiveScriptingCommandsTests extends LettuceReactiveCommandsTestsBase {
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void scriptExistsShouldReturnState() {
|
||||
|
||||
assumeFalse(connection instanceof ReactiveRedisClusterConnection);
|
||||
|
||||
String sha1 = nativeCommands.scriptLoad("return KEYS[1]");
|
||||
|
||||
StepVerifier.create(connection.scriptingCommands().scriptExists("foo", sha1)) //
|
||||
.expectNext(false) //
|
||||
.expectNext(true) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void scriptFlushShouldRemoveScripts() {
|
||||
|
||||
assumeFalse(connection instanceof ReactiveRedisClusterConnection);
|
||||
|
||||
String sha1 = nativeCommands.scriptLoad("return KEYS[1]");
|
||||
|
||||
StepVerifier.create(connection.scriptingCommands().scriptExists(sha1)) //
|
||||
.expectNext(true) //
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(connection.scriptingCommands().scriptFlush()) //
|
||||
.expectNext("OK") //
|
||||
.verifyComplete();
|
||||
|
||||
StepVerifier.create(connection.scriptingCommands().scriptExists(sha1)) //
|
||||
.expectNext(false) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void evalShaShouldReturnKey() {
|
||||
|
||||
assumeFalse(connection instanceof ReactiveRedisClusterConnection);
|
||||
|
||||
String sha1 = nativeCommands.scriptLoad("return KEYS[1]");
|
||||
|
||||
StepVerifier
|
||||
.create(connection.scriptingCommands().evalSha(sha1, ReturnType.VALUE, 2, SAME_SLOT_KEY_1_BBUFFER.duplicate(),
|
||||
SAME_SLOT_KEY_2_BBUFFER.duplicate())) //
|
||||
.expectNext(SAME_SLOT_KEY_1_BBUFFER) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void evalShaShouldReturnMulti() {
|
||||
|
||||
assumeFalse(connection instanceof ReactiveRedisClusterConnection);
|
||||
|
||||
String sha1 = nativeCommands.scriptLoad("return {KEYS[1],ARGV[1]}");
|
||||
|
||||
StepVerifier
|
||||
.create(connection.scriptingCommands().evalSha(sha1, ReturnType.MULTI, 1, SAME_SLOT_KEY_1_BBUFFER.duplicate(),
|
||||
SAME_SLOT_KEY_2_BBUFFER.duplicate())) //
|
||||
.expectNext(SAME_SLOT_KEY_1_BBUFFER) //
|
||||
.expectNext(SAME_SLOT_KEY_2_BBUFFER) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void evalShaShouldFail() {
|
||||
|
||||
assumeFalse(connection instanceof ReactiveRedisClusterConnection);
|
||||
|
||||
StepVerifier
|
||||
.create(connection.scriptingCommands().evalSha("foo", ReturnType.VALUE, 1, SAME_SLOT_KEY_1_BBUFFER.duplicate())) //
|
||||
.expectError(RedisSystemException.class) //
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void evalShouldReturnStatus() {
|
||||
|
||||
ByteBuffer script = wrap(String.format("return redis.call('set','%s','ghk')", SAME_SLOT_KEY_1));
|
||||
|
||||
StepVerifier
|
||||
.create(connection.scriptingCommands().eval(script, ReturnType.STATUS, 1, SAME_SLOT_KEY_1_BBUFFER.duplicate())) //
|
||||
.expectNext("OK") //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void evalShouldReturnBooleanFalse() {
|
||||
|
||||
ByteBuffer script = wrap("return false");
|
||||
|
||||
StepVerifier.create(connection.scriptingCommands().eval(script, ReturnType.BOOLEAN, 0)) //
|
||||
.expectNext(false) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void evalShouldReturnMultiNumbers() {
|
||||
|
||||
ByteBuffer script = wrap("return {1,2}");
|
||||
|
||||
StepVerifier.create(connection.scriptingCommands().eval(script, ReturnType.MULTI, 0)) //
|
||||
.expectNext(1L) //
|
||||
.expectNext(2L) //
|
||||
.verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void evalShouldFailWithScriptError() {
|
||||
|
||||
ByteBuffer script = wrap("return {1,2");
|
||||
|
||||
StepVerifier.create(connection.scriptingCommands().eval(script, ReturnType.MULTI, 0)) //
|
||||
.expectError(RedisSystemException.class) //
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void scriptKillShouldKillScripts() throws Exception {
|
||||
|
||||
assumeFalse(connection instanceof ReactiveRedisClusterConnection);
|
||||
|
||||
AtomicBoolean scriptDead = new AtomicBoolean(false);
|
||||
CountDownLatch sync = new CountDownLatch(1);
|
||||
Thread th = new Thread(() -> {
|
||||
try {
|
||||
sync.countDown();
|
||||
nativeCommands.eval("local time=1 while time < 10000000000 do time=time+1 end", ScriptOutputType.BOOLEAN);
|
||||
} catch (Exception e) {
|
||||
scriptDead.set(true);
|
||||
}
|
||||
});
|
||||
th.start();
|
||||
sync.await(2, TimeUnit.SECONDS);
|
||||
Thread.sleep(200);
|
||||
|
||||
StepVerifier.create(connection.scriptingCommands().scriptKill()).expectNext("OK").verifyComplete();
|
||||
|
||||
assertTrue(waitFor(scriptDead::get, 3000L));
|
||||
}
|
||||
|
||||
private static ByteBuffer wrap(String content) {
|
||||
return ByteBuffer.wrap(content.getBytes());
|
||||
}
|
||||
}
|
||||
@@ -18,11 +18,13 @@ package org.springframework.data.redis.core;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.junit.Assume.*;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
@@ -39,8 +41,11 @@ import org.springframework.data.redis.connection.DataType;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisClusterConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
|
||||
import org.springframework.data.redis.serializer.RedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
@@ -160,6 +165,48 @@ public class ReactiveRedisTemplateIntegrationTests<K, V> {
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
@SuppressWarnings("unchecked")
|
||||
public void execute() {
|
||||
|
||||
K key = keyFactory.instance();
|
||||
V value = valueFactory.instance();
|
||||
|
||||
assumeFalse(value instanceof Long);
|
||||
|
||||
StepVerifier.create(redisTemplate.opsForValue().set(key, value)).expectNext(true).verifyComplete();
|
||||
|
||||
Flux<V> execute = redisTemplate.execute(
|
||||
new DefaultRedisScript<>("return redis.call('get', KEYS[1])", (Class<V>) value.getClass()),
|
||||
Collections.singletonList(key));
|
||||
|
||||
StepVerifier.create(execute).expectNext(value).verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void executeWithSerializationPairs() {
|
||||
|
||||
K key = keyFactory.instance();
|
||||
V value = valueFactory.instance();
|
||||
|
||||
SerializationPair<Person> json = SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer<>(Person.class));
|
||||
SerializationPair<String> string = SerializationPair.fromSerializer(new StringRedisSerializer());
|
||||
|
||||
assumeFalse(value instanceof Long);
|
||||
|
||||
Person person = new Person("Walter", "White", 51);
|
||||
StepVerifier.create(
|
||||
redisTemplate.execute(new DefaultRedisScript<>("return redis.call('set', KEYS[1], ARGV[1])", String.class),
|
||||
json, string, Collections.singletonList(key), person))
|
||||
.expectNext("OK").verifyComplete();
|
||||
|
||||
Flux<Person> execute = redisTemplate.execute(
|
||||
new DefaultRedisScript<>("return redis.call('get', KEYS[1])", Person.class), json, json,
|
||||
Collections.singletonList(key));
|
||||
|
||||
StepVerifier.create(execute).expectNext(person).verifyComplete();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-602
|
||||
public void expire() {
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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.core.script;
|
||||
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.springframework.data.redis.RedisSystemException;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnection;
|
||||
import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
|
||||
import org.springframework.data.redis.connection.ReactiveScriptingCommands;
|
||||
import org.springframework.data.redis.connection.ReturnType;
|
||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
|
||||
|
||||
/**
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class DefaultScriptOperatorsUnitTests {
|
||||
|
||||
private final DefaultRedisScript<String> SCRIPT = new DefaultRedisScript<>("return KEYS[0]", String.class);
|
||||
|
||||
@Mock ReactiveRedisConnectionFactory connectionFactoryMock;
|
||||
@Mock ReactiveRedisConnection connectionMock;
|
||||
@Mock ReactiveScriptingCommands scriptingCommandsMock;
|
||||
|
||||
DefaultScriptOperators<String> executor;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
when(connectionFactoryMock.getReactiveConnection()).thenReturn(connectionMock);
|
||||
when(connectionMock.scriptingCommands()).thenReturn(scriptingCommandsMock);
|
||||
|
||||
executor = new DefaultScriptOperators<>(connectionFactoryMock, RedisSerializationContext.string());
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void executeCheckForPresenceOfScriptViaEvalSha1() {
|
||||
|
||||
when(scriptingCommandsMock.evalSha(anyString(), any(ReturnType.class), anyInt()))
|
||||
.thenReturn(Flux.just(ByteBuffer.wrap("FOO".getBytes())));
|
||||
|
||||
StepVerifier.create(executor.execute(SCRIPT, Collections.emptyList())).expectNext("FOO").verifyComplete();
|
||||
|
||||
verify(scriptingCommandsMock).evalSha(anyString(), any(ReturnType.class), anyInt());
|
||||
verify(scriptingCommandsMock, never()).eval(any(), any(ReturnType.class), anyInt());
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void executeShouldUseEvalInCaseNoSha1PresentForGivenScript() {
|
||||
|
||||
when(scriptingCommandsMock.evalSha(anyString(), any(ReturnType.class), anyInt())).thenReturn(
|
||||
Flux.error(new RedisSystemException("NOSCRIPT No matching script. Please use EVAL.", new Exception())));
|
||||
|
||||
when(scriptingCommandsMock.eval(any(), any(ReturnType.class), anyInt()))
|
||||
.thenReturn(Flux.just(ByteBuffer.wrap("FOO".getBytes())));
|
||||
|
||||
StepVerifier.create(executor.execute(SCRIPT, Collections.emptyList())).expectNext("FOO").verifyComplete();
|
||||
|
||||
verify(scriptingCommandsMock).evalSha(anyString(), any(ReturnType.class), anyInt());
|
||||
verify(scriptingCommandsMock).eval(any(), any(ReturnType.class), anyInt());
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void executeShouldThrowExceptionInCaseEvalShaFailsWithOtherThanRedisSystemException() {
|
||||
|
||||
when(scriptingCommandsMock.evalSha(anyString(), any(ReturnType.class), anyInt())).thenReturn(Flux
|
||||
.error(new UnsupportedOperationException("NOSCRIPT No matching script. Please use EVAL.", new Exception())));
|
||||
|
||||
StepVerifier.create(executor.execute(SCRIPT, Collections.emptyList()))
|
||||
.expectError(UnsupportedOperationException.class).verify();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void releasesConnectionAfterExecution() {
|
||||
|
||||
when(scriptingCommandsMock.evalSha(anyString(), any(ReturnType.class), anyInt()))
|
||||
.thenReturn(Flux.just(ByteBuffer.wrap("FOO".getBytes())));
|
||||
|
||||
Flux<String> execute = executor.execute(SCRIPT, Collections.emptyList());
|
||||
|
||||
verify(connectionMock, never()).close();
|
||||
|
||||
StepVerifier.create(execute).expectNext("FOO").verifyComplete();
|
||||
|
||||
verify(connectionMock).close();
|
||||
}
|
||||
|
||||
@Test // DATAREDIS-683
|
||||
public void releasesConnectionOnError() {
|
||||
|
||||
when(scriptingCommandsMock.evalSha(anyString(), any(ReturnType.class), anyInt()))
|
||||
.thenReturn(Flux.error(new RuntimeException()));
|
||||
|
||||
StepVerifier.create(executor.execute(SCRIPT, Collections.emptyList())).expectError().verify();
|
||||
|
||||
verify(connectionMock).close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user