Skip to content

Commit 773acd0

Browse files
committed
Pass-thru custom Redis commands using Lettuce.
We now accept unknown custom Redis commands when using the Lettuce driver. Previously, custom commands were required to exist in Lettuce's CommandType enumeration and unknown commands (such as modules) failed to run. Closes #1979
1 parent 9cc6fa3 commit 773acd0

File tree

2 files changed

+84
-7
lines changed

2 files changed

+84
-7
lines changed

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

+62-6
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,12 @@
3636
import io.lettuce.core.protocol.Command;
3737
import io.lettuce.core.protocol.CommandArgs;
3838
import io.lettuce.core.protocol.CommandType;
39+
import io.lettuce.core.protocol.ProtocolKeyword;
3940
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
4041
import io.lettuce.core.sentinel.api.StatefulRedisSentinelConnection;
4142

4243
import java.lang.reflect.Constructor;
44+
import java.nio.charset.StandardCharsets;
4345
import java.util.ArrayList;
4446
import java.util.Collections;
4547
import java.util.HashMap;
@@ -389,7 +391,7 @@ public Object execute(String command, @Nullable CommandOutput commandOutputTypeH
389391
Assert.hasText(command, "a valid command needs to be specified");
390392

391393
String name = command.trim().toUpperCase();
392-
CommandType commandType = CommandType.valueOf(name);
394+
ProtocolKeyword commandType = getCommandType(name);
393395

394396
validateCommandIfRunningInTransactionMode(commandType, args);
395397

@@ -1109,14 +1111,14 @@ io.lettuce.core.ScanCursor getScanCursor(long cursorId) {
11091111
return io.lettuce.core.ScanCursor.of(Long.toString(cursorId));
11101112
}
11111113

1112-
private void validateCommandIfRunningInTransactionMode(CommandType cmd, byte[]... args) {
1114+
private void validateCommandIfRunningInTransactionMode(ProtocolKeyword cmd, byte[]... args) {
11131115

11141116
if (this.isQueueing()) {
11151117
validateCommand(cmd, args);
11161118
}
11171119
}
11181120

1119-
private void validateCommand(CommandType cmd, @Nullable byte[]... args) {
1121+
private void validateCommand(ProtocolKeyword cmd, @Nullable byte[]... args) {
11201122

11211123
RedisCommand redisCommand = RedisCommand.failsafeCommandLookup(cmd.name());
11221124
if (!RedisCommand.UNKNOWN.equals(redisCommand) && redisCommand.requiresArguments()) {
@@ -1128,6 +1130,15 @@ private void validateCommand(CommandType cmd, @Nullable byte[]... args) {
11281130
}
11291131
}
11301132

1133+
private static ProtocolKeyword getCommandType(String name) {
1134+
1135+
try {
1136+
return CommandType.valueOf(name);
1137+
} catch (IllegalArgumentException e) {
1138+
return new CustomCommandType(name);
1139+
}
1140+
}
1141+
11311142
/**
11321143
* {@link TypeHints} provide {@link CommandOutput} information for a given {@link CommandType}.
11331144
*
@@ -1136,7 +1147,7 @@ private void validateCommand(CommandType cmd, @Nullable byte[]... args) {
11361147
static class TypeHints {
11371148

11381149
@SuppressWarnings("rawtypes") //
1139-
private static final Map<CommandType, Class<? extends CommandOutput>> COMMAND_OUTPUT_TYPE_MAPPING = new HashMap<>();
1150+
private static final Map<ProtocolKeyword, Class<? extends CommandOutput>> COMMAND_OUTPUT_TYPE_MAPPING = new HashMap<>();
11401151

11411152
@SuppressWarnings("rawtypes") //
11421153
private static final Map<Class<?>, Constructor<CommandOutput>> CONSTRUCTORS = new ConcurrentHashMap<>();
@@ -1298,7 +1309,7 @@ static class TypeHints {
12981309
* @return {@link ByteArrayOutput} as default when no matching {@link CommandOutput} available.
12991310
*/
13001311
@SuppressWarnings("rawtypes")
1301-
public CommandOutput getTypeHint(CommandType type) {
1312+
public CommandOutput getTypeHint(ProtocolKeyword type) {
13021313
return getTypeHint(type, new ByteArrayOutput<>(CODEC));
13031314
}
13041315

@@ -1309,7 +1320,7 @@ public CommandOutput getTypeHint(CommandType type) {
13091320
* @return
13101321
*/
13111322
@SuppressWarnings("rawtypes")
1312-
public CommandOutput getTypeHint(CommandType type, CommandOutput defaultType) {
1323+
public CommandOutput getTypeHint(ProtocolKeyword type, CommandOutput defaultType) {
13131324

13141325
if (type == null || !COMMAND_OUTPUT_TYPE_MAPPING.containsKey(type)) {
13151326
return defaultType;
@@ -1552,4 +1563,49 @@ public void onClose(StatefulConnection<?, ?> connection) {
15521563
connection.setAutoFlushCommands(true);
15531564
}
15541565
}
1566+
1567+
/**
1568+
* @since 2.3.8
1569+
*/
1570+
static class CustomCommandType implements ProtocolKeyword {
1571+
1572+
private final String name;
1573+
1574+
CustomCommandType(String name) {
1575+
this.name = name;
1576+
}
1577+
1578+
@Override
1579+
public byte[] getBytes() {
1580+
return name.getBytes(StandardCharsets.US_ASCII);
1581+
}
1582+
1583+
@Override
1584+
public String name() {
1585+
return name;
1586+
}
1587+
1588+
@Override
1589+
public boolean equals(Object o) {
1590+
1591+
if (this == o) {
1592+
return true;
1593+
}
1594+
if (!(o instanceof CustomCommandType)) {
1595+
return false;
1596+
}
1597+
CustomCommandType that = (CustomCommandType) o;
1598+
return ObjectUtils.nullSafeEquals(name, that.name);
1599+
}
1600+
1601+
@Override
1602+
public int hashCode() {
1603+
return ObjectUtils.nullSafeHashCode(name);
1604+
}
1605+
1606+
@Override
1607+
public String toString() {
1608+
return name;
1609+
}
1610+
}
15551611
}

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

+22-1
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,18 @@
1919
import static org.mockito.Mockito.*;
2020

2121
import io.lettuce.core.RedisClient;
22+
import io.lettuce.core.RedisFuture;
2223
import io.lettuce.core.XAddArgs;
2324
import io.lettuce.core.XClaimArgs;
2425
import io.lettuce.core.api.StatefulRedisConnection;
2526
import io.lettuce.core.api.async.RedisAsyncCommands;
2627
import io.lettuce.core.api.sync.RedisCommands;
28+
import io.lettuce.core.codec.ByteArrayCodec;
2729
import io.lettuce.core.codec.RedisCodec;
30+
import io.lettuce.core.output.StatusOutput;
31+
import io.lettuce.core.protocol.AsyncCommand;
32+
import io.lettuce.core.protocol.Command;
33+
import io.lettuce.core.protocol.CommandArgs;
2834

2935
import java.lang.reflect.InvocationTargetException;
3036
import java.time.Duration;
@@ -33,6 +39,7 @@
3339
import org.junit.jupiter.api.BeforeEach;
3440
import org.junit.jupiter.api.Test;
3541
import org.mockito.ArgumentCaptor;
42+
3643
import org.springframework.dao.InvalidDataAccessResourceUsageException;
3744
import org.springframework.data.redis.connection.AbstractConnectionUnitTestBase;
3845
import org.springframework.data.redis.connection.RedisServerCommands.ShutdownOption;
@@ -198,7 +205,6 @@ void xClaimShouldNotAddJustIdFlagToArgs() {
198205
}
199206

200207
assertThat(ReflectionTestUtils.getField(args.getValue(), "justid")).isEqualTo(false);
201-
202208
}
203209

204210
@Test // DATAREDIS-1226
@@ -216,6 +222,21 @@ void xClaimJustIdShouldAddJustIdFlagToArgs() {
216222

217223
assertThat(ReflectionTestUtils.getField(args.getValue(), "justid")).isEqualTo(true);
218224
}
225+
226+
@Test // GH-1979
227+
void executeShouldPassThruCustomCommands() {
228+
229+
Command<byte[], byte[], String> command = new Command<>(new LettuceConnection.CustomCommandType("FOO.BAR"),
230+
new StatusOutput<>(ByteArrayCodec.INSTANCE));
231+
AsyncCommand<byte[], byte[], String> future = new AsyncCommand<>(command);
232+
future.complete();
233+
234+
when(asyncCommandsMock.dispatch(any(), any(), any())).thenReturn((RedisFuture) future);
235+
236+
connection.execute("foo.bar", command.getOutput());
237+
238+
verify(asyncCommandsMock).dispatch(eq(command.getType()), eq(command.getOutput()), any(CommandArgs.class));
239+
}
219240
}
220241

221242
public static class LettucePipelineConnectionUnitTests extends BasicUnitTests {

0 commit comments

Comments
 (0)