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:
Mark Paluch
2017-08-24 12:20:53 +02:00
committed by Christoph Strobl
parent 95500d4d72
commit a3b50ba765
16 changed files with 1126 additions and 54 deletions

View File

@@ -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());
}
}

View File

@@ -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() {

View File

@@ -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();
}
}