Skip to content

Commit bcfcf28

Browse files
mp911dechristophstrobl
authored andcommitted
DATAREDIS-711 - Emit Lua array responses as List.
We now emit array responses from Lua script execution as complete List instead of emitting the individual elements through the Publisher. Lua scripts may return nil (null) elements that would be not emitted through a Publisher. Skipping null values would garble up the response – and in several cases, the response array is required as List. Emitting the whole List aligns the response to the signatures imposed by generics and aligns the behavior with the imperative API. Original Pull Request: #282
1 parent 95ad34d commit bcfcf28

File tree

7 files changed

+297
-13
lines changed

7 files changed

+297
-13
lines changed

src/main/java/org/springframework/data/redis/connection/ReactiveScriptingCommands.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ default Mono<Boolean> scriptExists(String scriptSha) {
8282
* Evaluate given {@code script}.
8383
*
8484
* @param script must not be {@literal null}.
85-
* @param returnType must not be {@literal null}.
85+
* @param returnType must not be {@literal null}. Using {@link ReturnType#MULTI} emits a {@link List} as-is instead of
86+
* emitting the individual elements from the array response.
8687
* @param numKeys
8788
* @param keysAndArgs must not be {@literal null}.
8889
* @return never {@literal null}.
@@ -94,7 +95,8 @@ default Mono<Boolean> scriptExists(String scriptSha) {
9495
* Evaluate given {@code scriptSha}.
9596
*
9697
* @param scriptSha must not be {@literal null}.
97-
* @param returnType must not be {@literal null}.
98+
* @param returnType must not be {@literal null}. Using {@link ReturnType#MULTI} emits a {@link List} as-is instead of
99+
* emitting the individual elements from the array response.
98100
* @param numKeys
99101
* @param keysAndArgs must not be {@literal null}.
100102
* @return never {@literal null}.

src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommands.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,6 @@ private <T> Flux<T> convertIfNecessary(Flux<T> eval, ReturnType returnType) {
141141
if (returnType == ReturnType.MULTI) {
142142

143143
return eval.flatMap(t -> {
144-
return t instanceof Iterable ? Flux.fromIterable((Iterable<T>) t) : Flux.just(t);
145-
}).flatMap(t -> {
146144
return t instanceof Exception ? Flux.error(connection.translateException().apply((Exception) t)) : Flux.just(t);
147145
});
148146
}

src/main/java/org/springframework/data/redis/core/script/DefaultReactiveScriptExecutor.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,10 @@ public <T> Flux<T> execute(RedisScript<T> script, List<K> keys, List<?> args) {
7575
Assert.notNull(keys, "Keys must not be null!");
7676
Assert.notNull(args, "Args must not be null!");
7777

78-
// use the Template's value serializer for args and result
79-
return execute(script, keys, args, serializationContext.getKeySerializationPair().getWriter(),
80-
(RedisElementReader<T>) serializationContext.getValueSerializationPair().getReader());
78+
SerializationPair<?> serializationPair = serializationContext.getValueSerializationPair();
79+
80+
return execute(script, keys, args, serializationPair.getWriter(),
81+
(RedisElementReader<T>) serializationPair.getReader());
8182
}
8283

8384
/*

src/test/java/org/springframework/data/redis/SettingsUtils.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717

1818
import java.util.Properties;
1919

20+
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
21+
2022
/**
2123
* @author Costin Leau
24+
* @author Mark Paluch
2225
*/
2326
public abstract class SettingsUtils {
2427
private final static Properties DEFAULTS = new Properties();
@@ -44,4 +47,8 @@ public static String getHost() {
4447
public static int getPort() {
4548
return Integer.valueOf(SETTINGS.getProperty("port"));
4649
}
50+
51+
public static RedisStandaloneConfiguration standaloneConfiguration() {
52+
return new RedisStandaloneConfiguration(getHost(), getPort());
53+
}
4754
}

src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveScriptingCommandsTests.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void evalShaShouldReturnKey() {
8686
.verifyComplete();
8787
}
8888

89-
@Test // DATAREDIS-683
89+
@Test // DATAREDIS-683, DATAREDIS-711
9090
public void evalShaShouldReturnMulti() {
9191

9292
assumeFalse(connection instanceof ReactiveRedisClusterConnection);
@@ -96,8 +96,7 @@ public void evalShaShouldReturnMulti() {
9696
StepVerifier
9797
.create(connection.scriptingCommands().evalSha(sha1, ReturnType.MULTI, 1, SAME_SLOT_KEY_1_BBUFFER.duplicate(),
9898
SAME_SLOT_KEY_2_BBUFFER.duplicate())) //
99-
.expectNext(SAME_SLOT_KEY_1_BBUFFER) //
100-
.expectNext(SAME_SLOT_KEY_2_BBUFFER) //
99+
.expectNext(Arrays.asList(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER)) //
101100
.verifyComplete();
102101
}
103102

@@ -133,14 +132,13 @@ public void evalShouldReturnBooleanFalse() {
133132
.verifyComplete();
134133
}
135134

136-
@Test // DATAREDIS-683
135+
@Test // DATAREDIS-683, DATAREDIS-711
137136
public void evalShouldReturnMultiNumbers() {
138137

139138
ByteBuffer script = wrap("return {1,2}");
140139

141140
StepVerifier.create(connection.scriptingCommands().eval(script, ReturnType.MULTI, 0)) //
142-
.expectNext(1L) //
143-
.expectNext(2L) //
141+
.expectNext(Arrays.asList(1L, 2L)) //
144142
.verifyComplete();
145143
}
146144

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.connection.lettuce;
17+
18+
import io.lettuce.core.resource.ClientResources;
19+
20+
import java.time.Duration;
21+
22+
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder;
23+
24+
/**
25+
* Creates a specific client configuration for Lettuce tests.
26+
*
27+
* @author Mark Paluch
28+
*/
29+
public class LettuceTestClientConfiguration {
30+
31+
/**
32+
* @return an initialized {@link LettuceClientConfigurationBuilder} with shutdown timeout and {@link ClientResources}.
33+
* @see LettuceTestClientResources#getSharedClientResources()
34+
*/
35+
public static LettuceClientConfigurationBuilder builder() {
36+
return LettuceClientConfiguration.builder().shutdownTimeout(Duration.ZERO)
37+
.clientResources(LettuceTestClientResources.getSharedClientResources());
38+
}
39+
40+
/**
41+
* @return a defaulted {@link LettuceClientConfiguration} for testing.
42+
*/
43+
public static LettuceClientConfiguration create() {
44+
return builder().build();
45+
}
46+
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
/*
2+
* Copyright 2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.redis.core.script;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
20+
import reactor.core.publisher.Flux;
21+
import reactor.test.StepVerifier;
22+
23+
import java.util.Arrays;
24+
import java.util.Collections;
25+
import java.util.List;
26+
27+
import org.junit.AfterClass;
28+
import org.junit.Before;
29+
import org.junit.BeforeClass;
30+
import org.junit.Test;
31+
import org.springframework.core.io.ClassPathResource;
32+
import org.springframework.data.redis.Person;
33+
import org.springframework.data.redis.SettingsUtils;
34+
import org.springframework.data.redis.connection.RedisConnection;
35+
import org.springframework.data.redis.connection.RedisConnectionFactory;
36+
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
37+
import org.springframework.data.redis.connection.lettuce.LettuceTestClientConfiguration;
38+
import org.springframework.data.redis.core.RedisTemplate;
39+
import org.springframework.data.redis.core.StringRedisTemplate;
40+
import org.springframework.data.redis.serializer.GenericToStringSerializer;
41+
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
42+
import org.springframework.data.redis.serializer.RedisElementReader;
43+
import org.springframework.data.redis.serializer.RedisElementWriter;
44+
import org.springframework.data.redis.serializer.RedisSerializationContext;
45+
import org.springframework.data.redis.serializer.RedisSerializationContext.RedisSerializationContextBuilder;
46+
import org.springframework.data.redis.serializer.StringRedisSerializer;
47+
import org.springframework.scripting.support.StaticScriptSource;
48+
49+
/**
50+
* @author Mark Paluch
51+
*/
52+
public class DefaultReactiveScriptExecutorTests {
53+
54+
private static LettuceConnectionFactory connectionFactory;
55+
private static StringRedisTemplate stringTemplate;
56+
private static ReactiveScriptExecutor<String> stringScriptExecutor;
57+
58+
@BeforeClass
59+
public static void setUp() {
60+
61+
connectionFactory = new LettuceConnectionFactory(SettingsUtils.standaloneConfiguration(),
62+
LettuceTestClientConfiguration.create());
63+
connectionFactory.afterPropertiesSet();
64+
65+
stringTemplate = new StringRedisTemplate(connectionFactory);
66+
stringScriptExecutor = new DefaultReactiveScriptExecutor<>(connectionFactory, RedisSerializationContext.string());
67+
}
68+
69+
@AfterClass
70+
public static void cleanUp() {
71+
72+
if (connectionFactory != null) {
73+
connectionFactory.destroy();
74+
}
75+
}
76+
77+
@Before
78+
public void before() {
79+
80+
RedisConnection connection = connectionFactory.getConnection();
81+
connection.flushDb();
82+
connection.close();
83+
}
84+
85+
protected RedisConnectionFactory getConnectionFactory() {
86+
return connectionFactory;
87+
}
88+
89+
@Test // DATAREDIS-711
90+
public void shouldReturnLong() {
91+
92+
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
93+
script.setLocation(new ClassPathResource("org/springframework/data/redis/core/script/increment.lua"));
94+
script.setResultType(Long.class);
95+
96+
StepVerifier.create(stringScriptExecutor.execute(script, Collections.singletonList("mykey"))).verifyComplete();
97+
98+
stringTemplate.opsForValue().set("mykey", "2");
99+
100+
StepVerifier.create(stringScriptExecutor.execute(script, Collections.singletonList("mykey"))).expectNext(3L)
101+
.verifyComplete();
102+
}
103+
104+
@Test // DATAREDIS-711
105+
public void shouldReturnBoolean() {
106+
107+
RedisSerializationContextBuilder<String, Long> builder = RedisSerializationContext
108+
.newSerializationContext(new StringRedisSerializer());
109+
builder.value(new GenericToStringSerializer<>(Long.class));
110+
111+
DefaultRedisScript<Boolean> script = new DefaultRedisScript<>();
112+
script.setLocation(new ClassPathResource("org/springframework/data/redis/core/script/cas.lua"));
113+
script.setResultType(Boolean.class);
114+
115+
ReactiveScriptExecutor<String> scriptExecutor = new DefaultReactiveScriptExecutor<>(connectionFactory,
116+
builder.build());
117+
118+
stringTemplate.opsForValue().set("counter", "0");
119+
120+
StepVerifier.create(scriptExecutor.execute(script, Collections.singletonList("counter"), Arrays.asList(0, 3)))
121+
.expectNext(true).verifyComplete();
122+
123+
StepVerifier.create(scriptExecutor.execute(script, Collections.singletonList("counter"), Arrays.asList(0, 3)))
124+
.expectNext(false).verifyComplete();
125+
}
126+
127+
@Test // DATAREDIS-711
128+
@SuppressWarnings({ "unchecked", "rawtypes" })
129+
public void shouldApplyCustomArgsSerializer() {
130+
131+
DefaultRedisScript<List> script = new DefaultRedisScript<>();
132+
script.setLocation(new ClassPathResource("org/springframework/data/redis/core/script/bulkpop.lua"));
133+
script.setResultType(List.class);
134+
135+
stringTemplate.boundListOps("mylist").leftPushAll("a", "b", "c", "d");
136+
137+
Flux<List<String>> mylist = stringScriptExecutor.execute(script, Collections.singletonList("mylist"),
138+
Collections.singletonList(1L), RedisElementWriter.from(new GenericToStringSerializer<>(Long.class)),
139+
(RedisElementReader) RedisElementReader.from(new StringRedisSerializer()));
140+
141+
StepVerifier.create(mylist).expectNext(Collections.singletonList("a")).verifyComplete();
142+
}
143+
144+
@Test // DATAREDIS-711
145+
public void testExecuteMixedListResult() {
146+
147+
DefaultRedisScript<List> script = new DefaultRedisScript<>();
148+
script.setLocation(new ClassPathResource("org/springframework/data/redis/core/script/popandlength.lua"));
149+
script.setResultType(List.class);
150+
151+
StepVerifier.create(stringScriptExecutor.execute(script, Collections.singletonList("mylist")))
152+
.expectNext(Arrays.asList(null, 0L)).verifyComplete();
153+
154+
stringTemplate.boundListOps("mylist").leftPushAll("a", "b");
155+
156+
StepVerifier.create(stringScriptExecutor.execute(script, Collections.singletonList("mylist")))
157+
.expectNext(Arrays.asList("a", 1L)).verifyComplete();
158+
}
159+
160+
@Test // DATAREDIS-711
161+
public void shouldReturnValueResult() {
162+
163+
DefaultRedisScript<String> script = new DefaultRedisScript<>();
164+
script.setScriptText("return redis.call('GET',KEYS[1])");
165+
script.setResultType(String.class);
166+
167+
stringTemplate.opsForValue().set("foo", "bar");
168+
169+
Flux<String> foo = stringScriptExecutor.execute(script, Collections.singletonList("foo"));
170+
171+
StepVerifier.create(foo).expectNext("bar").expectNext();
172+
}
173+
174+
@Test // DATAREDIS-711
175+
@SuppressWarnings({ "unchecked", "rawtypes" })
176+
public void shouldReturnStatusValue() {
177+
178+
DefaultRedisScript script = new DefaultRedisScript();
179+
script.setScriptText("return redis.call('SET',KEYS[1], ARGV[1])");
180+
181+
RedisSerializationContextBuilder<String, Long> builder = RedisSerializationContext
182+
.newSerializationContext(new StringRedisSerializer());
183+
builder.value(new GenericToStringSerializer<>(Long.class));
184+
185+
ReactiveScriptExecutor<String> scriptExecutor = new DefaultReactiveScriptExecutor<>(connectionFactory,
186+
builder.build());
187+
188+
StepVerifier.create(scriptExecutor.execute(script, Collections.singletonList("foo"), Collections.singletonList(3L)))
189+
.expectNext("OK").verifyComplete();
190+
191+
assertThat(stringTemplate.opsForValue().get("foo")).isEqualTo("3");
192+
}
193+
194+
@Test // DATAREDIS-711
195+
public void shouldApplyCustomResultSerializer() {
196+
197+
Jackson2JsonRedisSerializer<Person> personSerializer = new Jackson2JsonRedisSerializer<>(Person.class);
198+
199+
RedisTemplate<String, Person> template = new RedisTemplate<>();
200+
template.setKeySerializer(new StringRedisSerializer());
201+
template.setValueSerializer(personSerializer);
202+
template.setConnectionFactory(getConnectionFactory());
203+
template.afterPropertiesSet();
204+
205+
DefaultRedisScript<String> script = new DefaultRedisScript<>();
206+
script.setScriptSource(new StaticScriptSource("redis.call('SET',KEYS[1], ARGV[1])\nreturn 'FOO'"));
207+
script.setResultType(String.class);
208+
209+
Person joe = new Person("Joe", "Schmoe", 23);
210+
Flux<String> result = stringScriptExecutor.execute(script, Collections.singletonList("bar"),
211+
Collections.singletonList(joe), RedisElementWriter.from(personSerializer),
212+
RedisElementReader.from(new StringRedisSerializer()));
213+
214+
StepVerifier.create(result).expectNext("FOO").verifyComplete();
215+
216+
assertThat(template.opsForValue().get("bar")).isEqualTo(joe);
217+
}
218+
219+
@Test // DATAREDIS-711
220+
public void testExecuteCachedNullKeys() {
221+
222+
DefaultRedisScript<String> script = new DefaultRedisScript<>();
223+
script.setScriptText("return 'HELLO'");
224+
script.setResultType(String.class);
225+
226+
// Execute script twice, second time should be from cache
227+
StepVerifier.create(stringScriptExecutor.execute(script, Collections.emptyList())).expectNext("HELLO")
228+
.verifyComplete();
229+
StepVerifier.create(stringScriptExecutor.execute(script, Collections.emptyList())).expectNext("HELLO")
230+
.verifyComplete();
231+
}
232+
}

0 commit comments

Comments
 (0)