From 4dfafc722a711bb373e40f6c7bba7451b9070309 Mon Sep 17 00:00:00 2001 From: Nikita Koksharov Date: Mon, 21 Nov 2022 10:01:30 +0300 Subject: [PATCH] Feature - Spring Data Redis 3.0 implementation #4689 --- redisson-spring-data/pom.xml | 1 + .../redisson-spring-data-30/pom.xml | 76 + .../data/connection/BinaryConvertor.java | 37 + .../ByteBufferGeoResultsDecoder.java | 67 + .../data/connection/DataTypeConvertor.java | 34 + .../data/connection/DistanceConvertor.java | 41 + .../data/connection/GeoResultsDecoder.java | 66 + .../connection/ObjectListReplayDecoder2.java | 61 + .../spring/data/connection/PointDecoder.java | 50 + .../data/connection/PropertiesDecoder.java | 46 + .../connection/PropertiesListDecoder.java | 44 + .../connection/RedisClusterNodeDecoder.java | 120 + .../data/connection/RedissonBaseCommands.java | 40 + .../data/connection/RedissonBaseReactive.java | 95 + .../connection/RedissonClusterConnection.java | 558 ++++ .../data/connection/RedissonConnection.java | 2407 +++++++++++++++++ .../connection/RedissonConnectionFactory.java | 170 ++ .../RedissonExceptionConverter.java | 62 + .../RedissonReactiveClusterGeoCommands.java | 32 + .../RedissonReactiveClusterHashCommands.java | 32 + ...sonReactiveClusterHyperLogLogCommands.java | 32 + .../RedissonReactiveClusterKeyCommands.java | 143 + .../RedissonReactiveClusterListCommands.java | 32 + ...RedissonReactiveClusterNumberCommands.java | 32 + ...RedissonReactiveClusterServerCommands.java | 153 ++ .../RedissonReactiveClusterSetCommands.java | 32 + ...RedissonReactiveClusterStreamCommands.java | 31 + ...RedissonReactiveClusterStringCommands.java | 32 + .../RedissonReactiveClusterZSetCommands.java | 35 + .../RedissonReactiveGeoCommands.java | 372 +++ .../RedissonReactiveHashCommands.java | 302 +++ .../RedissonReactiveHyperLogLogCommands.java | 98 + .../RedissonReactiveKeyCommands.java | 371 +++ .../RedissonReactiveListCommands.java | 370 +++ .../RedissonReactiveNumberCommands.java | 119 + .../RedissonReactivePubSubCommands.java | 64 + ...edissonReactiveRedisClusterConnection.java | 226 ++ .../RedissonReactiveRedisConnection.java | 111 + .../RedissonReactiveScriptingCommands.java | 150 + .../RedissonReactiveServerCommands.java | 201 ++ .../RedissonReactiveSetCommands.java | 320 +++ .../RedissonReactiveStreamCommands.java | 496 ++++ .../RedissonReactiveStringCommands.java | 459 ++++ .../RedissonReactiveSubscription.java | 313 +++ .../RedissonReactiveZSetCommands.java | 888 ++++++ .../RedissonSentinelConnection.java | 91 + .../connection/RedissonStreamCommands.java | 635 +++++ .../data/connection/RedissonSubscription.java | 179 ++ .../ScoredSortedListReplayDecoder.java | 53 + .../ScoredSortedSetReplayDecoder.java | 54 + .../data/connection/SecondsConvertor.java | 43 + .../data/connection/SetReplayDecoder.java | 51 + .../src/test/java/org/redisson/BaseTest.java | 86 + .../test/java/org/redisson/ClusterRunner.java | 156 ++ .../test/java/org/redisson/RedisRunner.java | 1070 ++++++++ .../test/java/org/redisson/RedisVersion.java | 58 + .../redisson/RedissonRuntimeEnvironment.java | 21 + .../data/connection/BaseConnectionTest.java | 16 + .../RedissonClusterConnectionRenameTest.java | 166 ++ .../RedissonClusterConnectionTest.java | 293 ++ .../connection/RedissonConnectionTest.java | 125 + .../RedissonMultiConnectionTest.java | 96 + .../RedissonPipelineConnectionTest.java | 64 + ...edissonReactiveClusterKeyCommandsTest.java | 193 ++ .../RedissonScriptReactiveTest.java | 33 + .../RedissonSentinelConnectionTest.java | 132 + .../data/connection/RedissonStreamTest.java | 76 + .../RedissonSubscribeReactiveTest.java | 113 + .../connection/RedissonSubscribeTest.java | 144 + .../src/test/resources/logback.xml | 36 + 70 files changed, 13405 insertions(+) create mode 100644 redisson-spring-data/redisson-spring-data-30/pom.xml create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/BinaryConvertor.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ByteBufferGeoResultsDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/DataTypeConvertor.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/DistanceConvertor.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/GeoResultsDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ObjectListReplayDecoder2.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PointDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PropertiesDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PropertiesListDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedisClusterNodeDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonBaseCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonBaseReactive.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonClusterConnection.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnectionFactory.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonExceptionConverter.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterGeoCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterHashCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterHyperLogLogCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterKeyCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterListCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterNumberCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterServerCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterSetCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterStreamCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterStringCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterZSetCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveGeoCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveHashCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveHyperLogLogCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveKeyCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveListCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveNumberCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactivePubSubCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveRedisClusterConnection.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveRedisConnection.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveScriptingCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveServerCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveSetCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveStreamCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveStringCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveSubscription.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveZSetCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonSentinelConnection.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonStreamCommands.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonSubscription.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ScoredSortedListReplayDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ScoredSortedSetReplayDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SecondsConvertor.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SetReplayDecoder.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/BaseTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/ClusterRunner.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedisRunner.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedisVersion.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedissonRuntimeEnvironment.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/BaseConnectionTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonClusterConnectionRenameTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonClusterConnectionTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonConnectionTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonMultiConnectionTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonPipelineConnectionTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonReactiveClusterKeyCommandsTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonScriptReactiveTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSentinelConnectionTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonStreamTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSubscribeReactiveTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSubscribeTest.java create mode 100644 redisson-spring-data/redisson-spring-data-30/src/test/resources/logback.xml diff --git a/redisson-spring-data/pom.xml b/redisson-spring-data/pom.xml index 3a8f37f37..0bd22f3e9 100644 --- a/redisson-spring-data/pom.xml +++ b/redisson-spring-data/pom.xml @@ -25,6 +25,7 @@ redisson-spring-data-25 redisson-spring-data-26 redisson-spring-data-27 + redisson-spring-data-30 diff --git a/redisson-spring-data/redisson-spring-data-30/pom.xml b/redisson-spring-data/redisson-spring-data-30/pom.xml new file mode 100644 index 000000000..ee3318582 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/pom.xml @@ -0,0 +1,76 @@ + + 4.0.0 + + + org.redisson + redisson-spring-data + 3.18.1-SNAPSHOT + ../ + + + redisson-spring-data-30 + jar + + Redisson/Spring Data Redis v3.0.x integration + + + + org.springframework.data + spring-data-redis + 3.0.0 + + + + + + + maven-jar-plugin + 3.1.2 + + + + ${maven.build.timestamp} + redisson.spring.data27 + + + + + + + com.mycila + license-maven-plugin + 3.0 + + ${basedir} +
${basedir}/../../header.txt
+ false + true + false + + src/main/java/org/redisson/ + + + target/** + + true + + JAVADOC_STYLE + + true + true + UTF-8 +
+ + + + check + + + +
+ +
+
+ + +
diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/BinaryConvertor.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/BinaryConvertor.java new file mode 100644 index 000000000..8561b2874 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/BinaryConvertor.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.protocol.convertor.Convertor; + +import io.netty.util.CharsetUtil; + +/** + * + * @author Nikita Koksharov + * + */ +public class BinaryConvertor implements Convertor { + + @Override + public Object convert(Object obj) { + if (obj instanceof String) { + return ((String) obj).getBytes(CharsetUtil.UTF_8); + } + return obj; + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ByteBufferGeoResultsDecoder.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ByteBufferGeoResultsDecoder.java new file mode 100644 index 000000000..a6c8707d8 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ByteBufferGeoResultsDecoder.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.handler.State; +import org.redisson.client.protocol.decoder.MultiDecoder; +import org.springframework.data.geo.*; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author Nikita Koksharov + * + */ +public class ByteBufferGeoResultsDecoder implements MultiDecoder>> { + + private final Metric metric; + + public ByteBufferGeoResultsDecoder() { + this(null); + } + + public ByteBufferGeoResultsDecoder(Metric metric) { + super(); + this.metric = metric; + } + + @Override + public GeoResults> decode(List parts, State state) { + List>> result = new ArrayList>>(); + for (Object object : parts) { + if (object instanceof List) { + List vals = ((List) object); + + if (metric != null) { + GeoLocation location = new GeoLocation(ByteBuffer.wrap((byte[])vals.get(0)), null); + result.add(new GeoResult>(location, new Distance((Double)vals.get(1), metric))); + } else { + GeoLocation location = new GeoLocation(ByteBuffer.wrap((byte[])vals.get(0)), (Point)vals.get(1)); + result.add(new GeoResult>(location, null)); + } + } else { + GeoLocation location = new GeoLocation(ByteBuffer.wrap((byte[])object), null); + result.add(new GeoResult>(location, new Distance(0))); + } + } + return new GeoResults>(result); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/DataTypeConvertor.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/DataTypeConvertor.java new file mode 100644 index 000000000..8559fbfbe --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/DataTypeConvertor.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.protocol.convertor.Convertor; +import org.springframework.data.redis.connection.DataType; + +/** + * + * @author Nikita Koksharov + * + */ +public class DataTypeConvertor implements Convertor { + + @Override + public DataType convert(Object obj) { + String val = obj.toString(); + return DataType.fromCode(val); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/DistanceConvertor.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/DistanceConvertor.java new file mode 100644 index 000000000..0db558d63 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/DistanceConvertor.java @@ -0,0 +1,41 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.protocol.convertor.Convertor; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metric; + +/** + * + * @author Nikita Koksharov + * + */ +public class DistanceConvertor implements Convertor { + + private final Metric metric; + + public DistanceConvertor(Metric metric) { + super(); + this.metric = metric; + } + + @Override + public Distance convert(Object obj) { + return new Distance((Double)obj, metric); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/GeoResultsDecoder.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/GeoResultsDecoder.java new file mode 100644 index 000000000..16392a548 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/GeoResultsDecoder.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.handler.State; +import org.redisson.client.protocol.decoder.MultiDecoder; +import org.springframework.data.geo.*; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; + +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author Nikita Koksharov + * + */ +public class GeoResultsDecoder implements MultiDecoder>> { + + private final Metric metric; + + public GeoResultsDecoder() { + this(null); + } + + public GeoResultsDecoder(Metric metric) { + super(); + this.metric = metric; + } + + @Override + public GeoResults> decode(List parts, State state) { + List>> result = new ArrayList>>(); + for (Object object : parts) { + if (object instanceof List) { + List vals = ((List) object); + + if (metric != null) { + GeoLocation location = new GeoLocation((byte[])vals.get(0), null); + result.add(new GeoResult>(location, new Distance((Double)vals.get(1), metric))); + } else { + GeoLocation location = new GeoLocation((byte[])vals.get(0), (Point)vals.get(1)); + result.add(new GeoResult>(location, new Distance(0))); + } + } else { + GeoLocation location = new GeoLocation((byte[])object, null); + result.add(new GeoResult>(location, new Distance(0))); + } + } + return new GeoResults>(result); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ObjectListReplayDecoder2.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ObjectListReplayDecoder2.java new file mode 100644 index 000000000..51ab8c3aa --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ObjectListReplayDecoder2.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.util.List; + +import org.redisson.client.codec.Codec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; + +/** + * + * @author Nikita Koksharov + * + * @param type + */ +public class ObjectListReplayDecoder2 implements MultiDecoder> { + + private final Decoder decoder; + + public ObjectListReplayDecoder2() { + this(null); + } + + public ObjectListReplayDecoder2(Decoder decoder) { + super(); + this.decoder = decoder; + } + + @Override + public List decode(List parts, State state) { + for (int i = 0; i < parts.size(); i++) { + Object object = parts.get(i); + if (object instanceof List) { + if (((List) object).isEmpty()) { + parts.set(i, null); + } + } + } + return (List) parts; + } + + @Override + public Decoder getDecoder(Codec codec, int paramNum, State state) { + return decoder; + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PointDecoder.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PointDecoder.java new file mode 100644 index 000000000..527c8e782 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PointDecoder.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.util.List; + +import org.redisson.client.codec.Codec; +import org.redisson.client.codec.DoubleCodec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; +import org.springframework.data.geo.Point; + +/** + * + * @author Nikita Koksharov + * + */ +public class PointDecoder implements MultiDecoder { + + @Override + public Decoder getDecoder(Codec codec, int paramNum, State state) { + return DoubleCodec.INSTANCE.getValueDecoder(); + } + + @Override + public Point decode(List parts, State state) { + if (parts.isEmpty()) { + return null; + } + + Double longitude = (Double)parts.get(0); + Double latitude = (Double)parts.get(1); + return new Point(longitude, latitude); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PropertiesDecoder.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PropertiesDecoder.java new file mode 100644 index 000000000..a68dbe519 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PropertiesDecoder.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.util.Properties; + +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; + +import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; + +/** + * + * @author Nikita Koksharov + * + */ +public class PropertiesDecoder implements Decoder { + + @Override + public Properties decode(ByteBuf buf, State state) { + String value = buf.toString(CharsetUtil.UTF_8); + Properties result = new Properties(); + for (String entry : value.split("\r\n|\n")) { + String[] parts = entry.split(":"); + if (parts.length == 2) { + result.put(parts[0], parts[1]); + } + } + return result; + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PropertiesListDecoder.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PropertiesListDecoder.java new file mode 100644 index 000000000..d3ce5ddd5 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/PropertiesListDecoder.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.util.List; +import java.util.Properties; + +import org.redisson.client.codec.Codec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; + +/** + * + * @author Nikita Koksharov + * + */ +public class PropertiesListDecoder implements MultiDecoder { + + @Override + public Properties decode(List parts, State state) { + Properties result = new Properties(); + for (int j = 0; j < parts.size(); j+=2) { + Object key = parts.get(j); + Object value = parts.get(j+1); + result.put(key, value); + } + return result; + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedisClusterNodeDecoder.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedisClusterNodeDecoder.java new file mode 100644 index 000000000..daa9ee31c --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedisClusterNodeDecoder.java @@ -0,0 +1,120 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.misc.RedisURI; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisClusterNode.Flag; +import org.springframework.data.redis.connection.RedisClusterNode.LinkState; +import org.springframework.data.redis.connection.RedisClusterNode.RedisClusterNodeBuilder; +import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; +import org.springframework.data.redis.connection.RedisNode.NodeType; + +import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedisClusterNodeDecoder implements Decoder> { + + @Override + public List decode(ByteBuf buf, State state) throws IOException { + String response = buf.toString(CharsetUtil.UTF_8); + + List nodes = new ArrayList(); + for (String nodeInfo : response.split("\n")) { + String[] params = nodeInfo.split(" "); + + String nodeId = params[0]; + + String flagsStr = params[2]; + Set flags = EnumSet.noneOf(Flag.class); + for (String flag : flagsStr.split(",")) { + String flagValue = flag.replace("slave", "replica") + .toUpperCase().replaceAll("\\?", ""); + flags.add(Flag.valueOf(flagValue)); + } + + RedisURI address = null; + if (!flags.contains(Flag.NOADDR)) { + String addr = params[1].split("@")[0]; + address = new RedisURI("redis://" + addr); + } + + String masterId = params[3]; + if ("-".equals(masterId)) { + masterId = null; + } + + Set slotsCollection = new HashSet(); + LinkState linkState = null; + if (params.length >= 8 && params[7] != null) { + linkState = LinkState.valueOf(params[7].toUpperCase()); + } + if (params.length > 8) { + for (int i = 0; i < params.length - 8; i++) { + String slots = params[i + 8]; + if (slots.indexOf("-<-") != -1 || slots.indexOf("->-") != -1) { + continue; + } + + String[] parts = slots.split("-"); + if(parts.length == 1) { + slotsCollection.add(Integer.valueOf(parts[0])); + } else if(parts.length == 2) { + for (int j = Integer.valueOf(parts[0]); j < Integer.valueOf(parts[1]) + 1; j++) { + slotsCollection.add(j); + } + } + } + } + + NodeType type = null; + if (flags.contains(Flag.MASTER)) { + type = NodeType.MASTER; + } else if (flags.contains(Flag.REPLICA)) { + type = NodeType.REPLICA; + } + + RedisClusterNodeBuilder builder = RedisClusterNode.newRedisClusterNode() + .linkState(linkState) + .replicaOf(masterId) + .serving(new SlotRange(slotsCollection)) + .withId(nodeId) + .promotedAs(type) + .withFlags(flags); + + if (address != null) { + builder.listeningAt(address.getHost(), address.getPort()); + } + nodes.add(builder.build()); + } + return nodes; + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonBaseCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonBaseCommands.java new file mode 100644 index 000000000..48046c102 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonBaseCommands.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.api.RFuture; +import org.redisson.client.codec.Codec; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.command.CommandAsyncService; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonBaseCommands { + + private RedissonConnection connection; + + public RedissonBaseCommands(RedissonConnection connection) { + this.connection = connection; + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonBaseReactive.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonBaseReactive.java new file mode 100644 index 000000000..85eb51a5b --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonBaseReactive.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.redisson.api.RFuture; +import org.redisson.client.codec.Codec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.connection.MasterSlaveEntry; +import org.redisson.misc.CompletableFutureWrapper; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.connection.RedisClusterNode; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +abstract class RedissonBaseReactive { + + final CommandReactiveExecutor executorService; + + RedissonBaseReactive(CommandReactiveExecutor executorService) { + this.executorService = executorService; + } + + public static byte[] toByteArray(ByteBuffer buffer) { + byte[] dst = new byte[buffer.remaining()]; + int pos = buffer.position(); + buffer.get(dst); + buffer.position(pos); + return dst; + } + + RFuture toStringFuture(RFuture f) { + CompletionStage ff = f.thenApply(r -> "OK"); + return new CompletableFutureWrapper<>(ff); + } + + Mono execute(RedisClusterNode node, RedisCommand command, Object... params) { + MasterSlaveEntry entry = getEntry(node); + return executorService.reactive(() -> { + return executorService.writeAsync(entry, StringCodec.INSTANCE, command, params); + }); + } + + MasterSlaveEntry getEntry(RedisClusterNode node) { + MasterSlaveEntry entry = executorService.getConnectionManager().getEntry(new InetSocketAddress(node.getHost(), node.getPort())); + return entry; + } + + Flux execute(Publisher commands, Function> mapper) { + Flux s = Flux.from(commands); + return s.concatMap(mapper); + } + + Mono write(byte[] key, Codec codec, RedisCommand command, Object... params) { + Mono f = executorService.reactive(() -> { + return executorService.writeAsync(key, codec, command, params); + }); + return f.onErrorMap(e -> new RedisSystemException(e.getMessage(), e)); + } + + Mono read(byte[] key, Codec codec, RedisCommand command, Object... params) { + Mono f = executorService.reactive(() -> { + return executorService.readAsync(key, codec, command, params); + }); + return f.onErrorMap(e -> new RedisSystemException(e.getMessage(), e)); + } + + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonClusterConnection.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonClusterConnection.java new file mode 100644 index 000000000..fdbec5bf2 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonClusterConnection.java @@ -0,0 +1,558 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import io.netty.util.CharsetUtil; +import org.redisson.api.BatchResult; +import org.redisson.api.RFuture; +import org.redisson.api.RedissonClient; +import org.redisson.client.RedisClient; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.LongCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.client.protocol.decoder.ListScanResult; +import org.redisson.client.protocol.decoder.ObjectDecoder; +import org.redisson.client.protocol.decoder.ObjectListReplayDecoder; +import org.redisson.client.protocol.decoder.StringMapDataDecoder; +import org.redisson.command.CommandBatchService; +import org.redisson.connection.MasterSlaveEntry; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.redis.connection.*; +import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; +import org.springframework.data.redis.connection.convert.Converters; +import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanCursor; +import org.springframework.data.redis.core.ScanIteration; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.util.Assert; + +import java.net.InetSocketAddress; +import java.util.*; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonClusterConnection extends RedissonConnection implements RedisClusterConnection, DefaultedRedisClusterConnection { + + private static final RedisStrictCommand> CLUSTER_NODES = + new RedisStrictCommand>("CLUSTER", "NODES", new ObjectDecoder(new RedisClusterNodeDecoder())); + + public RedissonClusterConnection(RedissonClient redisson) { + super(redisson); + } + + @Override + public Iterable clusterGetNodes() { + return read(null, StringCodec.INSTANCE, CLUSTER_NODES); + } + + @Override + public Collection clusterGetReplicas(RedisClusterNode master) { + Iterable res = clusterGetNodes(); + RedisClusterNode masterNode = null; + for (Iterator iterator = res.iterator(); iterator.hasNext();) { + RedisClusterNode redisClusterNode = iterator.next(); + if (master.getHost().equals(redisClusterNode.getHost()) + && master.getPort().equals(redisClusterNode.getPort())) { + masterNode = redisClusterNode; + break; + } + } + + if (masterNode == null) { + throw new IllegalStateException("Unable to find master node: " + master); + } + + for (Iterator iterator = res.iterator(); iterator.hasNext();) { + RedisClusterNode redisClusterNode = iterator.next(); + if (redisClusterNode.getMasterId() == null + || !redisClusterNode.getMasterId().equals(masterNode.getId())) { + iterator.remove(); + } + } + return (Collection) res; + } + + @Override + public Map> clusterGetMasterReplicaMap() { + Iterable res = clusterGetNodes(); + + Set masters = new HashSet(); + for (Iterator iterator = res.iterator(); iterator.hasNext();) { + RedisClusterNode redisClusterNode = iterator.next(); + if (redisClusterNode.isMaster()) { + masters.add(redisClusterNode); + } + } + + Map> result = new HashMap>(); + for (Iterator iterator = res.iterator(); iterator.hasNext();) { + RedisClusterNode redisClusterNode = iterator.next(); + + for (RedisClusterNode masterNode : masters) { + if (redisClusterNode.getMasterId() != null + && redisClusterNode.getMasterId().equals(masterNode.getId())) { + Collection list = result.get(masterNode); + if (list == null) { + list = new ArrayList(); + result.put(masterNode, list); + } + list.add(redisClusterNode); + } + } + } + return result; + } + + @Override + public Integer clusterGetSlotForKey(byte[] key) { + RFuture f = executorService.readAsync((String)null, StringCodec.INSTANCE, RedisCommands.KEYSLOT, key); + return syncFuture(f); + } + + @Override + public RedisClusterNode clusterGetNodeForSlot(int slot) { + Iterable res = clusterGetNodes(); + for (RedisClusterNode redisClusterNode : res) { + if (redisClusterNode.isMaster() && redisClusterNode.getSlotRange().contains(slot)) { + return redisClusterNode; + } + } + return null; + } + + @Override + public RedisClusterNode clusterGetNodeForKey(byte[] key) { + int slot = executorService.getConnectionManager().calcSlot(key); + return clusterGetNodeForSlot(slot); + } + + @Override + public ClusterInfo clusterGetClusterInfo() { + RFuture> f = executorService.readAsync((String)null, StringCodec.INSTANCE, RedisCommands.CLUSTER_INFO); + Map entries = syncFuture(f); + + Properties props = new Properties(); + for (Entry entry : entries.entrySet()) { + props.setProperty(entry.getKey(), entry.getValue()); + } + return new ClusterInfo(props); + } + + @Override + public void clusterAddSlots(RedisClusterNode node, int... slots) { + MasterSlaveEntry entry = getEntry(node); + List params = convert(slots); + RFuture> f = executorService.writeAsync(entry, StringCodec.INSTANCE, RedisCommands.CLUSTER_ADDSLOTS, params.toArray()); + syncFuture(f); + } + + protected List convert(int... slots) { + List params = new ArrayList(); + for (int slot : slots) { + params.add(slot); + } + return params; + } + + @Override + public void clusterAddSlots(RedisClusterNode node, SlotRange range) { + clusterAddSlots(node, range.getSlotsArray()); + } + + @Override + public Long clusterCountKeysInSlot(int slot) { + RedisClusterNode node = clusterGetNodeForSlot(slot); + MasterSlaveEntry entry = executorService.getConnectionManager().getEntry(new InetSocketAddress(node.getHost(), node.getPort())); + RFuture f = executorService.readAsync(entry, StringCodec.INSTANCE, RedisCommands.CLUSTER_COUNTKEYSINSLOT, slot); + return syncFuture(f); + } + + @Override + public void clusterDeleteSlots(RedisClusterNode node, int... slots) { + MasterSlaveEntry entry = getEntry(node); + List params = convert(slots); + RFuture f = executorService.writeAsync(entry, StringCodec.INSTANCE, RedisCommands.CLUSTER_DELSLOTS, params.toArray()); + syncFuture(f); + } + + @Override + public void clusterDeleteSlotsInRange(RedisClusterNode node, SlotRange range) { + clusterDeleteSlots(node, range.getSlotsArray()); + } + + @Override + public void clusterForget(RedisClusterNode node) { + RFuture f = executorService.writeAsync((String)null, StringCodec.INSTANCE, RedisCommands.CLUSTER_FORGET, node.getId()); + syncFuture(f); + } + + @Override + public void clusterMeet(RedisClusterNode node) { + Assert.notNull(node, "Cluster node must not be null for CLUSTER MEET command!"); + Assert.hasText(node.getHost(), "Node to meet cluster must have a host!"); + Assert.isTrue(node.getPort() > 0, "Node to meet cluster must have a port greater 0!"); + + RFuture f = executorService.writeAsync((String)null, StringCodec.INSTANCE, RedisCommands.CLUSTER_MEET, node.getHost(), node.getPort()); + syncFuture(f); + } + + @Override + public void clusterSetSlot(RedisClusterNode node, int slot, AddSlots mode) { + MasterSlaveEntry entry = getEntry(node); + RFuture> f = executorService.writeAsync(entry, StringCodec.INSTANCE, RedisCommands.CLUSTER_SETSLOT, slot, mode); + syncFuture(f); + } + + private static final RedisStrictCommand> CLUSTER_GETKEYSINSLOT = new RedisStrictCommand>("CLUSTER", "GETKEYSINSLOT", new ObjectListReplayDecoder()); + + @Override + public List clusterGetKeysInSlot(int slot, Integer count) { + RFuture> f = executorService.readAsync((String)null, ByteArrayCodec.INSTANCE, CLUSTER_GETKEYSINSLOT, slot, count); + return syncFuture(f); + } + + @Override + public void clusterReplicate(RedisClusterNode master, RedisClusterNode slave) { + MasterSlaveEntry entry = getEntry(master); + RFuture f = executorService.writeAsync(entry, StringCodec.INSTANCE, RedisCommands.CLUSTER_REPLICATE, slave.getId()); + syncFuture(f); + } + +// @Override +// public String ping(RedisClusterNode node) { +// return execute(node, RedisCommands.PING); +// } + + @Override + public void bgReWriteAof(RedisClusterNode node) { + execute(node, RedisCommands.BGREWRITEAOF); + } + + @Override + public void bgSave(RedisClusterNode node) { + execute(node, RedisCommands.BGSAVE); + } + + @Override + public Long lastSave(RedisClusterNode node) { + return execute(node, RedisCommands.LASTSAVE); + } + + @Override + public void save(RedisClusterNode node) { + execute(node, RedisCommands.SAVE); + } + + @Override + public Long dbSize(RedisClusterNode node) { + return execute(node, RedisCommands.DBSIZE); + } + + private T execute(RedisClusterNode node, RedisCommand command) { + MasterSlaveEntry entry = getEntry(node); + RFuture f = executorService.writeAsync(entry, StringCodec.INSTANCE, command); + return syncFuture(f); + } + + protected MasterSlaveEntry getEntry(RedisClusterNode node) { + MasterSlaveEntry entry = executorService.getConnectionManager().getEntry(new InetSocketAddress(node.getHost(), node.getPort())); + return entry; + } + + @Override + public void flushDb(RedisClusterNode node) { + execute(node, RedisCommands.FLUSHDB); + } + + @Override + public void flushAll(RedisClusterNode node) { + execute(node, RedisCommands.FLUSHALL); + } + + @Override + public Properties info(RedisClusterNode node) { + Map info = execute(node, RedisCommands.INFO_ALL); + Properties result = new Properties(); + for (Entry entry : info.entrySet()) { + result.setProperty(entry.getKey(), entry.getValue()); + } + return result; + } + + @Override + public Properties info(RedisClusterNode node, String section) { + RedisStrictCommand> command = new RedisStrictCommand>("INFO", section, new StringMapDataDecoder()); + + Map info = execute(node, command); + Properties result = new Properties(); + for (Entry entry : info.entrySet()) { + result.setProperty(entry.getKey(), entry.getValue()); + } + return result; + } + + private final RedisStrictCommand> KEYS = new RedisStrictCommand<>("KEYS"); + + @Override + public Set keys(RedisClusterNode node, byte[] pattern) { + MasterSlaveEntry entry = getEntry(node); + RFuture> f = executorService.readAsync(entry, ByteArrayCodec.INSTANCE, KEYS, pattern); + Collection keys = syncFuture(f); + return new HashSet<>(keys); + } + + @Override + public byte[] randomKey(RedisClusterNode node) { + MasterSlaveEntry entry = getEntry(node); + RFuture f = executorService.readRandomAsync(entry, ByteArrayCodec.INSTANCE, RedisCommands.RANDOM_KEY); + return syncFuture(f); + } + + @Override + public void shutdown(RedisClusterNode node) { + MasterSlaveEntry entry = getEntry(node); + RFuture f = executorService.readAsync(entry, ByteArrayCodec.INSTANCE, RedisCommands.SHUTDOWN); + syncFuture(f); + } + + @Override + public Properties getConfig(RedisClusterNode node, String pattern) { + MasterSlaveEntry entry = getEntry(node); + RFuture> f = executorService.writeAsync(entry, StringCodec.INSTANCE, RedisCommands.CONFIG_GET, pattern); + List r = syncFuture(f); + if (r != null) { + return Converters.toProperties(r); + } + return null; + } + + @Override + public void setConfig(RedisClusterNode node, String param, String value) { + MasterSlaveEntry entry = getEntry(node); + RFuture f = executorService.writeAsync(entry, StringCodec.INSTANCE, RedisCommands.CONFIG_SET, param, value); + syncFuture(f); + } + + @Override + public void resetConfigStats(RedisClusterNode node) { + MasterSlaveEntry entry = getEntry(node); + RFuture f = executorService.writeAsync(entry, StringCodec.INSTANCE, RedisCommands.CONFIG_RESETSTAT); + syncFuture(f); + } + + @Override + public Long time(RedisClusterNode node) { + MasterSlaveEntry entry = getEntry(node); + RFuture f = executorService.readAsync(entry, LongCodec.INSTANCE, RedisCommands.TIME_LONG); + return syncFuture(f); + } + + private static final StringToRedisClientInfoConverter CONVERTER = new StringToRedisClientInfoConverter(); + + @Override + public List getClientList(RedisClusterNode node) { + MasterSlaveEntry entry = getEntry(node); + RFuture> f = executorService.readAsync(entry, StringCodec.INSTANCE, RedisCommands.CLIENT_LIST); + List list = syncFuture(f); + return CONVERTER.convert(list.toArray(new String[list.size()])); + } + + @Override + public Cursor scan(RedisClusterNode node, ScanOptions options) { + return new ScanCursor(0, options) { + + private RedisClient client; + private MasterSlaveEntry entry = getEntry(node); + + @Override + protected ScanIteration doScan(long cursorId, ScanOptions options) { + if (isQueueing() || isPipelined()) { + throw new UnsupportedOperationException("'SSCAN' cannot be called in pipeline / transaction mode."); + } + + if (entry == null) { + return null; + } + + List args = new ArrayList(); + // to avoid negative value + cursorId = Math.max(cursorId, 0); + args.add(cursorId); + if (options.getPattern() != null) { + args.add("MATCH"); + args.add(options.getPattern()); + } + if (options.getCount() != null) { + args.add("COUNT"); + args.add(options.getCount()); + } + + RFuture> f = executorService.readAsync(client, entry, ByteArrayCodec.INSTANCE, RedisCommands.SCAN, args.toArray()); + ListScanResult res = syncFuture(f); + long pos = res.getPos(); + client = res.getRedisClient(); + if (pos == 0) { + entry = null; + } + + return new ScanIteration(pos, res.getValues()); + } + }.open(); + } + + @Override + public void rename(byte[] oldName, byte[] newName) { + + if (isPipelined()) { + throw new InvalidDataAccessResourceUsageException("Clustered rename is not supported in a pipeline"); + } + + if (redisson.getConnectionManager().calcSlot(oldName) == redisson.getConnectionManager().calcSlot(newName)) { + super.rename(oldName, newName); + return; + } + + final byte[] value = dump(oldName); + + if (null != value) { + + final Long sourceTtlInSeconds = ttl(oldName); + + final long ttlInMilliseconds; + if (null != sourceTtlInSeconds && sourceTtlInSeconds > 0) { + ttlInMilliseconds = sourceTtlInSeconds * 1000; + } else { + ttlInMilliseconds = 0; + } + + restore(newName, ttlInMilliseconds, value); + del(oldName); + } + } + + @Override + public Boolean renameNX(byte[] oldName, byte[] newName) { + if (isPipelined()) { + throw new InvalidDataAccessResourceUsageException("Clustered rename is not supported in a pipeline"); + } + + if (redisson.getConnectionManager().calcSlot(oldName) == redisson.getConnectionManager().calcSlot(newName)) { + return super.renameNX(oldName, newName); + } + + final byte[] value = dump(oldName); + + if (null != value && !exists(newName)) { + + final Long sourceTtlInSeconds = ttl(oldName); + + final long ttlInMilliseconds; + if (null != sourceTtlInSeconds && sourceTtlInSeconds > 0) { + ttlInMilliseconds = sourceTtlInSeconds * 1000; + } else { + ttlInMilliseconds = 0; + } + + restore(newName, ttlInMilliseconds, value); + del(oldName); + + return true; + } + + return false; + } + + @Override + public Long del(byte[]... keys) { + if (isQueueing() || isPipelined()) { + for (byte[] key: keys) { + write(key, LongCodec.INSTANCE, RedisCommands.DEL, key); + } + + return null; + } + + CommandBatchService es = new CommandBatchService(executorService); + for (byte[] key: keys) { + es.writeAsync(key, StringCodec.INSTANCE, RedisCommands.DEL, key); + } + BatchResult b = (BatchResult) es.execute(); + return b.getResponses().stream().collect(Collectors.summarizingLong(v -> v)).getSum(); + } + + @Override + public List mGet(byte[]... keys) { + if (isQueueing() || isPipelined()) { + for (byte[] key : keys) { + read(key, ByteArrayCodec.INSTANCE, RedisCommands.GET, key); + } + return null; + } + + CommandBatchService es = new CommandBatchService(executorService); + for (byte[] key: keys) { + es.readAsync(key, ByteArrayCodec.INSTANCE, RedisCommands.GET, key); + } + BatchResult r = (BatchResult) es.execute(); + return r.getResponses(); + } + + @Override + public Boolean mSet(Map tuple) { + if (isQueueing() || isPipelined()) { + for (Entry entry: tuple.entrySet()) { + write(entry.getKey(), StringCodec.INSTANCE, RedisCommands.SET, entry.getKey(), entry.getValue()); + } + return true; + } + + CommandBatchService es = new CommandBatchService(executorService); + for (Entry entry: tuple.entrySet()) { + es.writeAsync(entry.getKey(), StringCodec.INSTANCE, RedisCommands.SET, entry.getKey(), entry.getValue()); + } + es.execute(); + return true; + } + + @Override + public RedisClusterCommands clusterCommands() { + return this; + } + + @Override + public RedisClusterServerCommands serverCommands() { + return this; + } + + @Override + public String ping(RedisClusterNode node) { + MasterSlaveEntry entry = getEntry(node); + RFuture f = executorService.readAsync(entry, LongCodec.INSTANCE, RedisCommands.PING); + return syncFuture(f); + } + + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java new file mode 100644 index 000000000..5bf562141 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnection.java @@ -0,0 +1,2407 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import io.netty.buffer.ByteBuf; +import org.redisson.Redisson; +import org.redisson.api.BatchOptions; +import org.redisson.api.BatchOptions.ExecutionMode; +import org.redisson.api.BatchResult; +import org.redisson.api.RFuture; +import org.redisson.api.RedissonClient; +import org.redisson.client.RedisClient; +import org.redisson.client.codec.*; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.client.protocol.convertor.*; +import org.redisson.client.protocol.decoder.*; +import org.redisson.command.BatchPromise; +import org.redisson.command.CommandAsyncService; +import org.redisson.command.CommandBatchService; +import org.redisson.connection.MasterSlaveEntry; +import org.redisson.misc.CompletableFutureWrapper; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.geo.*; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.connection.*; +import org.springframework.data.redis.connection.zset.Aggregate; +import org.springframework.data.redis.connection.zset.Tuple; +import org.springframework.data.redis.connection.zset.Weights; +import org.springframework.data.redis.core.*; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.data.redis.domain.geo.BoxShape; +import org.springframework.data.redis.domain.geo.GeoReference; +import org.springframework.data.redis.domain.geo.GeoShape; +import org.springframework.data.redis.domain.geo.RadiusShape; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.data.domain.Range; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.redisson.client.protocol.RedisCommands.LRANGE; + +/** + * Redisson connection + * + * @author Nikita Koksharov + * + */ +public class RedissonConnection extends AbstractRedisConnection { + + private boolean closed; + protected final Redisson redisson; + + CommandAsyncService executorService; + private RedissonSubscription subscription; + + public RedissonConnection(RedissonClient redisson) { + super(); + this.redisson = (Redisson) redisson; + executorService = (CommandAsyncService) this.redisson.getCommandExecutor(); + } + + @Override + public void close() throws DataAccessException { + super.close(); + + if (isQueueing()) { + CommandBatchService es = (CommandBatchService) executorService; + if (!es.isExecuted()) { + discard(); + } + } + closed = true; + } + + @Override + public boolean isClosed() { + return closed; + } + + @Override + public Object getNativeConnection() { + return redisson; + } + + @Override + public boolean isQueueing() { + if (executorService instanceof CommandBatchService) { + CommandBatchService es = (CommandBatchService) executorService; + return es.getOptions().getExecutionMode() == ExecutionMode.REDIS_WRITE_ATOMIC; + } + return false; + } + + @Override + public boolean isPipelined() { + if (executorService instanceof CommandBatchService) { + CommandBatchService es = (CommandBatchService) executorService; + return es.getOptions().getExecutionMode() == ExecutionMode.IN_MEMORY || es.getOptions().getExecutionMode() == ExecutionMode.IN_MEMORY_ATOMIC; + } + return false; + } + + public boolean isPipelinedAtomic() { + if (executorService instanceof CommandBatchService) { + CommandBatchService es = (CommandBatchService) executorService; + return es.getOptions().getExecutionMode() == ExecutionMode.IN_MEMORY_ATOMIC; + } + return false; + } + + @Override + public void openPipeline() { + BatchOptions options = BatchOptions.defaults() + .executionMode(ExecutionMode.IN_MEMORY); + this.executorService = new CommandBatchService(executorService, options); + } + + @Override + public List closePipeline() throws RedisPipelineException { + if (isPipelined()) { + CommandBatchService es = (CommandBatchService) executorService; + try { + BatchResult result = es.execute(); + filterResults(result); + if (isPipelinedAtomic()) { + return Arrays.asList((List) result.getResponses()); + } + return (List) result.getResponses(); + } catch (Exception ex) { + throw new RedisPipelineException(ex); + } finally { + resetConnection(); + } + } + return Collections.emptyList(); + } + + @Override + public Object execute(String command, byte[]... args) { + for (Method method : this.getClass().getDeclaredMethods()) { + if (method.getName().equalsIgnoreCase(command) + && Modifier.isPublic(method.getModifiers()) + && (method.getParameterTypes().length == args.length)) { + try { + Object t = execute(method, args); + if (t instanceof String) { + return ((String) t).getBytes(); + } + return t; + } catch (IllegalArgumentException e) { + if (isPipelined()) { + throw new RedisPipelineException(e); + } + + throw new InvalidDataAccessApiUsageException(e.getMessage(), e); + } + } + } + throw new UnsupportedOperationException(); + } + + private Object execute(Method method, byte[]... args) { + if (method.getParameterTypes().length > 0 && method.getParameterTypes()[0] == byte[][].class) { + return ReflectionUtils.invokeMethod(method, this, args); + } + if (args == null) { + return ReflectionUtils.invokeMethod(method, this); + } + return ReflectionUtils.invokeMethod(method, this, Arrays.asList(args).toArray()); + } + + V syncFuture(RFuture future) { + try { + return executorService.get(future); + } catch (Exception ex) { + throw transform(ex); + } + } + + protected RuntimeException transform(Exception ex) { + DataAccessException exception = RedissonConnectionFactory.EXCEPTION_TRANSLATION.translate(ex); + if (exception != null) { + return exception; + } + return new RedisSystemException(ex.getMessage(), ex); + } + + @Override + public Boolean exists(byte[] key) { + return read(key, StringCodec.INSTANCE, RedisCommands.EXISTS, key); + } + + @Override + public Long del(byte[]... keys) { + return write(keys[0], LongCodec.INSTANCE, RedisCommands.DEL, Arrays.asList(keys).toArray()); + } + + @Override + public Long unlink(byte[]... keys) { + return write(keys[0], LongCodec.INSTANCE, RedisCommands.UNLINK, Arrays.asList(keys).toArray()); + } + + private static final RedisStrictCommand TYPE = new RedisStrictCommand("TYPE", new DataTypeConvertor()); + + @Override + public DataType type(byte[] key) { + return read(key, StringCodec.INSTANCE, TYPE, key); + } + + private static final RedisStrictCommand> KEYS = new RedisStrictCommand>("KEYS", new ObjectSetReplayDecoder()); + + @Override + public Set keys(byte[] pattern) { + if (isQueueing()) { + return read(null, ByteArrayCodec.INSTANCE, KEYS, pattern); + } + + List>> futures = executorService.readAllAsync(ByteArrayCodec.INSTANCE, KEYS, pattern); + CompletableFuture ff = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + CompletableFuture> future = ff.thenApply(r -> { + return futures.stream().flatMap(f -> f.getNow(new HashSet<>()).stream()).collect(Collectors.toSet()); + }).toCompletableFuture(); + return sync(new CompletableFutureWrapper<>(future)); + } + + @Override + public Cursor scan(ScanOptions options) { + return new ScanCursor(0, options) { + + private RedisClient client; + private Iterator entries = redisson.getConnectionManager().getEntrySet().iterator(); + private MasterSlaveEntry entry = entries.next(); + + @Override + protected ScanIteration doScan(long cursorId, ScanOptions options) { + if (isQueueing() || isPipelined()) { + throw new UnsupportedOperationException("'SSCAN' cannot be called in pipeline / transaction mode."); + } + + if (entry == null) { + return null; + } + + List args = new ArrayList(); + // to avoid negative value + cursorId = Math.max(cursorId, 0); + args.add(cursorId); + if (options.getPattern() != null) { + args.add("MATCH"); + args.add(options.getPattern()); + } + if (options.getCount() != null) { + args.add("COUNT"); + args.add(options.getCount()); + } + + RFuture> f = executorService.readAsync(client, entry, ByteArrayCodec.INSTANCE, RedisCommands.SCAN, args.toArray()); + ListScanResult res = syncFuture(f); + long pos = res.getPos(); + client = res.getRedisClient(); + if (pos == 0) { + if (entries.hasNext()) { + pos = -1; + entry = entries.next(); + client = null; + } else { + entry = null; + } + } + + return new ScanIteration(pos, res.getValues()); + } + }.open(); + } + + @Override + public byte[] randomKey() { + if (isQueueing()) { + return read(null, ByteArrayCodec.INSTANCE, RedisCommands.RANDOM_KEY); + } + + RFuture f = executorService.readRandomAsync(ByteArrayCodec.INSTANCE, RedisCommands.RANDOM_KEY); + return sync(f); + } + + @Override + public void rename(byte[] oldName, byte[] newName) { + write(oldName, StringCodec.INSTANCE, RedisCommands.RENAME, oldName, newName); + } + + @Override + public Boolean renameNX(byte[] oldName, byte[] newName) { + return write(oldName, StringCodec.INSTANCE, RedisCommands.RENAMENX, oldName, newName); + } + + private static final RedisStrictCommand EXPIRE = new RedisStrictCommand("EXPIRE", new BooleanReplayConvertor()); + + @Override + public Boolean expire(byte[] key, long seconds) { + return write(key, StringCodec.INSTANCE, EXPIRE, key, seconds); + } + + @Override + public Boolean pExpire(byte[] key, long millis) { + return write(key, StringCodec.INSTANCE, RedisCommands.PEXPIRE, key, millis); + } + + private static final RedisStrictCommand EXPIREAT = new RedisStrictCommand("EXPIREAT", new BooleanReplayConvertor()); + + @Override + public Boolean expireAt(byte[] key, long unixTime) { + return write(key, StringCodec.INSTANCE, EXPIREAT, key, unixTime); + } + + @Override + public Boolean pExpireAt(byte[] key, long unixTimeInMillis) { + return write(key, StringCodec.INSTANCE, RedisCommands.PEXPIREAT, key, unixTimeInMillis); + } + + @Override + public Boolean persist(byte[] key) { + return write(key, StringCodec.INSTANCE, RedisCommands.PERSIST, key); + } + + @Override + public Boolean move(byte[] key, int dbIndex) { + return write(key, StringCodec.INSTANCE, RedisCommands.MOVE, key, dbIndex); + } + + private static final RedisStrictCommand TTL = new RedisStrictCommand("TTL"); + + @Override + public Long ttl(byte[] key) { + return read(key, StringCodec.INSTANCE, TTL, key); + } + + protected T sync(RFuture f) { + if (isPipelined()) { + return null; + } + if (isQueueing()) { + ((BatchPromise)f.toCompletableFuture()).getSentPromise().join(); + return null; + } + + return syncFuture(f); + } + + @Override + public Long ttl(byte[] key, TimeUnit timeUnit) { + return read(key, StringCodec.INSTANCE, new RedisStrictCommand("TTL", new SecondsConvertor(timeUnit, TimeUnit.SECONDS)), key); + } + + @Override + public Long pTtl(byte[] key) { + return read(key, StringCodec.INSTANCE, RedisCommands.PTTL, key); + } + + @Override + public Long pTtl(byte[] key, TimeUnit timeUnit) { + return read(key, StringCodec.INSTANCE, new RedisStrictCommand("PTTL", new SecondsConvertor(timeUnit, TimeUnit.MILLISECONDS)), key); + } + + @Override + public List sort(byte[] key, SortParameters sortParams) { + List params = new ArrayList(); + params.add(key); + if (sortParams != null) { + if (sortParams.getByPattern() != null) { + params.add("BY"); + params.add(sortParams.getByPattern()); + } + + if (sortParams.getLimit() != null) { + params.add("LIMIT"); + if (sortParams.getLimit().getStart() != -1) { + params.add(sortParams.getLimit().getStart()); + } + if (sortParams.getLimit().getCount() != -1) { + params.add(sortParams.getLimit().getCount()); + } + } + + if (sortParams.getGetPattern() != null) { + for (byte[] pattern : sortParams.getGetPattern()) { + params.add("GET"); + params.add(pattern); + } + } + + if (sortParams.getOrder() != null) { + params.add(sortParams.getOrder()); + } + + Boolean isAlpha = sortParams.isAlphabetic(); + if (isAlpha != null && isAlpha) { + params.add("ALPHA"); + } + } + + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.SORT_LIST, params.toArray()); + } + + private static final RedisCommand SORT_TO = new RedisCommand("SORT"); + + @Override + public Long sort(byte[] key, SortParameters sortParams, byte[] storeKey) { + List params = new ArrayList(); + params.add(key); + if (sortParams != null) { + if (sortParams.getByPattern() != null) { + params.add("BY"); + params.add(sortParams.getByPattern()); + } + + if (sortParams.getLimit() != null) { + params.add("LIMIT"); + if (sortParams.getLimit().getStart() != -1) { + params.add(sortParams.getLimit().getStart()); + } + if (sortParams.getLimit().getCount() != -1) { + params.add(sortParams.getLimit().getCount()); + } + } + + if (sortParams.getGetPattern() != null) { + for (byte[] pattern : sortParams.getGetPattern()) { + params.add("GET"); + params.add(pattern); + } + } + + if (sortParams.getOrder() != null) { + params.add(sortParams.getOrder()); + } + + Boolean isAlpha = sortParams.isAlphabetic(); + if (isAlpha != null && isAlpha) { + params.add("ALPHA"); + } + } + + params.add("STORE"); + params.add(storeKey); + + return read(key, ByteArrayCodec.INSTANCE, SORT_TO, params.toArray()); + } + + @Override + public byte[] dump(byte[] key) { + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.DUMP, key); + } + + @Override + public void restore(byte[] key, long ttlInMillis, byte[] serializedValue) { + write(key, StringCodec.INSTANCE, RedisCommands.RESTORE, key, ttlInMillis, serializedValue); + } + + @Override + public byte[] get(byte[] key) { + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.GET, key); + } + + @Override + public byte[] getSet(byte[] key, byte[] value) { + return write(key, ByteArrayCodec.INSTANCE, RedisCommands.GETSET, key, value); + } + + private static final RedisCommand> MGET = new RedisCommand>("MGET", new ObjectListReplayDecoder()); + + @Override + public List mGet(byte[]... keys) { + return read(keys[0], ByteArrayCodec.INSTANCE, MGET, Arrays.asList(keys).toArray()); + } + + private static final RedisCommand SET = new RedisCommand<>("SET", new BooleanNullSafeReplayConvertor()); + + @Override + public Boolean set(byte[] key, byte[] value) { + return write(key, StringCodec.INSTANCE, SET, key, value); + } + + @Override + public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption option) { + if (expiration == null) { + return set(key, value); + } else if (expiration.isPersistent()) { + if (option == null || option == SetOption.UPSERT) { + return set(key, value); + } + if (option == SetOption.SET_IF_ABSENT) { + return write(key, StringCodec.INSTANCE, SET, key, value, "NX"); + } + if (option == SetOption.SET_IF_PRESENT) { + return write(key, StringCodec.INSTANCE, SET, key, value, "XX"); + } + } else { + if (option == null || option == SetOption.UPSERT) { + return write(key, StringCodec.INSTANCE, SET, key, value, "PX", expiration.getExpirationTimeInMilliseconds()); + } + if (option == SetOption.SET_IF_ABSENT) { + return write(key, StringCodec.INSTANCE, SET, key, value, "PX", expiration.getExpirationTimeInMilliseconds(), "NX"); + } + if (option == SetOption.SET_IF_PRESENT) { + return write(key, StringCodec.INSTANCE, SET, key, value, "PX", expiration.getExpirationTimeInMilliseconds(), "XX"); + } + } + throw new IllegalArgumentException(); + } + + @Override + public Boolean setNX(byte[] key, byte[] value) { + return write(key, StringCodec.INSTANCE, RedisCommands.SETNX, key, value); + } + + private static final RedisCommand SETEX = new RedisCommand("SETEX", new BooleanReplayConvertor()); + + @Override + public Boolean setEx(byte[] key, long seconds, byte[] value) { + return write(key, StringCodec.INSTANCE, SETEX, key, seconds, value); + } + + private static final RedisCommand PSETEX = new RedisCommand("PSETEX", new BooleanReplayConvertor()); + + @Override + public Boolean pSetEx(byte[] key, long milliseconds, byte[] value) { + return write(key, StringCodec.INSTANCE, PSETEX, key, milliseconds, value); + } + + private static final RedisCommand MSET = new RedisCommand("MSET", new BooleanReplayConvertor()); + + @Override + public Boolean mSet(Map tuple) { + List params = convert(tuple); + return write(tuple.keySet().iterator().next(), StringCodec.INSTANCE, MSET, params.toArray()); + } + + protected List convert(Map tuple) { + List params = new ArrayList(tuple.size()*2); + for (Entry entry : tuple.entrySet()) { + params.add(entry.getKey()); + params.add(entry.getValue()); + } + return params; + } + + @Override + public Boolean mSetNX(Map tuple) { + List params = convert(tuple); + return write(tuple.keySet().iterator().next(), StringCodec.INSTANCE, RedisCommands.MSETNX, params.toArray()); + } + + @Override + public Long incr(byte[] key) { + return write(key, StringCodec.INSTANCE, RedisCommands.INCR, key); + } + + @Override + public Long incrBy(byte[] key, long value) { + return write(key, StringCodec.INSTANCE, RedisCommands.INCRBY, key, value); + } + + @Override + public Double incrBy(byte[] key, double value) { + return write(key, StringCodec.INSTANCE, RedisCommands.INCRBYFLOAT, key, BigDecimal.valueOf(value).toPlainString()); + } + + @Override + public Long decr(byte[] key) { + return write(key, StringCodec.INSTANCE, RedisCommands.DECR, key); + } + + private static final RedisStrictCommand DECRBY = new RedisStrictCommand("DECRBY"); + + @Override + public Long decrBy(byte[] key, long value) { + return write(key, StringCodec.INSTANCE, DECRBY, key, value); + } + + private static final RedisStrictCommand APPEND = new RedisStrictCommand("APPEND"); + + @Override + public Long append(byte[] key, byte[] value) { + return write(key, StringCodec.INSTANCE, APPEND, key, value); + } + + private static final RedisCommand GETRANGE = new RedisCommand("GETRANGE"); + + @Override + public byte[] getRange(byte[] key, long begin, long end) { + return read(key, ByteArrayCodec.INSTANCE, GETRANGE, key, begin, end); + } + + private static final RedisCommand SETRANGE = new RedisCommand("SETRANGE", new VoidReplayConvertor()); + + @Override + public void setRange(byte[] key, byte[] value, long offset) { + write(key, ByteArrayCodec.INSTANCE, SETRANGE, key, offset, value); + } + + @Override + public Boolean getBit(byte[] key, long offset) { + return read(key, StringCodec.INSTANCE, RedisCommands.GETBIT, key, offset); + } + + @Override + public Boolean setBit(byte[] key, long offset, boolean value) { + return write(key, StringCodec.INSTANCE, RedisCommands.SETBIT, key, offset, value ? 1 : 0); + } + + @Override + public Long bitCount(byte[] key) { + return read(key, StringCodec.INSTANCE, RedisCommands.BITCOUNT, key); + } + + @Override + public Long bitCount(byte[] key, long begin, long end) { + return read(key, StringCodec.INSTANCE, RedisCommands.BITCOUNT, key, begin, end); + } + + private static final RedisStrictCommand BITOP = new RedisStrictCommand("BITOP"); + + @Override + public Long bitOp(BitOperation op, byte[] destination, byte[]... keys) { + if (op == BitOperation.NOT && keys.length > 1) { + throw new UnsupportedOperationException("NOT operation doesn't support more than single source key"); + } + + List params = new ArrayList(keys.length + 2); + params.add(op); + params.add(destination); + params.addAll(Arrays.asList(keys)); + return write(keys[0], StringCodec.INSTANCE, BITOP, params.toArray()); + } + + @Override + public Long strLen(byte[] key) { + return read(key, StringCodec.INSTANCE, RedisCommands.STRLEN, key); + } + + private static final RedisStrictCommand RPUSH = new RedisStrictCommand("RPUSH"); + + @Override + public Long rPush(byte[] key, byte[]... values) { + List args = new ArrayList(values.length + 1); + args.add(key); + args.addAll(Arrays.asList(values)); + return write(key, StringCodec.INSTANCE, RPUSH, args.toArray()); + } + + private static final RedisStrictCommand LPUSH = new RedisStrictCommand("LPUSH"); + + @Override + public Long lPush(byte[] key, byte[]... values) { + List args = new ArrayList(values.length + 1); + args.add(key); + args.addAll(Arrays.asList(values)); + return write(key, StringCodec.INSTANCE, LPUSH, args.toArray()); + } + + private static final RedisStrictCommand RPUSHX = new RedisStrictCommand("RPUSHX"); + + @Override + public Long rPushX(byte[] key, byte[] value) { + return write(key, StringCodec.INSTANCE, RPUSHX, key, value); + } + + private static final RedisStrictCommand LPUSHX = new RedisStrictCommand("LPUSHX"); + + @Override + public Long lPushX(byte[] key, byte[] value) { + return write(key, StringCodec.INSTANCE, LPUSHX, key, value); + } + + private static final RedisStrictCommand LLEN = new RedisStrictCommand("LLEN"); + + @Override + public Long lLen(byte[] key) { + return read(key, StringCodec.INSTANCE, LLEN, key); + } + + @Override + public List lRange(byte[] key, long start, long end) { + return read(key, ByteArrayCodec.INSTANCE, LRANGE, key, start, end); + } + + @Override + public void lTrim(byte[] key, long start, long end) { + write(key, StringCodec.INSTANCE, RedisCommands.LTRIM, key, start, end); + } + + @Override + public byte[] lIndex(byte[] key, long index) { + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.LINDEX, key, index); + } + + private static final RedisStrictCommand LINSERT = new RedisStrictCommand("LINSERT"); + + @Override + public Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { + return write(key, StringCodec.INSTANCE, LINSERT, key, where, pivot, value); + } + + private final List commandsToRemove = Arrays.asList("SET", + "RESTORE", "LTRIM", "SETEX", "SETRANGE", "FLUSHDB", "LSET", "MSET", "HMSET", "RENAME"); + private final List indexToRemove = new ArrayList(); + private int index = -1; + + T write(byte[] key, Codec codec, RedisCommand command, Object... params) { + RFuture f = executorService.writeAsync(key, codec, command, params); + indexCommand(command); + return sync(f); + } + + protected void indexCommand(RedisCommand command) { + if (isQueueing() || isPipelined()) { + index++; + if (commandsToRemove.contains(command.getName())) { + indexToRemove.add(index); + } + } + } + + T read(byte[] key, Codec codec, RedisCommand command, Object... params) { + RFuture f = executorService.readAsync(key, codec, command, params); + indexCommand(command); + return sync(f); + } + + @Override + public void lSet(byte[] key, long index, byte[] value) { + write(key, StringCodec.INSTANCE, RedisCommands.LSET, key, index, value); + } + + private static final RedisStrictCommand LREM = new RedisStrictCommand("LREM"); + + @Override + public Long lRem(byte[] key, long count, byte[] value) { + return write(key, StringCodec.INSTANCE, LREM, key, count, value); + } + + @Override + public byte[] lPop(byte[] key) { + return write(key, ByteArrayCodec.INSTANCE, RedisCommands.LPOP, key); + } + + @Override + public byte[] rPop(byte[] key) { + return write(key, ByteArrayCodec.INSTANCE, RedisCommands.RPOP, key); + } + + @Override + public List bLPop(int timeout, byte[]... keys) { + List params = new ArrayList(keys.length + 1); + params.addAll(Arrays.asList(keys)); + params.add(timeout); + return write(keys[0], ByteArrayCodec.INSTANCE, RedisCommands.BLPOP, params.toArray()); + } + + @Override + public List bRPop(int timeout, byte[]... keys) { + List params = new ArrayList(keys.length + 1); + params.addAll(Arrays.asList(keys)); + params.add(timeout); + return write(keys[0], ByteArrayCodec.INSTANCE, RedisCommands.BRPOP, params.toArray()); + } + + @Override + public byte[] rPopLPush(byte[] srcKey, byte[] dstKey) { + return write(srcKey, ByteArrayCodec.INSTANCE, RedisCommands.RPOPLPUSH, srcKey, dstKey); + } + + @Override + public byte[] bRPopLPush(int timeout, byte[] srcKey, byte[] dstKey) { + return write(srcKey, ByteArrayCodec.INSTANCE, RedisCommands.BRPOPLPUSH, srcKey, dstKey, timeout); + } + + private static final RedisCommand> LPOS = new RedisCommand<>("LPOS", new ObjectListReplayDecoder<>()); + + @Override + public List lPos(byte[] key, byte[] element, Integer rank, Integer count) { + List args = new ArrayList<>(); + args.add(key); + args.add(element); + if (rank != null) { + args.add("RANK"); + args.add(rank); + } + if (count != null) { + args.add("COUNT"); + args.add(count); + } + Object read = read(key, ByteArrayCodec.INSTANCE, LPOS, args.toArray()); + if (read == null) { + return Collections.emptyList(); + } else if (read instanceof Long) { + return Collections.singletonList((Long) read); + } else { + return (List) read; + } + } + + private static final RedisCommand SADD = new RedisCommand("SADD"); + + @Override + public Long sAdd(byte[] key, byte[]... values) { + List args = new ArrayList(values.length + 1); + args.add(key); + args.addAll(Arrays.asList(values)); + + return write(key, StringCodec.INSTANCE, SADD, args.toArray()); + } + + private static final RedisStrictCommand SREM = new RedisStrictCommand("SREM"); + + @Override + public Long sRem(byte[] key, byte[]... values) { + List args = new ArrayList(values.length + 1); + args.add(key); + args.addAll(Arrays.asList(values)); + + return write(key, StringCodec.INSTANCE, SREM, args.toArray()); + } + + @Override + public byte[] sPop(byte[] key) { + return write(key, ByteArrayCodec.INSTANCE, RedisCommands.SPOP_SINGLE, key); + } + + private static final RedisCommand> SPOP = new RedisCommand>("SPOP", new ObjectListReplayDecoder()); + + @Override + public List sPop(byte[] key, long count) { + return write(key, ByteArrayCodec.INSTANCE, SPOP, key, count); + } + + @Override + public Boolean sMove(byte[] srcKey, byte[] destKey, byte[] value) { + return write(srcKey, StringCodec.INSTANCE, RedisCommands.SMOVE, srcKey, destKey, value); + } + + private static final RedisStrictCommand SCARD = new RedisStrictCommand("SCARD"); + + @Override + public Long sCard(byte[] key) { + return read(key, StringCodec.INSTANCE, SCARD, key); + } + + @Override + public Boolean sIsMember(byte[] key, byte[] value) { + return read(key, StringCodec.INSTANCE, RedisCommands.SISMEMBER, key, value); + } + + @Override + public Set sInter(byte[]... keys) { + return write(keys[0], ByteArrayCodec.INSTANCE, RedisCommands.SINTER, Arrays.asList(keys).toArray()); + } + + @Override + public Long sInterStore(byte[] destKey, byte[]... keys) { + List args = new ArrayList(keys.length + 1); + args.add(destKey); + args.addAll(Arrays.asList(keys)); + return write(keys[0], StringCodec.INSTANCE, RedisCommands.SINTERSTORE, args.toArray()); + } + + @Override + public Set sUnion(byte[]... keys) { + return write(keys[0], ByteArrayCodec.INSTANCE, RedisCommands.SUNION, Arrays.asList(keys).toArray()); + } + + @Override + public Long sUnionStore(byte[] destKey, byte[]... keys) { + List args = new ArrayList(keys.length + 1); + args.add(destKey); + args.addAll(Arrays.asList(keys)); + return write(keys[0], StringCodec.INSTANCE, RedisCommands.SUNIONSTORE, args.toArray()); + } + + @Override + public Set sDiff(byte[]... keys) { + return write(keys[0], ByteArrayCodec.INSTANCE, RedisCommands.SDIFF, Arrays.asList(keys).toArray()); + } + + @Override + public Long sDiffStore(byte[] destKey, byte[]... keys) { + List args = new ArrayList(keys.length + 1); + args.add(destKey); + args.addAll(Arrays.asList(keys)); + return write(keys[0], StringCodec.INSTANCE, RedisCommands.SDIFFSTORE, args.toArray()); + } + + @Override + public Set sMembers(byte[] key) { + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.SMEMBERS, key); + } + + @Override + public byte[] sRandMember(byte[] key) { + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.SRANDMEMBER_SINGLE, key); + } + + private static final RedisCommand> SRANDMEMBER = new RedisCommand<>("SRANDMEMBER", new ObjectListReplayDecoder<>()); + + @Override + public List sRandMember(byte[] key, long count) { + return read(key, ByteArrayCodec.INSTANCE, SRANDMEMBER, key, count); + } + + @Override + public Cursor sScan(byte[] key, ScanOptions options) { + return new KeyBoundCursor(key, 0, options) { + + private RedisClient client; + + @Override + protected ScanIteration doScan(byte[] key, long cursorId, ScanOptions options) { + if (isQueueing() || isPipelined()) { + throw new UnsupportedOperationException("'SSCAN' cannot be called in pipeline / transaction mode."); + } + + List args = new ArrayList(); + args.add(key); + args.add(cursorId); + if (options.getPattern() != null) { + args.add("MATCH"); + args.add(options.getPattern()); + } + if (options.getCount() != null) { + args.add("COUNT"); + args.add(options.getCount()); + } + + RFuture> f = executorService.readAsync(client, key, ByteArrayCodec.INSTANCE, RedisCommands.SSCAN, args.toArray()); + ListScanResult res = syncFuture(f); + client = res.getRedisClient(); + return new ScanIteration(res.getPos(), res.getValues()); + } + }.open(); + } + + @Override + public Boolean zAdd(byte[] key, double score, byte[] value, ZAddArgs args) { + List params = new ArrayList<>(); + params.add(key); + + if (args.contains(ZAddArgs.Flag.XX)) { + params.add("XX"); + } + if (args.contains(ZAddArgs.Flag.NX)) { + params.add("NX"); + } + if (args.contains(ZAddArgs.Flag.GT)) { + params.add("GT"); + } + if (args.contains(ZAddArgs.Flag.LT)) { + params.add("LT"); + } + if (args.contains(ZAddArgs.Flag.CH)) { + params.add("CH"); + } + + params.add(BigDecimal.valueOf(score).toPlainString()); + params.add(value); + + return write(key, StringCodec.INSTANCE, RedisCommands.ZADD_BOOL, params.toArray()); + } + + @Override + public Long zAdd(byte[] key, Set tuples, ZAddArgs args) { + List params = new ArrayList<>(tuples.size()*2+1); + params.add(key); + + if (args.contains(ZAddArgs.Flag.XX)) { + params.add("XX"); + } + if (args.contains(ZAddArgs.Flag.NX)) { + params.add("NX"); + } + if (args.contains(ZAddArgs.Flag.GT)) { + params.add("GT"); + } + if (args.contains(ZAddArgs.Flag.LT)) { + params.add("LT"); + } + if (args.contains(ZAddArgs.Flag.CH)) { + params.add("CH"); + } + + for (Tuple entry : tuples) { + params.add(BigDecimal.valueOf(entry.getScore()).toPlainString()); + params.add(entry.getValue()); + } + return write(key, StringCodec.INSTANCE, RedisCommands.ZADD, params.toArray()); + } + + @Override + public Long zRem(byte[] key, byte[]... values) { + List params = new ArrayList(values.length+1); + params.add(key); + params.addAll(Arrays.asList(values)); + + return write(key, StringCodec.INSTANCE, RedisCommands.ZREM_LONG, params.toArray()); + } + + @Override + public Double zIncrBy(byte[] key, double increment, byte[] value) { + return write(key, DoubleCodec.INSTANCE, RedisCommands.ZINCRBY, + key, new BigDecimal(increment).toPlainString(), value); + } + + @Override + public Long zRank(byte[] key, byte[] value) { + return read(key, StringCodec.INSTANCE, RedisCommands.ZRANK, key, value); + } + + @Override + public Long zRevRank(byte[] key, byte[] value) { + return read(key, StringCodec.INSTANCE, RedisCommands.ZREVRANK, key, value); + } + + private static final RedisCommand> ZRANGE = new RedisCommand>("ZRANGE", new ObjectSetReplayDecoder()); + + @Override + public Set zRange(byte[] key, long start, long end) { + return read(key, ByteArrayCodec.INSTANCE, ZRANGE, key, start, end); + } + + private static final RedisCommand> ZRANGE_ENTRY = new RedisCommand>("ZRANGE", new ScoredSortedSetReplayDecoder()); + + @Override + public Set zRangeWithScores(byte[] key, long start, long end) { + return read(key, ByteArrayCodec.INSTANCE, ZRANGE_ENTRY, key, start, end, "WITHSCORES"); + } + + private String value(org.springframework.data.domain.Range.Bound boundary, String defaultValue) { + if (boundary == null) { + return defaultValue; + } + + Object score = boundary.getValue().orElse(null); + if (score == null) { + return defaultValue; + } + StringBuilder element = new StringBuilder(); + if (!boundary.isInclusive()) { + element.append("("); + } else { + if (!(score instanceof Double)) { + element.append("["); + } + } + if (score instanceof Double) { + if (Double.isInfinite((Double) score)) { + element.append((Double)score > 0 ? "+inf" : "-inf"); + } else { + element.append(BigDecimal.valueOf((Double)score).toPlainString()); + } + } else { + if (score instanceof byte[]) { + element.append(new String((byte[]) score, StandardCharsets.UTF_8)); + } else { + element.append(score); + } + } + + return element.toString(); + } + + private static final RedisCommand> ZRANGEBYSCORE = new RedisCommand>("ZRANGEBYSCORE", new ScoredSortedSetReplayDecoder()); + + @Override + public Set zRangeByScoreWithScores(byte[] key, org.springframework.data.domain.Range range, org.springframework.data.redis.connection.Limit limit) { + String min = value(range.getLowerBound(), "-inf"); + String max = value(range.getUpperBound(), "+inf"); + + List args = new ArrayList(); + args.add(key); + args.add(min); + args.add(max); + args.add("WITHSCORES"); + + if (limit != null) { + args.add("LIMIT"); + args.add(limit.getOffset()); + args.add(limit.getCount()); + } + + return read(key, ByteArrayCodec.INSTANCE, ZRANGEBYSCORE, args.toArray()); + } + + private static final RedisCommand> ZREVRANGE = new RedisCommand>("ZREVRANGE", new ObjectSetReplayDecoder()); + + @Override + public Set zRevRange(byte[] key, long start, long end) { + return read(key, ByteArrayCodec.INSTANCE, ZREVRANGE, key, start, end); + } + + private static final RedisCommand> ZREVRANGE_ENTRY = new RedisCommand>("ZREVRANGE", new ScoredSortedSetReplayDecoder()); + + @Override + public Set zRevRangeWithScores(byte[] key, long start, long end) { + return read(key, ByteArrayCodec.INSTANCE, ZREVRANGE_ENTRY, key, start, end, "WITHSCORES"); + } + + @Override + public Set zRevRangeByScore(byte[] key, double min, double max) { + return zRevRangeByScore(key, org.springframework.data.domain.Range.closed(min, max)); + } + + private static final RedisCommand> ZREVRANGEBYSCORE = new RedisCommand>("ZREVRANGEBYSCORE", new ObjectSetReplayDecoder()); + private static final RedisCommand> ZREVRANGEBYSCOREWITHSCORES = new RedisCommand>("ZREVRANGEBYSCORE", new ScoredSortedSetReplayDecoder()); + + @Override + public Set zRevRangeByScore(byte[] key, org.springframework.data.domain.Range range, org.springframework.data.redis.connection.Limit limit) { + String min = value(range.getLowerBound(), "-inf"); + String max = value(range.getUpperBound(), "+inf"); + + List args = new ArrayList(); + args.add(key); + args.add(max); + args.add(min); + + if (limit != null) { + args.add("LIMIT"); + args.add(limit.getOffset()); + args.add(limit.getCount()); + } + + return read(key, ByteArrayCodec.INSTANCE, ZREVRANGEBYSCORE, args.toArray()); + } + + @Override + public Set zRevRangeByScoreWithScores(byte[] key, org.springframework.data.domain.Range range, org.springframework.data.redis.connection.Limit limit) { + String min = value(range.getLowerBound(), "-inf"); + String max = value(range.getUpperBound(), "+inf"); + + List args = new ArrayList(); + args.add(key); + args.add(max); + args.add(min); + args.add("WITHSCORES"); + + if (limit != null) { + args.add("LIMIT"); + args.add(limit.getOffset()); + args.add(limit.getCount()); + } + + return read(key, ByteArrayCodec.INSTANCE, ZREVRANGEBYSCOREWITHSCORES, args.toArray()); + } + + @Override + public Long zCount(byte[] key, double min, double max) { + return zCount(key, org.springframework.data.domain.Range.closed(min, max)); + } + + private static final RedisStrictCommand ZCOUNT = new RedisStrictCommand("ZCOUNT"); + + @Override + public Long zCount(byte[] key, org.springframework.data.domain.Range range) { + String min = value(range.getLowerBound(), "-inf"); + String max = value(range.getUpperBound(), "+inf"); + return read(key, StringCodec.INSTANCE, ZCOUNT, key, min, max); + } + + @Override + public Long zCard(byte[] key) { + return read(key, StringCodec.INSTANCE, RedisCommands.ZCARD, key); + } + + @Override + public Double zScore(byte[] key, byte[] value) { + return read(key, StringCodec.INSTANCE, RedisCommands.ZSCORE, key, value); + } + + private static final RedisStrictCommand ZREMRANGEBYRANK = new RedisStrictCommand("ZREMRANGEBYRANK"); + private static final RedisStrictCommand ZREMRANGEBYSCORE = new RedisStrictCommand("ZREMRANGEBYSCORE"); + + @Override + public Long zRemRange(byte[] key, long start, long end) { + return write(key, StringCodec.INSTANCE, ZREMRANGEBYRANK, key, start, end); + } + + @Override + public Long zRemRangeByScore(byte[] key, double min, double max) { + return zRemRangeByScore(key, org.springframework.data.domain.Range.closed(min, max)); + } + + @Override + public Long zRemRangeByScore(byte[] key, org.springframework.data.domain.Range range) { + String min = value(range.getLowerBound(), "-inf"); + String max = value(range.getUpperBound(), "+inf"); + return write(key, StringCodec.INSTANCE, ZREMRANGEBYSCORE, key, min, max); + } + + @Override + public Long zUnionStore(byte[] destKey, byte[]... sets) { + return zUnionStore(destKey, null, (Weights)null, sets); + } + + private static final RedisStrictCommand ZUNIONSTORE = new RedisStrictCommand("ZUNIONSTORE"); + + @Override + public Long zUnionStore(byte[] destKey, Aggregate aggregate, Weights weights, byte[]... sets) { + List args = new ArrayList(sets.length*2 + 5); + args.add(destKey); + args.add(sets.length); + args.addAll(Arrays.asList(sets)); + if (weights != null) { + args.add("WEIGHTS"); + for (double weight : weights.toArray()) { + args.add(BigDecimal.valueOf(weight).toPlainString()); + } + } + if (aggregate != null) { + args.add("AGGREGATE"); + args.add(aggregate.name()); + } + return write(destKey, LongCodec.INSTANCE, ZUNIONSTORE, args.toArray()); + } + + private static final RedisStrictCommand ZINTERSTORE = new RedisStrictCommand("ZINTERSTORE"); + + @Override + public Long zInterStore(byte[] destKey, byte[]... sets) { + return zInterStore(destKey, null, (Weights)null, sets); + } + + @Override + public Long zInterStore(byte[] destKey, Aggregate aggregate, Weights weights, byte[]... sets) { + List args = new ArrayList(sets.length*2 + 5); + args.add(destKey); + args.add(sets.length); + args.addAll(Arrays.asList(sets)); + if (weights != null) { + args.add("WEIGHTS"); + for (double weight : weights.toArray()) { + args.add(BigDecimal.valueOf(weight).toPlainString()); + } + } + if (aggregate != null) { + args.add("AGGREGATE"); + args.add(aggregate.name()); + } + return write(destKey, StringCodec.INSTANCE, ZINTERSTORE, args.toArray()); + } + + private static final RedisCommand> ZSCAN = new RedisCommand<>("ZSCAN", new ListMultiDecoder2(new ListScanResultReplayDecoder(), new ScoredSortedListReplayDecoder())); + + @Override + public Cursor zScan(byte[] key, ScanOptions options) { + return new KeyBoundCursor(key, 0, options) { + + private RedisClient client; + + @Override + protected ScanIteration doScan(byte[] key, long cursorId, ScanOptions options) { + if (isQueueing() || isPipelined()) { + throw new UnsupportedOperationException("'ZSCAN' cannot be called in pipeline / transaction mode."); + } + + List args = new ArrayList(); + args.add(key); + args.add(cursorId); + if (options.getPattern() != null) { + args.add("MATCH"); + args.add(options.getPattern()); + } + if (options.getCount() != null) { + args.add("COUNT"); + args.add(options.getCount()); + } + + RFuture> f = executorService.readAsync(client, key, ByteArrayCodec.INSTANCE, ZSCAN, args.toArray()); + ListScanResult res = syncFuture(f); + client = res.getRedisClient(); + return new ScanIteration(res.getPos(), res.getValues()); + } + }.open(); + } + + @Override + public Set zRangeByScore(byte[] key, String min, String max) { + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.ZRANGEBYSCORE, key, min, max); + } + + @Override + public Set zRangeByScore(byte[] key, String min, String max, long offset, long count) { + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.ZRANGEBYSCORE, key, min, max, "LIMIT", offset, count); + } + + @Override + public Set zRangeByScore(byte[] key, org.springframework.data.domain.Range range, org.springframework.data.redis.connection.Limit limit) { + String min = value(range.getLowerBound(), "-inf"); + String max = value(range.getUpperBound(), "+inf"); + + List args = new ArrayList(); + args.add(key); + args.add(min); + args.add(max); + + if (limit != null) { + args.add("LIMIT"); + args.add(limit.getOffset()); + args.add(limit.getCount()); + } + + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.ZRANGEBYSCORE, args.toArray()); + } + + private static final RedisCommand> ZRANGEBYLEX = new RedisCommand>("ZRANGEBYLEX", new ObjectSetReplayDecoder()); + + @Override + public Set zRangeByLex(byte[] key, org.springframework.data.domain.Range range, org.springframework.data.redis.connection.Limit limit) { + String min = value(range.getLowerBound(), "-"); + String max = value(range.getUpperBound(), "+"); + + List args = new ArrayList(); + args.add(key); + args.add(min); + args.add(max); + + if (limit != null) { + args.add("LIMIT"); + args.add(limit.getOffset()); + args.add(limit.getCount()); + } + + return read(key, ByteArrayCodec.INSTANCE, ZRANGEBYLEX, args.toArray()); + } + + @Override + public Boolean hSet(byte[] key, byte[] field, byte[] value) { + return write(key, StringCodec.INSTANCE, RedisCommands.HSET, key, field, value); + } + + @Override + public Boolean hSetNX(byte[] key, byte[] field, byte[] value) { + return write(key, StringCodec.INSTANCE, RedisCommands.HSETNX, key, field, value); + } + + @Override + public byte[] hGet(byte[] key, byte[] field) { + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.HGET, key, field); + } + + private static final RedisCommand> HMGET = new RedisCommand>("HMGET", new ObjectListReplayDecoder()); + + @Override + public List hMGet(byte[] key, byte[]... fields) { + List args = new ArrayList(fields.length + 1); + args.add(key); + args.addAll(Arrays.asList(fields)); + return read(key, ByteArrayCodec.INSTANCE, HMGET, args.toArray()); + } + + @Override + public void hMSet(byte[] key, Map hashes) { + List params = new ArrayList(hashes.size()*2 + 1); + params.add(key); + for (Map.Entry entry : hashes.entrySet()) { + params.add(entry.getKey()); + params.add(entry.getValue()); + } + + write(key, StringCodec.INSTANCE, RedisCommands.HMSET, params.toArray()); + } + + private static final RedisCommand HINCRBY = new RedisCommand("HINCRBY"); + + @Override + public Long hIncrBy(byte[] key, byte[] field, long delta) { + return write(key, StringCodec.INSTANCE, HINCRBY, key, field, delta); + } + + private static final RedisCommand HINCRBYFLOAT = new RedisCommand("HINCRBYFLOAT", new DoubleReplayConvertor()); + + @Override + public Double hIncrBy(byte[] key, byte[] field, double delta) { + return write(key, StringCodec.INSTANCE, HINCRBYFLOAT, key, field, BigDecimal.valueOf(delta).toPlainString()); + } + + @Override + public Boolean hExists(byte[] key, byte[] field) { + return read(key, StringCodec.INSTANCE, RedisCommands.HEXISTS, key, field); + } + + @Override + public Long hDel(byte[] key, byte[]... fields) { + List args = new ArrayList(fields.length + 1); + args.add(key); + args.addAll(Arrays.asList(fields)); + return write(key, StringCodec.INSTANCE, RedisCommands.HDEL, args.toArray()); + } + + private static final RedisStrictCommand HLEN = new RedisStrictCommand("HLEN"); + + @Override + public Long hLen(byte[] key) { + return read(key, StringCodec.INSTANCE, HLEN, key); + } + + @Override + public Set hKeys(byte[] key) { + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.HKEYS, key); + } + + @Override + public List hVals(byte[] key) { + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.HVALS, key); + } + + @Override + public Map hGetAll(byte[] key) { + return read(key, ByteArrayCodec.INSTANCE, RedisCommands.HGETALL, key); + } + + @Override + public Cursor> hScan(byte[] key, ScanOptions options) { + return new KeyBoundCursor>(key, 0, options) { + + private RedisClient client; + + @Override + protected ScanIteration> doScan(byte[] key, long cursorId, ScanOptions options) { + if (isQueueing() || isPipelined()) { + throw new UnsupportedOperationException("'HSCAN' cannot be called in pipeline / transaction mode."); + } + + List args = new ArrayList(); + args.add(key); + args.add(cursorId); + if (options.getPattern() != null) { + args.add("MATCH"); + args.add(options.getPattern()); + } + if (options.getCount() != null) { + args.add("COUNT"); + args.add(options.getCount()); + } + + RFuture> f = executorService.readAsync(client, key, ByteArrayCodec.INSTANCE, RedisCommands.HSCAN, args.toArray()); + MapScanResult res = syncFuture(f); + client = res.getRedisClient(); + return new ScanIteration>(res.getPos(), res.getValues()); + } + }.open(); + } + + @Override + public void multi() { + if (isQueueing()) { + return; + } + + if (isPipelined()) { + BatchOptions options = BatchOptions.defaults() + .executionMode(ExecutionMode.IN_MEMORY_ATOMIC); + this.executorService = new CommandBatchService(executorService, options); + return; + } + + BatchOptions options = BatchOptions.defaults() + .executionMode(ExecutionMode.REDIS_WRITE_ATOMIC); + this.executorService = new CommandBatchService(executorService, options); + } + + @Override + public List exec() { + if (isPipelinedAtomic()) { + return null; + } + if (isQueueing()) { + try { + BatchResult result = ((CommandBatchService)executorService).execute(); + filterResults(result); + return (List) result.getResponses(); + } catch (Exception ex) { + throw transform(ex); + } finally { + resetConnection(); + } + } else { + throw new InvalidDataAccessApiUsageException("Not in transaction mode. Please invoke multi method"); + } + } + + protected void filterResults(BatchResult result) { + if (result.getResponses().isEmpty()) { + return; + } + + int t = 0; + for (Integer index : indexToRemove) { + index -= t; + result.getResponses().remove((int)index); + t++; + } + for (ListIterator iterator = (ListIterator) result.getResponses().listIterator(); iterator.hasNext();) { + Object object = iterator.next(); + if (object instanceof String) { + iterator.set(((String) object).getBytes()); + } + } + } + + protected void resetConnection() { + executorService = (CommandAsyncService) this.redisson.getCommandExecutor(); + index = -1; + indexToRemove.clear(); + } + + @Override + public void discard() { + if (isQueueing()) { + syncFuture(executorService.writeAsync(null, RedisCommands.DISCARD)); + resetConnection(); + } else { + throw new InvalidDataAccessApiUsageException("Not in transaction mode. Please invoke multi method"); + } + } + + @Override + public void watch(byte[]... keys) { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + + syncFuture(executorService.writeAsync(null, RedisCommands.WATCH, keys)); + } + + @Override + public void unwatch() { + syncFuture(executorService.writeAsync(null, RedisCommands.UNWATCH)); + } + + @Override + public boolean isSubscribed() { + return subscription != null && subscription.isAlive(); + } + + @Override + public Subscription getSubscription() { + return subscription; + } + + @Override + public Long publish(byte[] channel, byte[] message) { + return write(channel, StringCodec.INSTANCE, RedisCommands.PUBLISH, channel, message); + } + + @Override + public void subscribe(MessageListener listener, byte[]... channels) { + checkSubscription(); + + subscription = new RedissonSubscription(executorService, redisson.getConnectionManager().getSubscribeService(), listener); + subscription.subscribe(channels); + } + + private void checkSubscription() { + if (subscription != null) { + throw new RedisSubscribedConnectionException("Connection already subscribed"); + } + + if (isQueueing()) { + throw new UnsupportedOperationException("Not supported in queueing mode"); + } + if (isPipelined()) { + throw new UnsupportedOperationException("Not supported in pipelined mode"); + } + } + + @Override + public void pSubscribe(MessageListener listener, byte[]... patterns) { + checkSubscription(); + + subscription = new RedissonSubscription(executorService, redisson.getConnectionManager().getSubscribeService(), listener); + subscription.pSubscribe(patterns); + } + + @Override + public void select(int dbIndex) { + throw new UnsupportedOperationException(); + } + + private static final RedisCommand ECHO = new RedisCommand("ECHO"); + + @Override + public byte[] echo(byte[] message) { + return read(null, ByteArrayCodec.INSTANCE, ECHO, message); + } + + @Override + public String ping() { + return read(null, StringCodec.INSTANCE, RedisCommands.PING); + } + +// @Override +// public void bgWriteAof() { +// throw new UnsupportedOperationException(); +// } + + @Override + public void bgReWriteAof() { + write(null, StringCodec.INSTANCE, RedisCommands.BGREWRITEAOF); + } + + @Override + public void bgSave() { + write(null, StringCodec.INSTANCE, RedisCommands.BGSAVE); + } + + @Override + public Long lastSave() { + return write(null, StringCodec.INSTANCE, RedisCommands.LASTSAVE); + } + + private static final RedisStrictCommand SAVE = new RedisStrictCommand("SAVE", new VoidReplayConvertor()); + + @Override + public void save() { + write(null, StringCodec.INSTANCE, SAVE); + } + + @Override + public Long dbSize() { + if (isQueueing()) { + return read(null, StringCodec.INSTANCE, RedisCommands.DBSIZE); + } + + List> futures = executorService.readAllAsync(RedisCommands.DBSIZE); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + CompletableFuture s = f.thenApply(r -> futures.stream().mapToLong(v -> v.getNow(0L)).sum()); + CompletableFutureWrapper ff = new CompletableFutureWrapper<>(s); + return sync(ff); + } + + @Override + public void flushDb() { + if (isQueueing() || isPipelined()) { + write(null, StringCodec.INSTANCE, RedisCommands.FLUSHDB); + return; + } + + RFuture f = executorService.writeAllVoidAsync(RedisCommands.FLUSHDB); + sync(f); + } + + @Override + public void flushAll() { + RFuture f = executorService.writeAllVoidAsync(RedisCommands.FLUSHALL); + sync(f); + } + + private static final RedisStrictCommand INFO_DEFAULT = new RedisStrictCommand("INFO", "DEFAULT", new ObjectDecoder(new PropertiesDecoder())); + private static final RedisStrictCommand INFO = new RedisStrictCommand("INFO", new ObjectDecoder(new PropertiesDecoder())); + + @Override + public Properties info() { + return read(null, StringCodec.INSTANCE, INFO_DEFAULT); + } + + @Override + public Properties info(String section) { + return read(null, StringCodec.INSTANCE, INFO, section); + } + + @Override + public void shutdown() { + throw new UnsupportedOperationException(); + } + + @Override + public void shutdown(ShutdownOption option) { + throw new UnsupportedOperationException(); + } + + private static final RedisStrictCommand CONFIG_GET = new RedisStrictCommand("CONFIG", "GET", new PropertiesListDecoder()); + + @Override + public Properties getConfig(String pattern) { + return read(null, StringCodec.INSTANCE, CONFIG_GET, pattern); + } + + @Override + public void setConfig(String param, String value) { + write(null, StringCodec.INSTANCE, RedisCommands.CONFIG_SET, param, value); + } + + @Override + public void resetConfigStats() { + write(null, StringCodec.INSTANCE, RedisCommands.CONFIG_RESETSTAT); + } + + private static final RedisStrictCommand TIME = new RedisStrictCommand("TIME", new TimeLongObjectDecoder()); + + @Override + public Long time() { + return read(null, LongCodec.INSTANCE, TIME); + } + + @Override + public void killClient(String host, int port) { + throw new UnsupportedOperationException(); + } + + @Override + public void setClientName(byte[] name) { + throw new UnsupportedOperationException("Should be defined through Redisson Config object"); + } + + @Override + public String getClientName() { + throw new UnsupportedOperationException(); + } + + @Override + public List getClientList() { + throw new UnsupportedOperationException(); + } + +// @Override +// public void slaveOf(String host, int port) { +// throw new UnsupportedOperationException(); +// } +// +// @Override +// public void slaveOfNoOne() { +// throw new UnsupportedOperationException(); +// } + + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option) { + migrate(key, target, dbIndex, option, Long.MAX_VALUE); + } + + @Override + public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option, long timeout) { + write(key, StringCodec.INSTANCE, RedisCommands.MIGRATE, target.getHost(), target.getPort(), key, dbIndex, timeout); + } + + @Override + public void scriptFlush() { + if (isQueueing() || isPipelined()) { + throw new UnsupportedOperationException(); + } + + RFuture f = executorService.writeAllVoidAsync(RedisCommands.SCRIPT_FLUSH); + sync(f); + } + + @Override + public void scriptKill() { + throw new UnsupportedOperationException(); + } + + @Override + public String scriptLoad(byte[] script) { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + + List> futures = executorService.executeAllAsync(RedisCommands.SCRIPT_LOAD, (Object)script); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + CompletableFuture s = f.thenApply(r -> futures.get(0).getNow(null)); + return sync(new CompletableFutureWrapper<>(s)); + } + + @Override + public List scriptExists(final String... scriptShas) { + if (isQueueing() || isPipelined()) { + throw new UnsupportedOperationException(); + } + + List>> futures = executorService.writeAllAsync(RedisCommands.SCRIPT_EXISTS, (Object[]) scriptShas); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + CompletableFuture> s = f.thenApply(r -> { + List result = futures.get(0).getNow(new ArrayList<>()); + for (CompletableFuture> future : futures.subList(1, futures.size())) { + List l = future.getNow(new ArrayList<>()); + for (int i = 0; i < l.size(); i++) { + result.set(i, result.get(i) | l.get(i)); + } + } + return result; + }); + return sync(new CompletableFutureWrapper<>(s)); + } + + @Override + public T eval(byte[] script, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + + RedisCommand c = toCommand(returnType, "EVAL"); + List params = new ArrayList(); + params.add(script); + params.add(numKeys); + params.addAll(Arrays.asList(keysAndArgs)); + + byte[] key = getKey(numKeys, keysAndArgs); + return write(key, ByteArrayCodec.INSTANCE, c, params.toArray()); + } + + protected RedisCommand toCommand(ReturnType returnType, String name) { + RedisCommand c = null; + if (returnType == ReturnType.BOOLEAN) { + c = org.redisson.api.RScript.ReturnType.BOOLEAN.getCommand(); + } else if (returnType == ReturnType.INTEGER) { + c = org.redisson.api.RScript.ReturnType.INTEGER.getCommand(); + } else if (returnType == ReturnType.MULTI) { + c = org.redisson.api.RScript.ReturnType.MULTI.getCommand(); + return new RedisCommand(c, name, new BinaryConvertor()); + } else if (returnType == ReturnType.STATUS) { + c = org.redisson.api.RScript.ReturnType.STATUS.getCommand(); + } else if (returnType == ReturnType.VALUE) { + c = org.redisson.api.RScript.ReturnType.VALUE.getCommand(); + return new RedisCommand(c, name, new BinaryConvertor()); + } + return new RedisCommand(c, name); + } + + @Override + public T evalSha(String scriptSha, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + if (isQueueing()) { + throw new UnsupportedOperationException(); + } + if (isPipelined()) { + throw new UnsupportedOperationException(); + } + + RedisCommand c = toCommand(returnType, "EVALSHA"); + List params = new ArrayList(); + params.add(scriptSha); + params.add(numKeys); + params.addAll(Arrays.asList(keysAndArgs)); + + byte[] key = getKey(numKeys, keysAndArgs); + return write(key, ByteArrayCodec.INSTANCE, c, params.toArray()); + } + + @Override + public T evalSha(byte[] scriptSha, ReturnType returnType, int numKeys, byte[]... keysAndArgs) { + RedisCommand c = toCommand(returnType, "EVALSHA"); + List params = new ArrayList(); + params.add(scriptSha); + params.add(numKeys); + params.addAll(Arrays.asList(keysAndArgs)); + + byte[] key = getKey(numKeys, keysAndArgs); + return write(key, ByteArrayCodec.INSTANCE, c, params.toArray()); + } + + private static byte[] getKey(int numKeys, byte[][] keysAndArgs) { + if (numKeys > 0 && keysAndArgs.length > 0) { + return keysAndArgs[0]; + } + return null; + } + + @Override + public Long geoAdd(byte[] key, Point point, byte[] member) { + return write(key, StringCodec.INSTANCE, RedisCommands.GEOADD, key, point.getX(), point.getY(), member); + } + + @Override + public Long geoAdd(byte[] key, GeoLocation location) { + return write(key, StringCodec.INSTANCE, RedisCommands.GEOADD, key, location.getPoint().getX(), location.getPoint().getY(), location.getName()); + } + + @Override + public Long geoAdd(byte[] key, Map memberCoordinateMap) { + List params = new ArrayList(memberCoordinateMap.size()*3 + 1); + params.add(key); + for (Entry entry : memberCoordinateMap.entrySet()) { + params.add(entry.getValue().getX()); + params.add(entry.getValue().getY()); + params.add(entry.getKey()); + } + return write(key, StringCodec.INSTANCE, RedisCommands.GEOADD, params.toArray()); + } + + @Override + public Long geoAdd(byte[] key, Iterable> locations) { + List params = new ArrayList(); + params.add(key); + for (GeoLocation location : locations) { + params.add(location.getPoint().getX()); + params.add(location.getPoint().getY()); + params.add(location.getName()); + } + return write(key, StringCodec.INSTANCE, RedisCommands.GEOADD, params.toArray()); + } + + @Override + public Distance geoDist(byte[] key, byte[] member1, byte[] member2) { + return geoDist(key, member1, member2, DistanceUnit.METERS); + } + + @Override + public Distance geoDist(byte[] key, byte[] member1, byte[] member2, Metric metric) { + return read(key, DoubleCodec.INSTANCE, new RedisCommand("GEODIST", new DistanceConvertor(metric)), key, member1, member2, getAbbreviation(metric)); + } + + private static final RedisCommand> GEOHASH = new RedisCommand>("GEOHASH", new ObjectListReplayDecoder()); + + @Override + public List geoHash(byte[] key, byte[]... members) { + List params = new ArrayList(members.length + 1); + params.add(key); + for (byte[] member : members) { + params.add(member); + } + return read(key, StringCodec.INSTANCE, GEOHASH, params.toArray()); + } + + private final MultiDecoder> geoDecoder = new ListMultiDecoder2(new ObjectListReplayDecoder2(), new PointDecoder()); + + @Override + public List geoPos(byte[] key, byte[]... members) { + List params = new ArrayList(members.length + 1); + params.add(key); + params.addAll(Arrays.asList(members)); + + RedisCommand> command = new RedisCommand>("GEOPOS", geoDecoder); + return read(key, StringCodec.INSTANCE, command, params.toArray()); + } + + private String convert(double longitude) { + return BigDecimal.valueOf(longitude).toPlainString(); + } + + private final MultiDecoder>> postitionDecoder = new ListMultiDecoder2(new GeoResultsDecoder(), new CodecDecoder(), new PointDecoder(), new ObjectListReplayDecoder()); + + @Override + public GeoResults> geoRadius(byte[] key, Circle within) { + RedisCommand>> command = new RedisCommand>>("GEORADIUS_RO", new GeoResultsDecoder()); + return read(key, ByteArrayCodec.INSTANCE, command, key, + convert(within.getCenter().getX()), convert(within.getCenter().getY()), + within.getRadius().getValue(), getAbbreviation(within.getRadius().getMetric())); + } + + @Override + public GeoResults> geoRadius(byte[] key, Circle within, GeoRadiusCommandArgs args) { + List params = new ArrayList(); + params.add(key); + params.add(convert(within.getCenter().getX())); + params.add(convert(within.getCenter().getY())); + params.add(within.getRadius().getValue()); + params.add(getAbbreviation(within.getRadius().getMetric())); + + RedisCommand>> command; + if (args.getFlags().contains(GeoRadiusCommandArgs.Flag.WITHCOORD)) { + command = new RedisCommand>>("GEORADIUS_RO", postitionDecoder); + params.add("WITHCOORD"); + } else { + MultiDecoder>> distanceDecoder = new ListMultiDecoder2(new GeoResultsDecoder(within.getRadius().getMetric()), new GeoDistanceDecoder()); + command = new RedisCommand>>("GEORADIUS_RO", distanceDecoder); + params.add("WITHDIST"); + } + + if (args.getLimit() != null) { + params.add("COUNT"); + params.add(args.getLimit()); + } + if (args.getSortDirection() != null) { + params.add(args.getSortDirection().name()); + } + + return read(key, ByteArrayCodec.INSTANCE, command, params.toArray()); + } + + private String getAbbreviation(Metric metric) { + if (ObjectUtils.nullSafeEquals(Metrics.NEUTRAL, metric)) { + return DistanceUnit.METERS.getAbbreviation(); + } + return metric.getAbbreviation(); + } + + @Override + public GeoResults> geoRadiusByMember(byte[] key, byte[] member, double radius) { + return geoRadiusByMember(key, member, new Distance(radius, DistanceUnit.METERS)); + } + + private static final RedisCommand>> GEORADIUSBYMEMBER = new RedisCommand>>("GEORADIUSBYMEMBER_RO", new GeoResultsDecoder()); + + @Override + public GeoResults> geoRadiusByMember(byte[] key, byte[] member, Distance radius) { + return read(key, ByteArrayCodec.INSTANCE, GEORADIUSBYMEMBER, key, member, radius.getValue(), getAbbreviation(radius.getMetric())); + } + + @Override + public GeoResults> geoRadiusByMember(byte[] key, byte[] member, Distance radius, + GeoRadiusCommandArgs args) { + List params = new ArrayList(); + params.add(key); + params.add(member); + params.add(radius.getValue()); + params.add(getAbbreviation(radius.getMetric())); + + RedisCommand>> command; + if (args.getFlags().contains(GeoRadiusCommandArgs.Flag.WITHCOORD)) { + command = new RedisCommand>>("GEORADIUSBYMEMBER_RO", postitionDecoder); + params.add("WITHCOORD"); + } else { + MultiDecoder>> distanceDecoder = new ListMultiDecoder2(new GeoResultsDecoder(radius.getMetric()), new GeoDistanceDecoder()); + command = new RedisCommand>>("GEORADIUSBYMEMBER_RO", distanceDecoder); + params.add("WITHDIST"); + } + + if (args.getLimit() != null) { + params.add("COUNT"); + params.add(args.getLimit()); + } + if (args.getSortDirection() != null) { + params.add(args.getSortDirection().name()); + } + + return read(key, ByteArrayCodec.INSTANCE, command, params.toArray()); + } + + @Override + public Long geoRemove(byte[] key, byte[]... members) { + return zRem(key, members); + } + + @Override + public GeoResults> geoSearch(byte[] key, GeoReference reference, GeoShape predicate, GeoSearchCommandArgs args) { + Assert.notNull(args, "Args must not be null!"); + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(predicate, "Shape must not be null!"); + Assert.notNull(reference, "Reference must not be null!"); + + List commandParams = new ArrayList<>(); + commandParams.add(key); + + if (reference instanceof GeoReference.GeoCoordinateReference) { + GeoReference.GeoCoordinateReference ref = (GeoReference.GeoCoordinateReference) reference; + commandParams.add("FROMLONLAT"); + commandParams.add(convert(ref.getLongitude())); + commandParams.add(convert(ref.getLatitude())); + } else if (reference instanceof GeoReference.GeoMemberReference) { + GeoReference.GeoMemberReference ref = (GeoReference.GeoMemberReference) reference; + commandParams.add("FROMMEMBER"); + commandParams.add(encode(ref.getMember())); + } + + if (predicate instanceof RadiusShape) { + commandParams.add("BYRADIUS"); + RadiusShape shape = (RadiusShape) predicate; + commandParams.add(shape.getRadius().getValue()); + commandParams.add(convert(shape.getMetric()).getAbbreviation()); + } else if (predicate instanceof BoxShape) { + BoxShape shape = (BoxShape) predicate; + commandParams.add("BYBOX"); + commandParams.add(shape.getBoundingBox().getWidth().getValue()); + commandParams.add(shape.getBoundingBox().getHeight().getValue()); + commandParams.add(convert(shape.getMetric()).getAbbreviation()); + } + + if (args.hasSortDirection()) { + commandParams.add(args.getSortDirection()); + } + if (args.getLimit() != null) { + commandParams.add("COUNT"); + commandParams.add(args.getLimit()); + if (args.hasAnyLimit()) { + commandParams.add("ANY"); + } + } + RedisCommand>> cmd; + if (args.getFlags().contains(GeoRadiusCommandArgs.Flag.WITHCOORD)) { + cmd = new RedisCommand<>("GEOSEARCH", postitionDecoder); + commandParams.add("WITHCOORD"); + } else { + MultiDecoder>> distanceDecoder = new ListMultiDecoder2(new GeoResultsDecoder(predicate.getMetric()), new GeoDistanceDecoder()); + cmd = new RedisCommand<>("GEOSEARCH", distanceDecoder); + commandParams.add("WITHDIST"); + } + + return read(key, ByteArrayCodec.INSTANCE, cmd, commandParams.toArray()); + } + + @Override + public Long geoSearchStore(byte[] destKey, byte[] key, GeoReference reference, GeoShape predicate, GeoSearchStoreCommandArgs args) { + Assert.notNull(args, "Args must not be null!"); + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(destKey, "DestKey must not be null!"); + Assert.notNull(predicate, "Shape must not be null!"); + Assert.notNull(reference, "Reference must not be null!"); + + List commandParams = new ArrayList<>(); + commandParams.add(destKey); + commandParams.add(key); + + if (reference instanceof GeoReference.GeoCoordinateReference) { + GeoReference.GeoCoordinateReference ref = (GeoReference.GeoCoordinateReference) reference; + commandParams.add("FROMLONLAT"); + commandParams.add(convert(ref.getLongitude())); + commandParams.add(convert(ref.getLatitude())); + } else if (reference instanceof GeoReference.GeoMemberReference) { + GeoReference.GeoMemberReference ref = (GeoReference.GeoMemberReference) reference; + commandParams.add("FROMMEMBER"); + commandParams.add(encode(ref.getMember())); + } + + if (predicate instanceof RadiusShape) { + RadiusShape shape = (RadiusShape) predicate; + commandParams.add("BYRADIUS"); + commandParams.add(shape.getRadius().getValue()); + commandParams.add(convert(shape.getMetric()).getAbbreviation()); + } else if (predicate instanceof BoxShape) { + BoxShape shape = (BoxShape) predicate; + commandParams.add("BYBOX"); + commandParams.add(shape.getBoundingBox().getWidth().getValue()); + commandParams.add(shape.getBoundingBox().getHeight().getValue()); + commandParams.add(convert(shape.getMetric()).getAbbreviation()); + } + + if (args.hasSortDirection()) { + commandParams.add(args.getSortDirection()); + } + if (args.getLimit() != null) { + commandParams.add("COUNT"); + commandParams.add(args.getLimit()); + if (args.hasAnyLimit()) { + commandParams.add("ANY"); + } + } + if (args.isStoreDistance()) { + commandParams.add("STOREDIST"); + } + + return write(key, LongCodec.INSTANCE, RedisCommands.GEOSEARCHSTORE_STORE, commandParams.toArray()); + } + + private Metric convert(Metric metric) { + if (metric == Metrics.NEUTRAL) { + return DistanceUnit.METERS; + } + return metric; + } + + private ByteBuf encode(Object value) { + return executorService.encode(ByteArrayCodec.INSTANCE, value); + } + private static final RedisCommand PFADD = new RedisCommand("PFADD"); + + @Override + public Long pfAdd(byte[] key, byte[]... values) { + List params = new ArrayList(values.length + 1); + params.add(key); + for (byte[] member : values) { + params.add(member); + } + + return write(key, StringCodec.INSTANCE, PFADD, params.toArray()); + } + + @Override + public Long pfCount(byte[]... keys) { + Assert.notEmpty(keys, "PFCOUNT requires at least one non 'null' key."); + Assert.noNullElements(keys, "Keys for PFOUNT must not contain 'null'."); + + return write(keys[0], StringCodec.INSTANCE, RedisCommands.PFCOUNT, Arrays.asList(keys).toArray()); + } + + @Override + public void pfMerge(byte[] destinationKey, byte[]... sourceKeys) { + List args = new ArrayList(sourceKeys.length + 1); + args.add(destinationKey); + args.addAll(Arrays.asList(sourceKeys)); + write(destinationKey, StringCodec.INSTANCE, RedisCommands.PFMERGE, args.toArray()); + } + + private static final RedisCommand HSTRLEN = new RedisCommand("HSTRLEN"); + + @Override + public Long hStrLen(byte[] key, byte[] field) { + return read(key, StringCodec.INSTANCE, HSTRLEN, key, field); + } + + @Override + public RedisStreamCommands streamCommands() { + return new RedissonStreamCommands(this); + } + + private static final RedisStrictCommand> BITFIELD = new RedisStrictCommand<>("BITFIELD", new ObjectListReplayDecoder<>()); + + @Override + public List bitField(byte[] key, BitFieldSubCommands subCommands) { + List params = new ArrayList<>(); + params.add(key); + + boolean writeOp = false; + for (BitFieldSubCommands.BitFieldSubCommand subCommand : subCommands) { + String size = "u"; + if (subCommand.getType().isSigned()) { + size = "i"; + } + size += subCommand.getType().getBits(); + + String offset = "#"; + if (subCommand.getOffset().isZeroBased()) { + offset = ""; + } + offset += subCommand.getOffset().getValue(); + + if (subCommand instanceof BitFieldSubCommands.BitFieldGet) { + params.add("GET"); + params.add(size); + params.add(offset); + } else if (subCommand instanceof BitFieldSubCommands.BitFieldSet) { + writeOp = true; + params.add("SET"); + params.add(size); + params.add(offset); + params.add(((BitFieldSubCommands.BitFieldSet) subCommand).getValue()); + } else if (subCommand instanceof BitFieldSubCommands.BitFieldIncrBy) { + writeOp = true; + params.add("INCRBY"); + params.add(size); + params.add(offset); + params.add(((BitFieldSubCommands.BitFieldIncrBy) subCommand).getValue()); + + BitFieldSubCommands.BitFieldIncrBy.Overflow overflow = ((BitFieldSubCommands.BitFieldIncrBy) subCommand).getOverflow(); + if (overflow != null) { + params.add("OVERFLOW"); + params.add(overflow); + } + } + } + + if (writeOp) { + return write(key, StringCodec.INSTANCE, BITFIELD, params.toArray()); + } + return read(key, StringCodec.INSTANCE, BITFIELD, params.toArray()); + } + + @Override + public Long exists(byte[]... keys) { + return read(keys[0], StringCodec.INSTANCE, RedisCommands.EXISTS_LONG, Arrays.asList(keys).toArray()); + } + + @Override + public Long touch(byte[]... keys) { + return read(keys[0], StringCodec.INSTANCE, RedisCommands.TOUCH_LONG, Arrays.asList(keys).toArray()); + } + + private static final RedisStrictCommand OBJECT_ENCODING = new RedisStrictCommand("OBJECT", "ENCODING", new Convertor() { + @Override + public ValueEncoding convert(Object obj) { + return ValueEncoding.of((String) obj); + } + }); + + @Override + public ValueEncoding encodingOf(byte[] key) { + Assert.notNull(key, "Key must not be null!"); + return read(key, StringCodec.INSTANCE, OBJECT_ENCODING, key); + } + + private static final RedisStrictCommand OBJECT_IDLETIME = new RedisStrictCommand<>("OBJECT", "IDLETIME", new Convertor() { + @Override + public Duration convert(Object obj) { + return Duration.ofSeconds((Long)obj); + } + }); + + @Override + public Duration idletime(byte[] key) { + Assert.notNull(key, "Key must not be null!"); + return read(key, StringCodec.INSTANCE, OBJECT_IDLETIME, key); + } + + private static final RedisStrictCommand OBJECT_REFCOUNT = new RedisStrictCommand("OBJECT", "REFCOUNT"); + + @Override + public Long refcount(byte[] key) { + Assert.notNull(key, "Key must not be null!"); + return read(key, StringCodec.INSTANCE, OBJECT_REFCOUNT, key); + } + + private static final RedisStrictCommand BITPOS = new RedisStrictCommand<>("BITPOS"); + + @Override + public Long bitPos(byte[] key, boolean bit, org.springframework.data.domain.Range range) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null! Use Range.unbounded() instead."); + + List params = new ArrayList<>(); + params.add(key); + if (bit) { + params.add(1); + } else { + params.add(0); + } + if (range.getLowerBound().isBounded()) { + params.add(range.getLowerBound().getValue().get()); + if (range.getUpperBound().isBounded()) { + params.add(range.getUpperBound().getValue().get()); + } + } + + return read(key, StringCodec.INSTANCE, BITPOS, params.toArray()); + } + + @Override + public void restore(byte[] key, long ttlInMillis, byte[] serializedValue, boolean replace) { + if (replace) { + write(key, StringCodec.INSTANCE, RedisCommands.RESTORE, key, ttlInMillis, serializedValue, "REPLACE"); + return; + } + restore(key, ttlInMillis, serializedValue); + } + + @Override + public org.springframework.data.redis.connection.RedisCommands commands() { + return this; + } + + @Override + public RedisGeoCommands geoCommands() { + return this; + } + + @Override + public RedisHashCommands hashCommands() { + return this; + } + + @Override + public RedisHyperLogLogCommands hyperLogLogCommands() { + return this; + } + + @Override + public RedisKeyCommands keyCommands() { + return this; + } + + @Override + public RedisListCommands listCommands() { + return this; + } + + @Override + public RedisSetCommands setCommands() { + return null; + } + + @Override + public RedisScriptingCommands scriptingCommands() { + return this; + } + + @Override + public RedisServerCommands serverCommands() { + return this; + } + + @Override + public RedisStringCommands stringCommands() { + return this; + } + + @Override + public RedisZSetCommands zSetCommands() { + return this; + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnectionFactory.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnectionFactory.java new file mode 100644 index 000000000..0cf559901 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonConnectionFactory.java @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.redisson.Redisson; +import org.redisson.RedissonKeys; +import org.redisson.RedissonReactive; +import org.redisson.api.RedissonClient; +import org.redisson.client.RedisClient; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.config.Config; +import org.redisson.connection.SentinelConnectionManager; +import org.redisson.reactive.CommandReactiveService; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.redis.ExceptionTranslationStrategy; +import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; +import org.springframework.data.redis.connection.ReactiveRedisClusterConnection; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.connection.RedisClusterConnection; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisSentinelConnection; + +/** + * Redisson based connection factory + * + * @author Nikita Koksharov + * + */ +public class RedissonConnectionFactory implements RedisConnectionFactory, + ReactiveRedisConnectionFactory, InitializingBean, DisposableBean { + + private final static Log log = LogFactory.getLog(RedissonConnectionFactory.class); + + public static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = + new PassThroughExceptionTranslationStrategy(new RedissonExceptionConverter()); + + private Config config; + private RedissonClient redisson; + private boolean hasOwnRedisson; + + /** + * Creates factory with default Redisson configuration + */ + public RedissonConnectionFactory() { + this(Redisson.create()); + hasOwnRedisson = true; + } + + /** + * Creates factory with defined Redisson instance + * + * @param redisson - Redisson instance + */ + public RedissonConnectionFactory(RedissonClient redisson) { + this.redisson = redisson; + } + + /** + * Creates factory with defined Redisson config + * + * @param config - Redisson config + */ + public RedissonConnectionFactory(Config config) { + super(); + this.config = config; + hasOwnRedisson = true; + } + + @Override + public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + return EXCEPTION_TRANSLATION.translate(ex); + } + + @Override + public void destroy() throws Exception { + if (hasOwnRedisson) { + redisson.shutdown(); + } + } + + @Override + public void afterPropertiesSet() throws Exception { + if (config != null) { + redisson = Redisson.create(config); + } + } + + @Override + public RedisConnection getConnection() { + if (redisson.getConfig().isClusterConfig()) { + return new RedissonClusterConnection(redisson); + } + return new RedissonConnection(redisson); + } + + @Override + public RedisClusterConnection getClusterConnection() { + if (!redisson.getConfig().isClusterConfig()) { + throw new InvalidDataAccessResourceUsageException("Redisson is not in Cluster mode"); + } + return new RedissonClusterConnection(redisson); + } + + @Override + public boolean getConvertPipelineAndTxResults() { + return true; + } + + @Override + public RedisSentinelConnection getSentinelConnection() { + if (!redisson.getConfig().isSentinelConfig()) { + throw new InvalidDataAccessResourceUsageException("Redisson is not in Sentinel mode"); + } + + SentinelConnectionManager manager = (SentinelConnectionManager)(((Redisson)redisson).getConnectionManager()); + for (RedisClient client : manager.getSentinels()) { + org.redisson.client.RedisConnection connection = client.connect(); + try { + String res = connection.sync(RedisCommands.PING); + if ("pong".equalsIgnoreCase(res)) { + return new RedissonSentinelConnection(connection); + } + } catch (Exception e) { + log.warn("Can't connect to " + client, e); + connection.closeAsync(); + } + } + + throw new InvalidDataAccessResourceUsageException("Sentinels are not found"); + } + + @Override + public ReactiveRedisConnection getReactiveConnection() { + if (redisson.getConfig().isClusterConfig()) { + return new RedissonReactiveRedisClusterConnection(((RedissonReactive)redisson.reactive()).getCommandExecutor()); + } + + return new RedissonReactiveRedisConnection(((RedissonReactive)redisson.reactive()).getCommandExecutor()); + } + + @Override + public ReactiveRedisClusterConnection getReactiveClusterConnection() { + if (!redisson.getConfig().isClusterConfig()) { + throw new InvalidDataAccessResourceUsageException("Redisson is not in Cluster mode"); + } + + return new RedissonReactiveRedisClusterConnection(((RedissonReactive)redisson.reactive()).getCommandExecutor()); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonExceptionConverter.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonExceptionConverter.java new file mode 100644 index 000000000..7b8c2cb12 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonExceptionConverter.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.RedisConnectionException; +import org.redisson.client.RedisException; +import org.redisson.client.RedisRedirectException; +import org.redisson.client.RedisTimeoutException; +import org.springframework.core.convert.converter.Converter; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.data.redis.ClusterRedirectException; +import org.springframework.data.redis.RedisConnectionFailureException; + +/** + * Converts Redisson exceptions to Spring compatible + * + * @author Nikita Koksharov + * + */ +public class RedissonExceptionConverter implements Converter { + + @Override + public DataAccessException convert(Exception source) { + if (source instanceof RedisConnectionException) { + return new RedisConnectionFailureException(source.getMessage(), source); + } + if (source instanceof RedisRedirectException) { + RedisRedirectException ex = (RedisRedirectException) source; + return new ClusterRedirectException(ex.getSlot(), ex.getUrl().getHost(), ex.getUrl().getPort(), source); + } + + if (source instanceof RedisTimeoutException) { + return new QueryTimeoutException(source.getMessage(), source); + } + + if (source instanceof RedisException) { + return new InvalidDataAccessApiUsageException(source.getMessage(), source); + } + + if (source instanceof DataAccessException) { + return (DataAccessException) source; + } + + return null; + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterGeoCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterGeoCommands.java new file mode 100644 index 000000000..cccad0ddc --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterGeoCommands.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveClusterGeoCommands; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveClusterGeoCommands extends RedissonReactiveGeoCommands implements ReactiveClusterGeoCommands { + + RedissonReactiveClusterGeoCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterHashCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterHashCommands.java new file mode 100644 index 000000000..c7e82dbcc --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterHashCommands.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveClusterHashCommands; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveClusterHashCommands extends RedissonReactiveHashCommands implements ReactiveClusterHashCommands { + + RedissonReactiveClusterHashCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterHyperLogLogCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterHyperLogLogCommands.java new file mode 100644 index 000000000..dbb969f63 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterHyperLogLogCommands.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveClusterHyperLogLogCommands; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveClusterHyperLogLogCommands extends RedissonReactiveHyperLogLogCommands implements ReactiveClusterHyperLogLogCommands { + + RedissonReactiveClusterHyperLogLogCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterKeyCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterKeyCommands.java new file mode 100644 index 000000000..c6a8353d9 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterKeyCommands.java @@ -0,0 +1,143 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.connection.MasterSlaveEntry; +import org.redisson.misc.CompletableFutureWrapper; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveClusterKeyCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.util.Assert; + +import io.netty.util.CharsetUtil; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveClusterKeyCommands extends RedissonReactiveKeyCommands implements ReactiveClusterKeyCommands { + + public RedissonReactiveClusterKeyCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + @Override + public Mono> keys(RedisClusterNode node, ByteBuffer pattern) { + Mono> m = executorService.reactive(() -> { + List>> futures = executorService.readAllAsync(StringCodec.INSTANCE, RedisCommands.KEYS, toByteArray(pattern)); + CompletableFuture ff = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + CompletableFuture> future = ff.thenApply(r -> { + return futures.stream().flatMap(f -> f.getNow(new ArrayList<>()).stream()).collect(Collectors.toList()); + }).toCompletableFuture(); + return new CompletableFutureWrapper<>(future); + }); + return m.map(v -> v.stream().map(t -> ByteBuffer.wrap(t.getBytes(CharsetUtil.UTF_8))).collect(Collectors.toList())); + } + + @Override + public Mono randomKey(RedisClusterNode node) { + MasterSlaveEntry entry = getEntry(node); + Mono m = executorService.reactive(() -> { + return executorService.readRandomAsync(entry, ByteArrayCodec.INSTANCE, RedisCommands.RANDOM_KEY); + }); + return m.map(v -> ByteBuffer.wrap(v)); + } + + @Override + public Flux> rename(Publisher commands) { + + return execute(commands, command -> { + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getNewKey(), "New name must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] newKeyBuf = toByteArray(command.getNewKey()); + + if (executorService.getConnectionManager().calcSlot(keyBuf) == executorService.getConnectionManager().calcSlot(newKeyBuf)) { + return super.rename(commands); + } + + return read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.DUMP, keyBuf) + .filter(Objects::nonNull) + .zipWith( + Mono.defer(() -> pTtl(command.getKey()) + .filter(Objects::nonNull) + .map(ttl -> Math.max(0, ttl)) + .switchIfEmpty(Mono.just(0L)) + ) + ) + .flatMap(valueAndTtl -> { + return write(newKeyBuf, StringCodec.INSTANCE, RedisCommands.RESTORE, newKeyBuf, valueAndTtl.getT2(), valueAndTtl.getT1()); + }) + .thenReturn(new BooleanResponse<>(command, true)) + .doOnSuccess((ignored) -> del(command.getKey())); + }); + } + + @Override + public Flux> renameNX(Publisher commands) { + return execute(commands, command -> { + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getNewKey(), "New name must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] newKeyBuf = toByteArray(command.getNewKey()); + + if (executorService.getConnectionManager().calcSlot(keyBuf) == executorService.getConnectionManager().calcSlot(newKeyBuf)) { + return super.renameNX(commands); + } + + return exists(command.getNewKey()) + .zipWith(read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.DUMP, keyBuf)) + .filter(newKeyExistsAndDump -> !newKeyExistsAndDump.getT1() && Objects.nonNull(newKeyExistsAndDump.getT2())) + .map(Tuple2::getT2) + .zipWhen(value -> + pTtl(command.getKey()) + .filter(Objects::nonNull) + .map(ttl -> Math.max(0, ttl)) + .switchIfEmpty(Mono.just(0L)) + + ) + .flatMap(valueAndTtl -> write(newKeyBuf, StringCodec.INSTANCE, RedisCommands.RESTORE, newKeyBuf, valueAndTtl.getT2(), valueAndTtl.getT1()) + .then(Mono.just(true))) + .switchIfEmpty(Mono.just(false)) + .doOnSuccess(didRename -> { + if (didRename) { + del(command.getKey()); + } + }) + .map(didRename -> new BooleanResponse<>(command, didRename)); + }); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterListCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterListCommands.java new file mode 100644 index 000000000..3b9e9f9c1 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterListCommands.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveClusterListCommands; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveClusterListCommands extends RedissonReactiveListCommands implements ReactiveClusterListCommands { + + RedissonReactiveClusterListCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterNumberCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterNumberCommands.java new file mode 100644 index 000000000..2879d861e --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterNumberCommands.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveClusterNumberCommands; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveClusterNumberCommands extends RedissonReactiveNumberCommands implements ReactiveClusterNumberCommands { + + public RedissonReactiveClusterNumberCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterServerCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterServerCommands.java new file mode 100644 index 000000000..ed65ff385 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterServerCommands.java @@ -0,0 +1,153 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.util.List; +import java.util.Properties; + +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.connection.MasterSlaveEntry; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveClusterServerCommands; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisServerCommands; +import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter; +import org.springframework.data.redis.core.types.RedisClientInfo; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveClusterServerCommands extends RedissonReactiveServerCommands implements ReactiveClusterServerCommands { + + RedissonReactiveClusterServerCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + @Override + public Mono bgReWriteAof(RedisClusterNode node) { + return execute(node, BGREWRITEAOF); + } + + @Override + public Mono bgSave(RedisClusterNode node) { + return execute(node, BGSAVE); + } + + @Override + public Mono lastSave(RedisClusterNode node) { + return execute(node, RedisCommands.LASTSAVE); + } + + @Override + public Mono save(RedisClusterNode node) { + return execute(node, SAVE); + } + + @Override + public Mono dbSize(RedisClusterNode node) { + return execute(node, RedisCommands.DBSIZE); + } + + private static final RedisStrictCommand FLUSHDB = new RedisStrictCommand("FLUSHDB"); + + @Override + public Mono flushDb(RedisClusterNode node) { + return execute(node, FLUSHDB); + } + + private static final RedisStrictCommand FLUSHALL = new RedisStrictCommand("FLUSHALL"); + + @Override + public Mono flushAll(RedisClusterNode node) { + return execute(node, FLUSHALL); + } + + @Override + public Mono flushDb(RedisClusterNode node, RedisServerCommands.FlushOption option) { + if (option == RedisServerCommands.FlushOption.ASYNC) { + return execute(node, FLUSHDB, option.toString()); + } + return execute(node, FLUSHDB); + } + + @Override + public Mono flushAll(RedisClusterNode node, RedisServerCommands.FlushOption option) { + if (option == RedisServerCommands.FlushOption.ASYNC) { + return execute(node, FLUSHALL, option.toString()); + } + return execute(node, FLUSHALL); + } + + @Override + public Mono info() { + return read(null, StringCodec.INSTANCE, INFO_DEFAULT); + } + + @Override + public Mono info(String section) { + return read(null, StringCodec.INSTANCE, INFO, section); + } + + + @Override + public Mono info(RedisClusterNode node) { + return execute(node, INFO_DEFAULT); + } + + @Override + public Mono info(RedisClusterNode node, String section) { + return execute(node, INFO, section); + } + + @Override + public Mono getConfig(RedisClusterNode node, String pattern) { + return execute(node, CONFIG_GET, pattern); + } + + @Override + public Mono setConfig(RedisClusterNode node, String param, String value) { + return execute(node, CONFIG_SET, param, value); + } + + @Override + public Mono resetConfigStats(RedisClusterNode node) { + return execute(node, CONFIG_RESETSTAT); + } + + @Override + public Mono time(RedisClusterNode node) { + return execute(node, TIME); + } + + private static final StringToRedisClientInfoConverter CONVERTER = new StringToRedisClientInfoConverter(); + + @Override + public Flux getClientList(RedisClusterNode node) { + MasterSlaveEntry entry = getEntry(node); + Mono> m = executorService.reactive(() -> { + return executorService.readAsync(entry, StringCodec.INSTANCE, RedisCommands.CLIENT_LIST); + }); + return m.flatMapMany(s -> Flux.fromIterable(CONVERTER.convert(s.toArray(new String[s.size()])))); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterSetCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterSetCommands.java new file mode 100644 index 000000000..570392dcd --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterSetCommands.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveClusterSetCommands; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveClusterSetCommands extends RedissonReactiveSetCommands implements ReactiveClusterSetCommands { + + RedissonReactiveClusterSetCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterStreamCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterStreamCommands.java new file mode 100644 index 000000000..9afc81d8a --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterStreamCommands.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveClusterStreamCommands; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveClusterStreamCommands extends RedissonReactiveStreamCommands implements ReactiveClusterStreamCommands { + + RedissonReactiveClusterStreamCommands(CommandReactiveExecutor executorService) { + super(executorService); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterStringCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterStringCommands.java new file mode 100644 index 000000000..2421e3f50 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterStringCommands.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveClusterStringCommands; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveClusterStringCommands extends RedissonReactiveStringCommands implements ReactiveClusterStringCommands { + + RedissonReactiveClusterStringCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterZSetCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterZSetCommands.java new file mode 100644 index 000000000..e423a299d --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveClusterZSetCommands.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.reactivestreams.Publisher; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveClusterZSetCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import reactor.core.publisher.Flux; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveClusterZSetCommands extends RedissonReactiveZSetCommands implements ReactiveClusterZSetCommands { + + RedissonReactiveClusterZSetCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveGeoCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveGeoCommands.java new file mode 100644 index 000000000..4135ad012 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveGeoCommands.java @@ -0,0 +1,372 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import io.netty.buffer.ByteBuf; +import org.reactivestreams.Publisher; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.DoubleCodec; +import org.redisson.client.codec.LongCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.decoder.*; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.geo.*; +import org.springframework.data.redis.connection.ReactiveGeoCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; +import org.springframework.data.redis.domain.geo.BoxShape; +import org.springframework.data.redis.domain.geo.GeoReference; +import org.springframework.data.redis.domain.geo.RadiusShape; +import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveGeoCommands extends RedissonBaseReactive implements ReactiveGeoCommands { + + RedissonReactiveGeoCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + @Override + public Flux> geoAdd(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getGeoLocations(), "Locations must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + List args = new ArrayList(); + args.add(keyBuf); + for (GeoLocation location : command.getGeoLocations()) { + args.add(location.getPoint().getX()); + args.add(location.getPoint().getY()); + args.add(toByteArray(location.getName())); + } + + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.GEOADD, args.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> geoDist(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getFrom(), "From member must not be null!"); + Assert.notNull(command.getTo(), "To member must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] fromBuf = toByteArray(command.getFrom()); + byte[] toBuf = toByteArray(command.getTo()); + + Metric metric = RedisGeoCommands.DistanceUnit.METERS; + if (command.getMetric().isPresent()) { + metric = command.getMetric().get(); + } + + Mono m = write(keyBuf, DoubleCodec.INSTANCE, new RedisCommand("GEODIST", new DistanceConvertor(metric)), + keyBuf, fromBuf, toBuf, metric.getAbbreviation()); + return m.map(v -> new CommandResponse<>(command, v)); + }); + } + + private static final RedisCommand> GEOHASH = new RedisCommand>("GEOHASH", new ObjectListReplayDecoder()); + + @Override + public Flux> geoHash(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getMembers(), "Members must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + List args = new ArrayList(command.getMembers().size() + 1); + args.add(keyBuf); + args.addAll(command.getMembers().stream().map(buf -> toByteArray(buf)).collect(Collectors.toList())); + + Mono> m = read(keyBuf, StringCodec.INSTANCE, GEOHASH, args.toArray()); + return m.map(v -> new MultiValueResponse<>(command, v)); + }); + } + + private final MultiDecoder> geoDecoder = new ListMultiDecoder2(new ObjectListReplayDecoder2(), new PointDecoder()); + + @Override + public Flux> geoPos(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getMembers(), "Members must not be null!"); + + RedisCommand> cmd = new RedisCommand>("GEOPOS", geoDecoder); + + byte[] keyBuf = toByteArray(command.getKey()); + List args = new ArrayList(command.getMembers().size() + 1); + args.add(keyBuf); + args.addAll(command.getMembers().stream().map(buf -> toByteArray(buf)).collect(Collectors.toList())); + + Mono> m = read(keyBuf, StringCodec.INSTANCE, cmd, args.toArray()); + return m.map(v -> new MultiValueResponse<>(command, v)); + }); + } + + private final MultiDecoder>> postitionDecoder = new ListMultiDecoder2(new ByteBufferGeoResultsDecoder(), new CodecDecoder(), new PointDecoder(), new ObjectListReplayDecoder()); + + @Override + public Flux>>>> geoRadius( + Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getPoint(), "Point must not be null!"); + Assert.notNull(command.getDistance(), "Distance must not be null!"); + + GeoRadiusCommandArgs args = command.getArgs().orElse(GeoRadiusCommandArgs.newGeoRadiusArgs()); + byte[] keyBuf = toByteArray(command.getKey()); + + List params = new ArrayList(); + params.add(keyBuf); + params.add(BigDecimal.valueOf(command.getPoint().getX()).toPlainString()); + params.add(BigDecimal.valueOf(command.getPoint().getY()).toPlainString()); + params.add(command.getDistance().getValue()); + params.add(command.getDistance().getMetric().getAbbreviation()); + + RedisCommand>> cmd; + if (args.getFlags().contains(GeoRadiusCommandArgs.Flag.WITHCOORD)) { + cmd = new RedisCommand<>("GEORADIUS_RO", postitionDecoder); + params.add("WITHCOORD"); + } else { + MultiDecoder>> distanceDecoder = new ListMultiDecoder2(new ByteBufferGeoResultsDecoder(command.getDistance().getMetric()), new GeoDistanceDecoder()); + cmd = new RedisCommand<>("GEORADIUS_RO", distanceDecoder); + params.add("WITHDIST"); + } + + if (args.getLimit() != null) { + params.add("COUNT"); + params.add(args.getLimit()); + } + if (args.getSortDirection() != null) { + params.add(args.getSortDirection().name()); + } + + Mono>> m = read(keyBuf, ByteArrayCodec.INSTANCE, cmd, params.toArray()); + return m.map(v -> new CommandResponse<>(command, Flux.fromIterable(v.getContent()))); + }); + } + + @Override + public Flux>>>> geoRadiusByMember( + Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getMember(), "Member must not be null!"); + Assert.notNull(command.getDistance(), "Distance must not be null!"); + + GeoRadiusCommandArgs args = command.getArgs().orElse(GeoRadiusCommandArgs.newGeoRadiusArgs()); + byte[] keyBuf = toByteArray(command.getKey()); + byte[] memberBuf = toByteArray(command.getMember()); + + List params = new ArrayList(); + params.add(keyBuf); + params.add(memberBuf); + params.add(command.getDistance().getValue()); + params.add(command.getDistance().getMetric().getAbbreviation()); + + RedisCommand>> cmd; + if (args.getFlags().contains(GeoRadiusCommandArgs.Flag.WITHCOORD)) { + cmd = new RedisCommand<>("GEORADIUSBYMEMBER_RO", postitionDecoder); + params.add("WITHCOORD"); + } else { + MultiDecoder>> distanceDecoder = new ListMultiDecoder2(new ByteBufferGeoResultsDecoder(command.getDistance().getMetric()), new GeoDistanceDecoder()); + cmd = new RedisCommand<>("GEORADIUSBYMEMBER_RO", distanceDecoder); + params.add("WITHDIST"); + } + + if (args.getLimit() != null) { + params.add("COUNT"); + params.add(args.getLimit()); + } + if (args.getSortDirection() != null) { + params.add(args.getSortDirection().name()); + } + + Mono>> m = read(keyBuf, ByteArrayCodec.INSTANCE, cmd, params.toArray()); + return m.map(v -> new CommandResponse<>(command, Flux.fromIterable(v.getContent()))); + }); + } + + private String convert(double longitude) { + return BigDecimal.valueOf(longitude).toPlainString(); + } + + private ByteBuf encode(Object value) { + return executorService.encode(ByteArrayCodec.INSTANCE, value); + } + + @Override + public Flux>>>> geoSearch(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getArgs(), "Args must not be null!"); + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getShape(), "Shape must not be null!"); + Assert.notNull(command.getReference(), "Reference must not be null!"); + + List commandParams = new ArrayList<>(); + byte[] keyBuf = toByteArray(command.getKey()); + commandParams.add(keyBuf); + + if (command.getReference() instanceof GeoReference.GeoCoordinateReference) { + GeoReference.GeoCoordinateReference ref = (GeoReference.GeoCoordinateReference) command.getReference(); + commandParams.add("FROMLONLAT"); + commandParams.add(convert(ref.getLongitude())); + commandParams.add(convert(ref.getLatitude())); + } else if (command.getReference() instanceof GeoReference.GeoMemberReference) { + GeoReference.GeoMemberReference ref = (GeoReference.GeoMemberReference) command.getReference(); + commandParams.add("FROMMEMBER"); + commandParams.add(encode(ref.getMember())); + } + + if (command.getShape() instanceof RadiusShape) { + commandParams.add("BYRADIUS"); + RadiusShape shape = (RadiusShape) command.getShape(); + commandParams.add(shape.getRadius().getValue()); + commandParams.add(convert(shape.getMetric()).getAbbreviation()); + } else if (command.getShape() instanceof BoxShape) { + BoxShape shape = (BoxShape) command.getShape(); + commandParams.add("BYBOX"); + commandParams.add(shape.getBoundingBox().getWidth().getValue()); + commandParams.add(shape.getBoundingBox().getHeight().getValue()); + commandParams.add(convert(shape.getMetric()).getAbbreviation()); + } + + RedisGeoCommands.GeoSearchCommandArgs args = command.getArgs() + .orElse(RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()); + if (args.hasSortDirection()) { + commandParams.add(args.getSortDirection()); + } + if (args.getLimit() != null) { + commandParams.add("COUNT"); + commandParams.add(args.getLimit()); + if (args.hasAnyLimit()) { + commandParams.add("ANY"); + } + } + RedisCommand>> cmd; + if (args.getFlags().contains(GeoRadiusCommandArgs.Flag.WITHCOORD)) { + cmd = new RedisCommand<>("GEOSEARCH", postitionDecoder); + commandParams.add("WITHCOORD"); + } else { + MultiDecoder>> distanceDecoder = new ListMultiDecoder2(new ByteBufferGeoResultsDecoder(command.getShape().getMetric()), new GeoDistanceDecoder()); + cmd = new RedisCommand<>("GEOSEARCH", distanceDecoder); + commandParams.add("WITHDIST"); + } + + Mono>> m = read(keyBuf, ByteArrayCodec.INSTANCE, cmd, commandParams.toArray()); + return m.map(v -> new CommandResponse<>(command, Flux.fromIterable(v.getContent()))); + }); + } + + @Override + public Flux> geoSearchStore(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getArgs(), "Args must not be null!"); + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDestKey(), "DestKey must not be null!"); + Assert.notNull(command.getShape(), "Shape must not be null!"); + Assert.notNull(command.getReference(), "Reference must not be null!"); + + List commandParams = new ArrayList<>(); + byte[] destKeyBuf = toByteArray(command.getDestKey()); + commandParams.add(destKeyBuf); + byte[] keyBuf = toByteArray(command.getKey()); + commandParams.add(keyBuf); + + if (command.getReference() instanceof GeoReference.GeoCoordinateReference) { + GeoReference.GeoCoordinateReference ref = (GeoReference.GeoCoordinateReference) command.getReference(); + commandParams.add("FROMLONLAT"); + commandParams.add(convert(ref.getLongitude())); + commandParams.add(convert(ref.getLatitude())); + } else if (command.getReference() instanceof GeoReference.GeoMemberReference) { + GeoReference.GeoMemberReference ref = (GeoReference.GeoMemberReference) command.getReference(); + commandParams.add("FROMMEMBER"); + commandParams.add(encode(ref.getMember())); + } + + if (command.getShape() instanceof RadiusShape) { + RadiusShape shape = (RadiusShape) command.getShape(); + commandParams.add("BYRADIUS"); + commandParams.add(shape.getRadius().getValue()); + commandParams.add(convert(shape.getMetric()).getAbbreviation()); + } else if (command.getShape() instanceof BoxShape) { + BoxShape shape = (BoxShape) command.getShape(); + commandParams.add("BYBOX"); + commandParams.add(shape.getBoundingBox().getWidth().getValue()); + commandParams.add(shape.getBoundingBox().getHeight().getValue()); + commandParams.add(convert(shape.getMetric()).getAbbreviation()); + } + + RedisGeoCommands.GeoSearchStoreCommandArgs args = command.getArgs() + .orElse(RedisGeoCommands.GeoSearchStoreCommandArgs.newGeoSearchStoreArgs()); + if (args.hasSortDirection()) { + commandParams.add(args.getSortDirection()); + } + if (args.getLimit() != null) { + commandParams.add("COUNT"); + commandParams.add(args.getLimit()); + if (args.hasAnyLimit()) { + commandParams.add("ANY"); + } + } + if (args.isStoreDistance()) { + commandParams.add("STOREDIST"); + } + + Mono m = write(keyBuf, LongCodec.INSTANCE, RedisCommands.GEOSEARCHSTORE_STORE, commandParams.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private Metric convert(Metric metric) { + if (metric == Metrics.NEUTRAL) { + return RedisGeoCommands.DistanceUnit.METERS; + } + return metric; + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveHashCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveHashCommands.java new file mode 100644 index 000000000..27cfb916e --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveHashCommands.java @@ -0,0 +1,302 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.redisson.ScanResult; +import org.redisson.api.RFuture; +import org.redisson.client.RedisClient; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.Codec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.decoder.MapScanResult; +import org.redisson.client.protocol.decoder.MultiDecoder; +import org.redisson.reactive.CommandReactiveExecutor; +import org.redisson.reactive.MapReactiveIterator; +import org.springframework.data.redis.connection.ReactiveHashCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyScanCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveHashCommands extends RedissonBaseReactive implements ReactiveHashCommands { + + RedissonReactiveHashCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + private static final RedisCommand HMSET = new RedisCommand("HMSET"); + + @Override + public Flux> hSet(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getFieldValueMap(), "FieldValueMap must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + if (command.getFieldValueMap().size() == 1) { + Entry entry = command.getFieldValueMap().entrySet().iterator().next(); + byte[] mapKeyBuf = toByteArray(entry.getKey()); + byte[] mapValueBuf = toByteArray(entry.getValue()); + RedisCommand cmd = RedisCommands.HSETNX; + if (command.isUpsert()) { + cmd = RedisCommands.HSET; + } + Mono m = write(keyBuf, StringCodec.INSTANCE, cmd, keyBuf, mapKeyBuf, mapValueBuf); + return m.map(v -> new BooleanResponse<>(command, v)); + } else { + List params = new ArrayList(command.getFieldValueMap().size()*2 + 1); + params.add(keyBuf); + for (Entry entry : command.getFieldValueMap().entrySet()) { + params.add(toByteArray(entry.getKey())); + params.add(toByteArray(entry.getValue())); + } + + Mono m = write(keyBuf, StringCodec.INSTANCE, HMSET, params.toArray()); + return m.map(v -> new BooleanResponse<>(command, true)); + } + }); + } + + private static final RedisCommand> HMGET = new RedisCommand>("HMGET", new MultiDecoder>() { + + @Override + public List decode(List parts, State state) { + List list = parts.stream().filter(e -> e != null).collect(Collectors.toList()); + if (list.isEmpty()) { + return null; + } + return parts; + } + + }); + + @Override + public Flux> hMGet(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getFields(), "Fields must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + List args = new ArrayList(command.getFields().size() + 1); + args.add(keyBuf); + args.addAll(command.getFields().stream().map(buf -> toByteArray(buf)).collect(Collectors.toList())); + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, HMGET, args.toArray()); + return m.map(v -> { + List values = v.stream().map(array -> { + if (array != null) { + return ByteBuffer.wrap(array); + } + return null; + }).collect(Collectors.toList()); + return new MultiValueResponse<>(command, values); + }); + }); + } + + @Override + public Flux> hExists(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getField(), "Field must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] fieldBuf = toByteArray(command.getField()); + + Mono m =read(keyBuf, StringCodec.INSTANCE, RedisCommands.HEXISTS, keyBuf, fieldBuf); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + @Override + public Flux> hDel(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getFields(), "Fields must not be null!"); + + List args = new ArrayList(command.getFields().size() + 1); + args.add(toByteArray(command.getKey())); + args.addAll(command.getFields().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + + Mono m = write((byte[])args.get(0), StringCodec.INSTANCE, RedisCommands.HDEL, args.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> hLen(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = read(keyBuf, StringCodec.INSTANCE, RedisCommands.HLEN_LONG, keyBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux>> hKeys(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.HKEYS, keyBuf); + return m.map(v -> new CommandResponse<>(command, Flux.fromIterable(v).map(e -> ByteBuffer.wrap(e)))); + }); + } + + @Override + public Flux>> hVals(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.HVALS, keyBuf); + return m.map(v -> new CommandResponse<>(command, Flux.fromIterable(v).map(e -> ByteBuffer.wrap(e)))); + }); + } + + @Override + public Flux>>> hGetAll( + Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.HGETALL, keyBuf); + Mono> f = m.map(v -> v.entrySet().stream().collect(Collectors.toMap(e -> ByteBuffer.wrap(e.getKey()), e -> ByteBuffer.wrap(e.getValue())))); + return f.map(v -> new CommandResponse<>(command, Flux.fromIterable(v.entrySet()))); + }); + } + + @Override + public Flux>>> hScan( + Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getOptions(), "ScanOptions must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Flux> flux = Flux.create(new MapReactiveIterator>(null, null, 0) { + @Override + public RFuture> scanIterator(RedisClient client, long nextIterPos) { + if (command.getOptions().getPattern() == null) { + return executorService.readAsync(client, keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.HSCAN, + keyBuf, nextIterPos, "COUNT", Optional.ofNullable(command.getOptions().getCount()).orElse(10L)); + } + + return executorService.readAsync(client, keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.HSCAN, + keyBuf, nextIterPos, "MATCH", command.getOptions().getPattern(), + "COUNT", Optional.ofNullable(command.getOptions().getCount()).orElse(10L)); + } + }); + Flux> f = flux.map(v -> Collections.singletonMap(ByteBuffer.wrap((byte[])v.getKey()), ByteBuffer.wrap((byte[])v.getValue())).entrySet().iterator().next()); + return Mono.just(new CommandResponse<>(command, f)); + }); + } + + private static final RedisCommand HSTRLEN = new RedisCommand("HSTRLEN"); + + @Override + public Flux> hStrLen(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getField(), "Field must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] fieldBuf = toByteArray(command.getField()); + + Mono m = read(keyBuf, StringCodec.INSTANCE, HSTRLEN, keyBuf, fieldBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux>> hRandField(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + Mono> m; + if (command.getCount() > 0) { + m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.HRANDFIELD_KEYS, keyBuf, command.getCount()); + } else { + m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.HRANDFIELD_KEYS, keyBuf); + } + return m.map(v -> new CommandResponse<>(command, Flux.fromIterable(v).map(e -> ByteBuffer.wrap(e)))); + }); + } + + @Override + public Flux>>> hRandFieldWithValues(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + Mono> m; + if (command.getCount() > 0) { + m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.HRANDFIELD, keyBuf, command.getCount()); + } else { + m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.HRANDFIELD, keyBuf); + } + + Mono> f = m.map(v -> v.entrySet().stream().collect(Collectors.toMap(e -> ByteBuffer.wrap(e.getKey()), e -> ByteBuffer.wrap(e.getValue())))); + return f.map(v -> new CommandResponse<>(command, Flux.fromIterable(v.entrySet()))); + }); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveHyperLogLogCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveHyperLogLogCommands.java new file mode 100644 index 000000000..d7d845b5d --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveHyperLogLogCommands.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveHyperLogLogCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveHyperLogLogCommands extends RedissonBaseReactive implements ReactiveHyperLogLogCommands { + + RedissonReactiveHyperLogLogCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + private static final RedisCommand PFADD = new RedisCommand("PFADD"); + + @Override + public Flux> pfAdd(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notEmpty(command.getValues(), "Values must not be empty!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + List params = new ArrayList(command.getValues().size() + 1); + params.add(keyBuf); + params.addAll(command.getValues().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + + Mono m = write(keyBuf, StringCodec.INSTANCE, PFADD, params.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> pfCount(Publisher commands) { + return execute(commands, command -> { + + Assert.notEmpty(command.getKeys(), "Keys must not be empty!"); + + Object[] args = command.getKeys().stream().map(v -> toByteArray(v)).toArray(); + + Mono m = write((byte[])args[0], StringCodec.INSTANCE, RedisCommands.PFCOUNT, args); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand PFMERGE = new RedisStrictCommand("PFMERGE"); + + @Override + public Flux> pfMerge(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Destination key must not be null!"); + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + List args = new ArrayList(command.getSourceKeys().size() + 1); + args.add(keyBuf); + args.addAll(command.getSourceKeys().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + Mono m = write(keyBuf, StringCodec.INSTANCE, PFMERGE, args.toArray()); + return m.map(v -> new BooleanResponse<>(command, true)); + }); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveKeyCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveKeyCommands.java new file mode 100644 index 000000000..7938322ab --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveKeyCommands.java @@ -0,0 +1,371 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.client.protocol.convertor.BooleanReplayConvertor; +import org.redisson.client.protocol.convertor.Convertor; +import org.redisson.reactive.CommandReactiveExecutor; +import org.redisson.reactive.RedissonKeysReactive; +import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ReactiveKeyCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.ValueEncoding; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveKeyCommands extends RedissonBaseReactive implements ReactiveKeyCommands { + + public RedissonReactiveKeyCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + @Override + public Flux> exists(Publisher keys) { + return execute(keys, key -> { + + Assert.notNull(key.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(key.getKey()); + Mono m = read(keyBuf, StringCodec.INSTANCE, RedisCommands.EXISTS, keyBuf); + return m.map(v -> new BooleanResponse<>(key, v)); + }); + } + + private static final RedisStrictCommand TYPE = new RedisStrictCommand("TYPE", new Convertor() { + @Override + public DataType convert(Object obj) { + return DataType.fromCode(obj.toString()); + } + }); + + @Override + public Flux> type(Publisher keys) { + return execute(keys, key -> { + + Assert.notNull(key.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(key.getKey()); + Mono m = read(keyBuf, StringCodec.INSTANCE, TYPE, keyBuf); + return m.map(v -> new CommandResponse<>(key, v)); + }); + } + + @Override + public Flux, Long>> touch(Publisher> keys) { + return execute(keys, coll -> { + + Assert.notNull(coll, "Collection must not be null!"); + + Object[] params = coll.stream().map(buf -> toByteArray(buf)).toArray(Object[]::new); + + Mono m = write(null, StringCodec.INSTANCE, RedisCommands.TOUCH_LONG, params); + return m.map(v -> new NumericResponse<>(coll, v)); + }); + } + + @Override + public Flux> keys(Publisher patterns) { + return execute(patterns, pattern -> { + + Assert.notNull(pattern, "Pattern must not be null!"); + + Mono> m = read(null, StringCodec.INSTANCE, RedisCommands.KEYS, toByteArray(pattern)); + return m.map(v -> { + List values = v.stream().map(t -> ByteBuffer.wrap(t.getBytes())).collect(Collectors.toList()); + return new MultiValueResponse<>(pattern, values); + }); + }); + } + + @Override + public Flux scan(ScanOptions options) { + RedissonKeysReactive reactive = new RedissonKeysReactive(executorService); + return reactive.getKeysByPattern(options.getPattern(), options.getCount().intValue()).map(t -> ByteBuffer.wrap(t.getBytes())); + } + + @Override + public Mono randomKey() { + return executorService.reactive(() -> { + return executorService.readRandomAsync(ByteArrayCodec.INSTANCE, RedisCommands.RANDOM_KEY); + }); + } + + static final RedisStrictCommand RENAME = new RedisStrictCommand("RENAME"); + + @Override + public Flux> rename(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getNewKey(), "New name must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] newKeyBuf = toByteArray(command.getNewKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, RENAME, keyBuf, newKeyBuf); + return m.map(v -> new BooleanResponse<>(command, true)); + }); + } + + @Override + public Flux> renameNX(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getNewKey(), "New name must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] newKeyBuf = toByteArray(command.getNewKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.RENAMENX, keyBuf, newKeyBuf); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + @Override + public Flux> del(Publisher keys) { + Flux s = Flux.from(keys); + return s.concatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.DEL, keyBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux, Long>> mDel(Publisher> keys) { + return execute(keys, coll -> { + + Assert.notNull(coll, "List must not be null!"); + + Object[] params = coll.stream().map(buf -> toByteArray(buf)).toArray(Object[]::new); + + Mono m = write(null, StringCodec.INSTANCE, RedisCommands.DEL, params); + return m.map(v -> new NumericResponse<>(coll, v)); + }); + } + + @Override + public Flux> unlink(Publisher keys) { + return execute(keys, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.UNLINK, keyBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux, Long>> mUnlink(Publisher> keys) { + return execute(keys, coll -> { + + Assert.notNull(coll, "List must not be null!"); + + Object[] params = coll.stream().map(buf -> toByteArray(buf)).toArray(Object[]::new); + + Mono m = write(null, StringCodec.INSTANCE, RedisCommands.UNLINK, params); + return m.map(v -> new NumericResponse<>(coll, v)); + }); + } + + private static final RedisStrictCommand EXPIRE = new RedisStrictCommand("EXPIRE", new BooleanReplayConvertor()); + + @Override + public Flux> expire(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, EXPIRE, keyBuf, command.getTimeout().getSeconds()); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + @Override + public Flux> pExpire(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.PEXPIRE, keyBuf); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand EXPIREAT = new RedisStrictCommand("EXPIREAT", new BooleanReplayConvertor()); + + @Override + public Flux> expireAt(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, EXPIREAT, keyBuf, command.getExpireAt().getEpochSecond()); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + @Override + public Flux> pExpireAt(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.PEXPIREAT, keyBuf, command.getExpireAt().toEpochMilli()); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + @Override + public Flux> persist(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.PERSIST, keyBuf); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand TTL = new RedisStrictCommand("TTL"); + + @Override + public Flux> ttl(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = read(keyBuf, StringCodec.INSTANCE, TTL, keyBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> pTtl(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = read(keyBuf, StringCodec.INSTANCE, RedisCommands.PTTL, keyBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> move(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDatabase(), "Database must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.MOVE, keyBuf, command.getDatabase()); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand OBJECT_ENCODING = new RedisStrictCommand("OBJECT", "ENCODING", new Convertor() { + @Override + public ValueEncoding convert(Object obj) { + return ValueEncoding.of((String) obj); + } + }); + + @Override + public Mono encodingOf(ByteBuffer key) { + Assert.notNull(key, "Key must not be null!"); + + byte[] keyBuf = toByteArray(key); + return read(keyBuf, StringCodec.INSTANCE, OBJECT_ENCODING, keyBuf); + } + + private static final RedisStrictCommand OBJECT_IDLETIME = new RedisStrictCommand("OBJECT", "IDLETIME"); + + @Override + public Mono idletime(ByteBuffer key) { + Assert.notNull(key, "Key must not be null!"); + + byte[] keyBuf = toByteArray(key); + Mono m = read(keyBuf, StringCodec.INSTANCE, OBJECT_IDLETIME, keyBuf); + return m.map(Duration::ofSeconds); + } + + private static final RedisStrictCommand OBJECT_REFCOUNT = new RedisStrictCommand("OBJECT", "REFCOUNT"); + + @Override + public Mono refcount(ByteBuffer key) { + Assert.notNull(key, "Key must not be null!"); + + byte[] keyBuf = toByteArray(key); + return read(keyBuf, StringCodec.INSTANCE, OBJECT_REFCOUNT, keyBuf); + } + + @Override + public Flux> copy(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getTarget(), "Target must not be null!"); + + List params = new ArrayList<>(); + byte[] keyBuf = toByteArray(command.getKey()); + params.add(keyBuf); + byte[] targetBuf = toByteArray(command.getTarget()); + params.add(targetBuf); + if (command.getDatabase() != null) { + params.add("DB"); + params.add(command.getDatabase()); + } + + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.COPY, params.toArray()); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveListCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveListCommands.java new file mode 100644 index 000000000..1992e6bfd --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveListCommands.java @@ -0,0 +1,370 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ReactiveListCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.ByteBufferResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.RangeCommand; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveListCommands extends RedissonBaseReactive implements ReactiveListCommands { + + private static final RedisStrictCommand RPUSH = new RedisStrictCommand("RPUSH"); + private static final RedisStrictCommand LPUSH = new RedisStrictCommand("LPUSH"); + private static final RedisStrictCommand RPUSHX = new RedisStrictCommand("RPUSHX"); + private static final RedisStrictCommand LPUSHX = new RedisStrictCommand("LPUSHX"); + + RedissonReactiveListCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + @Override + public Flux> push(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notEmpty(command.getValues(), "Values must not be null or empty!"); + + if (!command.getUpsert() && command.getValues().size() > 1) { + throw new InvalidDataAccessApiUsageException( + String.format("%s PUSHX only allows one value!", command.getDirection())); + } + + RedisStrictCommand redisCommand; + + List params = new ArrayList(); + params.add(toByteArray(command.getKey())); + params.addAll(command.getValues().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + + if (ObjectUtils.nullSafeEquals(Direction.RIGHT, command.getDirection())) { + if (command.getUpsert()) { + redisCommand = RPUSH; + } else { + redisCommand = RPUSHX; + } + } else { + if (command.getUpsert()) { + redisCommand = LPUSH; + } else { + redisCommand = LPUSHX; + } + } + + Mono m = write((byte[])params.get(0), StringCodec.INSTANCE, redisCommand, params.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand LLEN = new RedisStrictCommand("LLEN"); + + @Override + public Flux> lLen(Publisher commands) { + return execute(commands, key -> { + + Assert.notNull(key.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(key.getKey()); + Mono m = read(keyBuf, StringCodec.INSTANCE, LLEN, keyBuf); + return m.map(v -> new NumericResponse<>(key, v)); + }); + } + + @Override + public Flux>> lRange(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.LRANGE, + keyBuf, command.getRange().getLowerBound().getValue().orElse(0L), + command.getRange().getUpperBound().getValue().orElse(-1L)); + return m.map(v -> new CommandResponse<>(command, Flux.fromIterable(v).map(e -> ByteBuffer.wrap(e)))); + }); + } + + private static final RedisStrictCommand LTRIM = new RedisStrictCommand("LTRIM"); + + @Override + public Flux> lTrim(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, LTRIM, + keyBuf, command.getRange().getLowerBound().getValue().orElse(0L), + command.getRange().getUpperBound().getValue().orElse(-1L)); + return m.map(v -> new BooleanResponse<>(command, true)); + }); + } + + @Override + public Flux> lIndex(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getIndex(), "Index value must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.LINDEX, + keyBuf, command.getIndex()); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))); + }); + } + + private static final RedisStrictCommand LINSERT = new RedisStrictCommand("LINSERT"); + + @Override + public Flux> lInsert(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + Assert.notNull(command.getPivot(), "Pivot must not be null!"); + Assert.notNull(command.getPosition(), "Position must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + byte[] pivotBuf = toByteArray(command.getPivot()); + Mono m = write(keyBuf, StringCodec.INSTANCE, LINSERT, keyBuf, command.getPosition(), pivotBuf, valueBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand LSET = new RedisStrictCommand("LSET"); + + @Override + public Flux> lSet(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "value must not be null!"); + Assert.notNull(command.getIndex(), "Index must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + Mono m = write(keyBuf, StringCodec.INSTANCE, LSET, + keyBuf, command.getIndex(), valueBuf); + return m.map(v -> new BooleanResponse<>(command, true)); + }); + } + + private static final RedisStrictCommand LREM = new RedisStrictCommand("LREM"); + + @Override + public Flux> lRem(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + Assert.notNull(command.getCount(), "Count must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + Mono m = write(keyBuf, StringCodec.INSTANCE, LREM, keyBuf, command.getCount(), valueBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> pop(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDirection(), "Direction must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + RedisCommand redisCommand = RedisCommands.LPOP; + if (command.getDirection() == Direction.RIGHT) { + redisCommand = RedisCommands.RPOP; + } + + Mono m = write(keyBuf, ByteArrayCodec.INSTANCE, redisCommand, keyBuf); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))); + }); + } + + @Override + public Flux bPop(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + Assert.notNull(command.getDirection(), "Direction must not be null!"); + Assert.notNull(command.getTimeout(), "Timeout must not be null!"); + + RedisCommand> redisCommand = RedisCommands.BLPOP; + if (command.getDirection() == Direction.RIGHT) { + redisCommand = RedisCommands.BRPOP; + } + + List params = new ArrayList(command.getKeys().size() + 1); + params.addAll(command.getKeys().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + params.add(command.getTimeout().getSeconds()); + + Mono> m = write((byte[])params.get(0), ByteArrayCodec.INSTANCE, redisCommand, params.toArray()); + return m.map(v -> new PopResponse(command, + new PopResult(v.stream().map(e -> ByteBuffer.wrap(e)).collect(Collectors.toList())))); + }); + } + + @Override + public Flux> rPopLPush(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDestination(), "Destination key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] destinationBuf = toByteArray(command.getDestination()); + + Mono m = write(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.RPOPLPUSH, keyBuf, destinationBuf); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))); + }); + } + + @Override + public Flux> bRPopLPush(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDestination(), "Destination key must not be null!"); + Assert.notNull(command.getTimeout(), "Timeout must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] destinationBuf = toByteArray(command.getDestination()); + + Mono m = write(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.BRPOPLPUSH, + keyBuf, destinationBuf, command.getTimeout().getSeconds()); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))); + }); + } + + @Override + public Flux> lPos(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getElement(), "Element must not be null!"); + + List params = new ArrayList(); + byte[] keyBuf = toByteArray(command.getKey()); + params.add(keyBuf); + params.add(toByteArray(command.getElement())); + if (command.getRank() != null) { + params.add("RANK"); + params.add(command.getRank()); + } + if (command.getCount() != null) { + params.add("COUNT"); + params.add(command.getCount()); + } + + Mono m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.LPOS, params.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> lMove(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDestinationKey(), "Destination key must not be null!"); + Assert.notNull(command.getFrom(), "From must not be null!"); + Assert.notNull(command.getTo(), "To must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] destinationBuf = toByteArray(command.getDestinationKey()); + + Mono m = write(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.LMOVE, + keyBuf, destinationBuf, command.getFrom(), command.getTo()); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))); + }); + } + + @Override + public Flux> bLMove(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDestinationKey(), "Destination key must not be null!"); + Assert.notNull(command.getFrom(), "From must not be null!"); + Assert.notNull(command.getTo(), "To must not be null!"); + Assert.notNull(command.getTimeout(), "Timeout must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] destinationBuf = toByteArray(command.getDestinationKey()); + + Mono m = write(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.BLMOVE, + keyBuf, destinationBuf, command.getFrom(), command.getTo(), command.getTimeout().getSeconds()); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))); + }); + } + + @Override + public Flux>> popList(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + RedisCommand cmd; + if (command.getDirection() == Direction.RIGHT) { + cmd = RedisCommands.RPOP_LIST; + } else { + cmd = RedisCommands.LPOP_LIST; + } + + List params = new ArrayList<>(2); + params.add(keyBuf); + if (command.getCount() > 0) { + params.add(command.getCount()); + } + + Mono> m = write(keyBuf, ByteArrayCodec.INSTANCE, cmd, params.toArray()); + return m.map(v -> new CommandResponse<>(command, Flux.fromIterable(v).map(e -> ByteBuffer.wrap(e)))); + }); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveNumberCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveNumberCommands.java new file mode 100644 index 000000000..1e7ce6c77 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveNumberCommands.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.math.BigDecimal; + +import org.reactivestreams.Publisher; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.convertor.NumberConvertor; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveNumberCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveNumberCommands extends RedissonBaseReactive implements ReactiveNumberCommands { + + public RedissonReactiveNumberCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + @Override + public Flux> incr(Publisher keys) { + return execute(keys, key -> { + + Assert.notNull(key.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(key.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.INCR, keyBuf); + return m.map(v -> new NumericResponse<>(key, v)); + }); + } + + @Override + public Flux, T>> incrBy(Publisher> commands) { + return execute(commands, key -> { + + Assert.notNull(key.getKey(), "Key must not be null!"); + Assert.notNull(key.getValue(), "Value must not be null!"); + + byte[] keyBuf = toByteArray(key.getKey()); + + Mono m = write(keyBuf, StringCodec.INSTANCE, + new RedisCommand("INCRBYFLOAT", new NumberConvertor(key.getValue().getClass())), + keyBuf, new BigDecimal(key.getValue().toString()).toPlainString()); + return m.map(v -> new NumericResponse<>(key, v)); + }); + } + + @Override + public Flux> decr(Publisher keys) { + return execute(keys, key -> { + + Assert.notNull(key.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(key.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.DECR, keyBuf); + return m.map(v -> new NumericResponse<>(key, v)); + }); + } + + @Override + public Flux, T>> decrBy(Publisher> commands) { + return execute(commands, key -> { + + Assert.notNull(key.getKey(), "Key must not be null!"); + Assert.notNull(key.getValue(), "Value must not be null!"); + + byte[] keyBuf = toByteArray(key.getKey()); + + Mono m = write(keyBuf, StringCodec.INSTANCE, + new RedisCommand("INCRBYFLOAT", new NumberConvertor(key.getValue().getClass())), + keyBuf, "-" + new BigDecimal(key.getValue().toString()).toPlainString()); + return m.map(v -> new NumericResponse<>(key, v)); + }); + } + + @Override + public Flux, T>> hIncrBy( + Publisher> commands) { + return execute(commands, key -> { + + Assert.notNull(key.getKey(), "Key must not be null!"); + Assert.notNull(key.getValue(), "Value must not be null!"); + + byte[] keyBuf = toByteArray(key.getKey()); + byte[] fieldBuf = toByteArray(key.getField()); + + Mono m = write(keyBuf, StringCodec.INSTANCE, + new RedisCommand("HINCRBYFLOAT", new NumberConvertor(key.getValue().getClass())), + keyBuf, fieldBuf, new BigDecimal(key.getValue().toString()).toPlainString()); + return m.map(v -> new NumericResponse<>(key, v)); + }); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactivePubSubCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactivePubSubCommands.java new file mode 100644 index 000000000..4282c2940 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactivePubSubCommands.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactivePubSubCommands; +import org.springframework.data.redis.connection.ReactiveSubscription; +import org.springframework.data.redis.connection.ReactiveSubscription.ChannelMessage; + +import org.springframework.data.redis.connection.SubscriptionListener; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactivePubSubCommands extends RedissonBaseReactive implements ReactivePubSubCommands { + + RedissonReactivePubSubCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + @Override + public Flux publish(Publisher> messageStream) { + return execute(messageStream, msg -> { + return write(toByteArray(msg.getChannel()), StringCodec.INSTANCE, RedisCommands.PUBLISH, toByteArray(msg.getChannel()), toByteArray(msg.getMessage())); + }); + } + + @Override + public Mono subscribe(ByteBuffer... channels) { + throw new UnsupportedOperationException("Subscribe through ReactiveSubscription object created by createSubscription method"); + } + + @Override + public Mono pSubscribe(ByteBuffer... patterns) { + throw new UnsupportedOperationException("Subscribe through ReactiveSubscription object created by createSubscription method"); + } + + @Override + public Mono createSubscription(SubscriptionListener subscriptionListener) { + return Mono.just(new RedissonReactiveSubscription(executorService.getConnectionManager(), subscriptionListener)); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveRedisClusterConnection.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveRedisClusterConnection.java new file mode 100644 index 000000000..cae33e201 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveRedisClusterConnection.java @@ -0,0 +1,226 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.client.protocol.decoder.ObjectDecoder; +import org.redisson.client.protocol.decoder.ObjectListReplayDecoder; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; + +import java.nio.ByteBuffer; +import java.util.*; +import java.util.stream.Collectors; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveRedisClusterConnection extends RedissonReactiveRedisConnection implements ReactiveRedisClusterConnection { + + public RedissonReactiveRedisClusterConnection(CommandReactiveExecutor executorService) { + super(executorService); + } + + @Override + public ReactiveClusterKeyCommands keyCommands() { + return new RedissonReactiveClusterKeyCommands(executorService); + } + + @Override + public ReactiveClusterStringCommands stringCommands() { + return new RedissonReactiveClusterStringCommands(executorService); + } + + @Override + public ReactiveClusterNumberCommands numberCommands() { + return new RedissonReactiveClusterNumberCommands(executorService); + } + + @Override + public ReactiveClusterListCommands listCommands() { + return new RedissonReactiveClusterListCommands(executorService); + } + + @Override + public ReactiveClusterSetCommands setCommands() { + return new RedissonReactiveClusterSetCommands(executorService); + } + + @Override + public ReactiveClusterZSetCommands zSetCommands() { + return new RedissonReactiveClusterZSetCommands(executorService); + } + + @Override + public ReactiveClusterHashCommands hashCommands() { + return new RedissonReactiveClusterHashCommands(executorService); + } + + @Override + public ReactiveClusterGeoCommands geoCommands() { + return new RedissonReactiveClusterGeoCommands(executorService); + } + + @Override + public ReactiveClusterHyperLogLogCommands hyperLogLogCommands() { + return new RedissonReactiveClusterHyperLogLogCommands(executorService); + } + + @Override + public ReactiveClusterServerCommands serverCommands() { + return new RedissonReactiveClusterServerCommands(executorService); + } + + @Override + public ReactiveClusterStreamCommands streamCommands() { + return new RedissonReactiveClusterStreamCommands(executorService); + } + + @Override + public Mono ping(RedisClusterNode node) { + return execute(node, RedisCommands.PING); + } + + private static final RedisStrictCommand> CLUSTER_NODES = + new RedisStrictCommand<>("CLUSTER", "NODES", new ObjectDecoder(new RedisClusterNodeDecoder())); + + @Override + public Flux clusterGetNodes() { + Mono> result = read(null, StringCodec.INSTANCE, CLUSTER_NODES); + return result.flatMapMany(e -> Flux.fromIterable(e)); + } + + @Override + public Flux clusterGetReplicas(RedisClusterNode redisClusterNode) { + Flux nodes = clusterGetNodes(); + Flux master = nodes.filter(e -> e.getHost().equals(redisClusterNode.getHost()) && e.getPort().equals(redisClusterNode.getPort())); + return master.flatMap(node -> clusterGetNodes().filter(e -> Objects.equals(e.getMasterId(), node.getMasterId()))); + } + + @Override + public Mono>> clusterGetMasterReplicaMap() { + Flux nodes = clusterGetNodes(); + Flux masters = nodes.filter(e -> e.isMaster()); + return masters.flatMap(master -> Mono.just(master).zipWith(clusterGetNodes() + .filter(e -> Objects.equals(e.getMasterId(), master.getMasterId())) + .collect(Collectors.toSet()))) + .collect(Collectors.toMap(Tuple2::getT1, Tuple2::getT2)); + } + + @Override + public Mono clusterGetSlotForKey(ByteBuffer byteBuffer) { + return read(null, StringCodec.INSTANCE, RedisCommands.KEYSLOT, toByteArray(byteBuffer)); + } + + @Override + public Mono clusterGetNodeForSlot(int slot) { + return clusterGetNodes().filter(n -> n.isMaster() && n.getSlotRange().contains(slot)).next(); + } + + @Override + public Mono clusterGetNodeForKey(ByteBuffer byteBuffer) { + int slot = executorService.getConnectionManager().calcSlot(toByteArray(byteBuffer)); + return clusterGetNodeForSlot(slot); + } + + @Override + public Mono clusterGetClusterInfo() { + Mono> mono = read(null, StringCodec.INSTANCE, RedisCommands.CLUSTER_INFO); + return mono.map(e -> { + Properties props = new Properties(); + for (Map.Entry entry : e.entrySet()) { + props.setProperty(entry.getKey(), entry.getValue()); + } + return new ClusterInfo(props); + }); + } + + @Override + public Mono clusterAddSlots(RedisClusterNode redisClusterNode, int... ints) { + List params = convert(ints); + return execute(redisClusterNode, RedisCommands.CLUSTER_ADDSLOTS, params.toArray()); + } + + private List convert(int... slots) { + List params = new ArrayList(); + for (int slot : slots) { + params.add(slot); + } + return params; + } + + @Override + public Mono clusterAddSlots(RedisClusterNode redisClusterNode, RedisClusterNode.SlotRange slotRange) { + return clusterAddSlots(redisClusterNode, slotRange.getSlotsArray()); + } + + @Override + public Mono clusterCountKeysInSlot(int slot) { + Mono node = clusterGetNodeForSlot(slot); + return node.flatMap(e -> { + return execute(e, RedisCommands.CLUSTER_COUNTKEYSINSLOT, slot); + }); + } + + @Override + public Mono clusterDeleteSlots(RedisClusterNode redisClusterNode, int... ints) { + List params = convert(ints); + return execute(redisClusterNode, RedisCommands.CLUSTER_DELSLOTS, params.toArray()); + } + + @Override + public Mono clusterDeleteSlotsInRange(RedisClusterNode redisClusterNode, RedisClusterNode.SlotRange slotRange) { + return clusterDeleteSlots(redisClusterNode, slotRange.getSlotsArray()); + } + + @Override + public Mono clusterForget(RedisClusterNode redisClusterNode) { + return execute(redisClusterNode, RedisCommands.CLUSTER_FORGET, redisClusterNode.getId()); + } + + @Override + public Mono clusterMeet(RedisClusterNode redisClusterNode) { + return execute(redisClusterNode, RedisCommands.CLUSTER_MEET, redisClusterNode.getHost(), redisClusterNode.getPort()); + } + + @Override + public Mono clusterSetSlot(RedisClusterNode redisClusterNode, int slot, AddSlots addSlots) { + return execute(redisClusterNode, RedisCommands.CLUSTER_SETSLOT, slot, addSlots); + } + + private static final RedisStrictCommand> CLUSTER_GETKEYSINSLOT = new RedisStrictCommand>("CLUSTER", "GETKEYSINSLOT", new ObjectListReplayDecoder()); + + @Override + public Flux clusterGetKeysInSlot(int slot, int count) { + Mono> f = executorService.reactive(() -> { + return executorService.readAsync((String) null, ByteArrayCodec.INSTANCE, CLUSTER_GETKEYSINSLOT, slot, count); + }); + return f.flatMapMany(e -> Flux.fromIterable(e)).map(e -> ByteBuffer.wrap(e)); + } + + @Override + public Mono clusterReplicate(RedisClusterNode redisClusterNode, RedisClusterNode slave) { + return execute(redisClusterNode, RedisCommands.CLUSTER_REPLICATE, slave.getId()); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveRedisConnection.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveRedisConnection.java new file mode 100644 index 000000000..69f4585b9 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveRedisConnection.java @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.*; + +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveRedisConnection extends RedissonBaseReactive implements ReactiveRedisConnection { + + public RedissonReactiveRedisConnection(CommandReactiveExecutor executorService) { + super(executorService); + } + + @Override + public Mono closeLater() { + return Mono.empty(); + } + + @Override + public ReactiveKeyCommands keyCommands() { + return new RedissonReactiveKeyCommands(executorService); + } + + @Override + public ReactiveStringCommands stringCommands() { + return new RedissonReactiveStringCommands(executorService); + } + + @Override + public ReactiveNumberCommands numberCommands() { + return new RedissonReactiveNumberCommands(executorService); + } + + @Override + public ReactiveListCommands listCommands() { + return new RedissonReactiveListCommands(executorService); + } + + @Override + public ReactiveSetCommands setCommands() { + return new RedissonReactiveSetCommands(executorService); + } + + @Override + public ReactiveZSetCommands zSetCommands() { + return new RedissonReactiveZSetCommands(executorService); + } + + @Override + public ReactiveHashCommands hashCommands() { + return new RedissonReactiveHashCommands(executorService); + } + + @Override + public ReactiveGeoCommands geoCommands() { + return new RedissonReactiveGeoCommands(executorService); + } + + @Override + public ReactiveHyperLogLogCommands hyperLogLogCommands() { + return new RedissonReactiveHyperLogLogCommands(executorService); + } + + @Override + public ReactivePubSubCommands pubSubCommands() { + return new RedissonReactivePubSubCommands(executorService); + } + + @Override + public ReactiveScriptingCommands scriptingCommands() { + return new RedissonReactiveScriptingCommands(executorService); + } + + @Override + public ReactiveServerCommands serverCommands() { + return new RedissonReactiveServerCommands(executorService); + } + + @Override + public ReactiveStreamCommands streamCommands() { + return new RedissonReactiveStreamCommands(executorService); + } + + @Override + public Mono ping() { + return read(null, StringCodec.INSTANCE, RedisCommands.PING); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveScriptingCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveScriptingCommands.java new file mode 100644 index 000000000..8c96d83dc --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveScriptingCommands.java @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.api.RFuture; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.misc.CompletableFutureWrapper; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveScriptingCommands; +import org.springframework.data.redis.connection.ReturnType; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveScriptingCommands extends RedissonBaseReactive implements ReactiveScriptingCommands { + + RedissonReactiveScriptingCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + @Override + public Mono scriptFlush() { + return executorService.reactive(() -> { + RFuture f = executorService.writeAllVoidAsync(RedisCommands.SCRIPT_FLUSH); + return toStringFuture(f); + }); + } + + @Override + public Mono scriptKill() { + throw new UnsupportedOperationException(); + } + + @Override + public Mono scriptLoad(ByteBuffer script) { + return executorService.reactive(() -> { + List> futures = executorService.executeAllAsync(RedisCommands.SCRIPT_LOAD, (Object)toByteArray(script)); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + CompletableFuture s = f.thenApply(r -> futures.get(0).getNow(null)); + return new CompletableFutureWrapper<>(s); + }); + } + + @Override + public Flux scriptExists(List scriptShas) { + Mono> m = executorService.reactive(() -> { + List>> futures = executorService.writeAllAsync(RedisCommands.SCRIPT_EXISTS, scriptShas.toArray()); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + CompletableFuture> s = f.thenApply(r -> { + List result = futures.get(0).getNow(new ArrayList<>()); + for (CompletableFuture> future : futures.subList(1, futures.size())) { + List l = future.getNow(new ArrayList<>()); + for (int i = 0; i < l.size(); i++) { + result.set(i, result.get(i) | l.get(i)); + } + } + return result; + }); + return new CompletableFutureWrapper<>(s); + }); + return m.flatMapMany(v -> Flux.fromIterable(v)); + } + + protected RedisCommand toCommand(ReturnType returnType, String name) { + RedisCommand c = null; + if (returnType == ReturnType.BOOLEAN) { + c = org.redisson.api.RScript.ReturnType.BOOLEAN.getCommand(); + } else if (returnType == ReturnType.INTEGER) { + c = org.redisson.api.RScript.ReturnType.INTEGER.getCommand(); + } else if (returnType == ReturnType.MULTI) { + c = org.redisson.api.RScript.ReturnType.MULTI.getCommand(); + return new RedisCommand(c, name, new BinaryConvertor()); + } else if (returnType == ReturnType.STATUS) { + c = org.redisson.api.RScript.ReturnType.STATUS.getCommand(); + } else if (returnType == ReturnType.VALUE) { + c = org.redisson.api.RScript.ReturnType.VALUE.getCommand(); + return new RedisCommand(c, name, new BinaryConvertor()); + } + return new RedisCommand(c, name); + } + + @Override + public Flux eval(ByteBuffer script, ReturnType returnType, int numKeys, ByteBuffer... keysAndArgs) { + RedisCommand c = toCommand(returnType, "EVAL"); + List params = new ArrayList(); + params.add(toByteArray(script)); + params.add(numKeys); + params.addAll(Arrays.stream(keysAndArgs).map(m -> toByteArray(m)).collect(Collectors.toList())); + Mono m = write(null, ByteArrayCodec.INSTANCE, c, params.toArray()); + return convert(m); + } + + protected Flux convert(Mono m) { + return (Flux) m.map(e -> { + if (e.getClass().isArray()) { + return ByteBuffer.wrap((byte[])e); + } + if (e instanceof List) { + List l = (List) e; + if (!l.isEmpty()) { + for (int i = 0; i < l.size(); i++) { + if (l.get(i).getClass().isArray()) { + l.set(i, ByteBuffer.wrap((byte[])l.get(i))); + } + } + return l; + } + } + return e; + }).flux(); + } + + @Override + public Flux evalSha(String scriptSha, ReturnType returnType, int numKeys, ByteBuffer... keysAndArgs) { + RedisCommand c = toCommand(returnType, "EVALSHA"); + List params = new ArrayList(); + params.add(scriptSha); + params.add(numKeys); + params.addAll(Arrays.stream(keysAndArgs).map(m -> toByteArray(m)).collect(Collectors.toList())); + Mono m = write(null, ByteArrayCodec.INSTANCE, c, params.toArray()); + return convert(m); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveServerCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveServerCommands.java new file mode 100644 index 000000000..74744c624 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveServerCommands.java @@ -0,0 +1,201 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import org.redisson.api.RFuture; +import org.redisson.client.codec.LongCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.client.protocol.decoder.ObjectDecoder; +import org.redisson.client.protocol.decoder.TimeLongObjectDecoder; +import org.redisson.misc.CompletableFutureWrapper; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.redis.connection.ReactiveServerCommands; +import org.springframework.data.redis.connection.RedisServerCommands; +import org.springframework.data.redis.core.types.RedisClientInfo; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveServerCommands extends RedissonBaseReactive implements ReactiveServerCommands { + + RedissonReactiveServerCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + static final RedisStrictCommand BGREWRITEAOF = new RedisStrictCommand("BGREWRITEAOF"); + + @Override + public Mono bgReWriteAof() { + return write(null, StringCodec.INSTANCE, BGREWRITEAOF); + } + + static final RedisStrictCommand BGSAVE = new RedisStrictCommand("BGSAVE"); + + @Override + public Mono bgSave() { + return write(null, StringCodec.INSTANCE, BGSAVE); + } + + @Override + public Mono lastSave() { + return write(null, StringCodec.INSTANCE, RedisCommands.LASTSAVE); + } + + static final RedisStrictCommand SAVE = new RedisStrictCommand("SAVE"); + + @Override + public Mono save() { + return write(null, StringCodec.INSTANCE, SAVE); + } + + @Override + public Mono dbSize() { + return executorService.reactive(() -> { + List> futures = executorService.readAllAsync(RedisCommands.DBSIZE); + CompletableFuture f = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + CompletableFuture s = f.thenApply(r -> futures.stream().mapToLong(v -> v.getNow(0L)).sum()); + return new CompletableFutureWrapper<>(s); + }); + } + + private static final RedisStrictCommand FLUSHDB = new RedisStrictCommand("FLUSHDB"); + + @Override + public Mono flushDb() { + return executorService.reactive(() -> { + RFuture f = executorService.writeAllVoidAsync(FLUSHDB); + return toStringFuture(f); + }); + } + + private static final RedisStrictCommand FLUSHALL = new RedisStrictCommand("FLUSHALL"); + + @Override + public Mono flushAll() { + return executorService.reactive(() -> { + RFuture f = executorService.writeAllVoidAsync(FLUSHALL); + return toStringFuture(f); + }); + } + + @Override + public Mono flushDb(RedisServerCommands.FlushOption option) { + if (option == RedisServerCommands.FlushOption.ASYNC) { + return executorService.reactive(() -> { + RFuture f = executorService.writeAllVoidAsync(FLUSHDB, option.toString()); + return toStringFuture(f); + }); + } + return flushDb(); + } + + @Override + public Mono flushAll(RedisServerCommands.FlushOption option) { + if (option == RedisServerCommands.FlushOption.ASYNC) { + return executorService.reactive(() -> { + RFuture f = executorService.writeAllVoidAsync(FLUSHALL, option.toString()); + return toStringFuture(f); + }); + } + return flushAll(); + } + + static final RedisStrictCommand INFO_DEFAULT = new RedisStrictCommand("INFO", "DEFAULT", new ObjectDecoder(new PropertiesDecoder())); + static final RedisStrictCommand INFO = new RedisStrictCommand("INFO", new ObjectDecoder(new PropertiesDecoder())); + + @Override + public Mono info() { + return read(null, StringCodec.INSTANCE, INFO_DEFAULT); + } + + @Override + public Mono info(String section) { + return read(null, StringCodec.INSTANCE, INFO, section); + } + + static final RedisStrictCommand CONFIG_GET = new RedisStrictCommand("CONFIG", "GET", new PropertiesListDecoder()); + + @Override + public Mono getConfig(String pattern) { + return read(null, StringCodec.INSTANCE, CONFIG_GET, pattern); + } + + static final RedisStrictCommand CONFIG_SET = new RedisStrictCommand("CONFIG", "SET"); + + @Override + public Mono setConfig(String param, String value) { + return write(null, StringCodec.INSTANCE, CONFIG_SET, param, value); + } + + static final RedisStrictCommand CONFIG_RESETSTAT = new RedisStrictCommand("CONFIG", "RESETSTAT"); + + @Override + public Mono resetConfigStats() { + return write(null, StringCodec.INSTANCE, CONFIG_RESETSTAT); + } + + static final RedisStrictCommand TIME = new RedisStrictCommand("TIME", new TimeLongObjectDecoder()); + + @Override + public Mono time() { + return read(null, LongCodec.INSTANCE, TIME); + } + + @Override + public Mono time(TimeUnit timeUnit) { + return read(null, LongCodec.INSTANCE, new RedisStrictCommand<>("TIME", new TimeLongObjectDecoder() { + @Override + public Long decode(List parts, State state) { + Long time = super.decode(parts, state); + return timeUnit.convert(time, TimeUnit.MILLISECONDS); + } + })); + } + + @Override + public Mono killClient(String host, int port) { + throw new UnsupportedOperationException(); + } + + @Override + public Mono setClientName(String name) { + throw new UnsupportedOperationException("Should be defined through Redisson Config object"); + } + + @Override + public Mono getClientName() { + throw new UnsupportedOperationException(); + } + + @Override + public Flux getClientList() { + throw new UnsupportedOperationException(); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveSetCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveSetCommands.java new file mode 100644 index 000000000..e1a6c68a3 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveSetCommands.java @@ -0,0 +1,320 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.redisson.ScanResult; +import org.redisson.api.RFuture; +import org.redisson.client.RedisClient; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.client.protocol.convertor.BooleanReplayConvertor; +import org.redisson.client.protocol.decoder.ListScanResult; +import org.redisson.client.protocol.decoder.ObjectListReplayDecoder; +import org.redisson.reactive.CommandReactiveExecutor; +import org.redisson.reactive.SetReactiveIterator; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.ByteBufferResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyScanCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.ReactiveSetCommands; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveSetCommands extends RedissonBaseReactive implements ReactiveSetCommands { + + RedissonReactiveSetCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + private static final RedisCommand SADD = new RedisCommand("SADD"); + + @Override + public Flux> sAdd(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValues(), "Values must not be null!"); + + List args = new ArrayList(command.getValues().size() + 1); + args.add(toByteArray(command.getKey())); + args.addAll(command.getValues().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, SADD, args.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisCommand SREM = new RedisCommand("SREM"); + + @Override + public Flux> sRem(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValues(), "Values must not be null!"); + + List args = new ArrayList(command.getValues().size() + 1); + args.add(toByteArray(command.getKey())); + args.addAll(command.getValues().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + + Mono m = write((byte[])args.get(0), StringCodec.INSTANCE, SREM, args.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux sPop(SPopCommand command) { + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono> m = write(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.SPOP, keyBuf, command.getCount()); + return m.flatMapMany(v -> Flux.fromIterable(v).map(e -> ByteBuffer.wrap(e))); + } + + @Override + public Flux> sPop(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.SPOP_SINGLE, keyBuf); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))); + }); + } + + @Override + public Flux> sMove(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDestination(), "Destination key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] destinationBuf = toByteArray(command.getDestination()); + byte[] valueBuf = toByteArray(command.getValue()); + Mono m = write(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.SMOVE, keyBuf, destinationBuf, valueBuf); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand SCARD = new RedisStrictCommand("SCARD"); + + @Override + public Flux> sCard(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, SCARD, keyBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> sIsMember(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + Mono m = read(keyBuf, StringCodec.INSTANCE, RedisCommands.SISMEMBER, keyBuf, valueBuf); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + @Override + public Flux>> sInter(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKeys(), "Key must not be null!"); + + List list = command.getKeys().stream().map(v -> toByteArray(v)).collect(Collectors.toList()); + Mono> m = write((byte[])list.get(0), ByteArrayCodec.INSTANCE, RedisCommands.SINTER, list.toArray()); + return m.map(v -> new CommandResponse<>(command, + Flux.fromIterable(v).map(e -> ByteBuffer.wrap(e)))); + }); + } + + @Override + public Flux> sInterStore(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + Assert.notNull(command.getKey(), "Destination key must not be null!"); + + List args = new ArrayList(command.getKeys().size() + 1); + args.add(toByteArray(command.getKey())); + args.addAll(command.getKeys().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + + Mono m = write((byte[])args.get(0), StringCodec.INSTANCE, RedisCommands.SINTERSTORE, args.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux>> sUnion(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKeys(), "Key must not be null!"); + + List list = command.getKeys().stream().map(v -> toByteArray(v)).collect(Collectors.toList()); + Mono> m = write((byte[])list.get(0), ByteArrayCodec.INSTANCE, RedisCommands.SUNION, list.toArray()); + return m.map(v -> new CommandResponse<>(command, + Flux.fromIterable(v).map(e -> ByteBuffer.wrap(e)))); + }); + } + + @Override + public Flux> sUnionStore(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + Assert.notNull(command.getKey(), "Destination key must not be null!"); + + List args = new ArrayList(command.getKeys().size() + 1); + args.add(toByteArray(command.getKey())); + args.addAll(command.getKeys().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + + Mono m = write((byte[])args.get(0), StringCodec.INSTANCE, RedisCommands.SUNIONSTORE, args.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux>> sDiff(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKeys(), "Key must not be null!"); + + List list = command.getKeys().stream().map(v -> toByteArray(v)).collect(Collectors.toList()); + Mono> m = write((byte[])list.get(0), ByteArrayCodec.INSTANCE, RedisCommands.SDIFF, list.toArray()); + return m.map(v -> new CommandResponse<>(command, + Flux.fromIterable(v).map(e -> ByteBuffer.wrap(e)))); + }); + } + + @Override + public Flux> sDiffStore(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + Assert.notNull(command.getKey(), "Destination key must not be null!"); + + List args = new ArrayList(command.getKeys().size() + 1); + args.add(toByteArray(command.getKey())); + args.addAll(command.getKeys().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + + Mono m = write((byte[])args.get(0), StringCodec.INSTANCE, RedisCommands.SDIFFSTORE, args.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux>> sMembers(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.SMEMBERS, keyBuf); + return m.map(v -> new CommandResponse<>(command, Flux.fromIterable(v).map(e -> ByteBuffer.wrap(e)))); + }); + } + + @Override + public Flux>> sScan(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getOptions(), "ScanOptions must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Flux flux = Flux.create(new SetReactiveIterator() { + @Override + protected RFuture> scanIterator(RedisClient client, long nextIterPos) { + if (command.getOptions().getPattern() == null) { + return executorService.readAsync(client, keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.SSCAN, + keyBuf, nextIterPos, "COUNT", Optional.ofNullable(command.getOptions().getCount()).orElse(10L)); + } + + return executorService.readAsync(client, keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.SSCAN, + keyBuf, nextIterPos, "MATCH", command.getOptions().getPattern(), + "COUNT", Optional.ofNullable(command.getOptions().getCount()).orElse(10L)); + } + }); + return Mono.just(new CommandResponse<>(command, flux.map(v -> ByteBuffer.wrap(v)))); + }); + } + + @Override + public Flux>> sRandMember( + Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.SRANDMEMBER, keyBuf, command.getCount().orElse(1L)); + return m.map(v -> new CommandResponse<>(command, Flux.fromIterable(v).map(e -> ByteBuffer.wrap(e)))); + }); + } + + private static final RedisCommand> SMISMEMBER = new RedisCommand<>("SMISMEMBER", new ObjectListReplayDecoder<>()); + + @Override + public Flux> sMIsMember(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValues(), "Values must not be null!"); + + List args = new ArrayList<>(); + byte[] keyBuf = toByteArray(command.getKey()); + args.add(keyBuf); + args.addAll(command.getValues()); + + Mono> m = read(keyBuf, StringCodec.INSTANCE, SMISMEMBER, args.toArray()); + return m.map(v -> new ReactiveRedisConnection.MultiValueResponse<>(command, v)); + }); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveStreamCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveStreamCommands.java new file mode 100644 index 000000000..0121c3105 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveStreamCommands.java @@ -0,0 +1,496 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.reactivestreams.Publisher; +import org.redisson.api.*; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.client.protocol.decoder.*; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.ReactiveStreamCommands; +import org.springframework.data.redis.connection.RedisStreamCommands; +import org.springframework.data.redis.connection.stream.StreamInfo; +import org.springframework.data.redis.connection.stream.*; +import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveStreamCommands extends RedissonBaseReactive implements ReactiveStreamCommands { + + RedissonReactiveStreamCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + private static List toStringList(List recordIds) { + return recordIds.stream().map(RecordId::getValue).collect(Collectors.toList()); + } + + @Override + public Flux>> xClaimJustId(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getGroupName(), "Group name must not be null!"); + Assert.notNull(command.getNewOwner(), "NewOwner must not be null!"); + Assert.notEmpty(command.getOptions().getIds(), "Ids collection must not be empty!"); + + List params = new ArrayList<>(); + byte[] k = toByteArray(command.getKey()); + params.add(k); + params.add(command.getGroupName()); + params.add(command.getNewOwner()); + params.add(Objects.requireNonNull(command.getOptions().getIdleTime()).toMillis()); + params.addAll(Arrays.asList(command.getOptions().getIdsAsStringArray())); + params.add("JUSTID"); + + Mono>> m = write(k, ByteArrayCodec.INSTANCE, RedisCommands.XCLAIM, params.toArray()); + return m.map(v -> new ReactiveRedisConnection.CommandResponse<>(command, Flux.fromStream(v.entrySet().stream()).map(e -> { + return RecordId.of(e.getKey().toString()); + }))); + }); + } + + @Override + public Flux>> xClaim(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getGroupName(), "Group name must not be null!"); + Assert.notNull(command.getNewOwner(), "NewOwner must not be null!"); + Assert.notEmpty(command.getOptions().getIds(), "Ids collection must not be empty!"); + + List params = new ArrayList<>(); + byte[] k = toByteArray(command.getKey()); + params.add(k); + params.add(command.getGroupName()); + params.add(command.getNewOwner()); + params.add(Objects.requireNonNull(command.getOptions().getIdleTime()).toMillis()); + params.addAll(Arrays.asList(command.getOptions().getIdsAsStringArray())); + + Mono>> m = write(k, ByteArrayCodec.INSTANCE, RedisCommands.XCLAIM, params.toArray()); + return m.map(v -> new ReactiveRedisConnection.CommandResponse<>(command, Flux.fromStream(v.entrySet().stream()).map(e -> { + Map map = e.getValue().entrySet().stream() + .collect(Collectors.toMap(entry -> ByteBuffer.wrap(entry.getKey()), + entry -> ByteBuffer.wrap(entry.getValue()))); + return StreamRecords.newRecord() + .in(command.getKey()) + .withId(RecordId.of(e.getKey().toString())) + .ofBuffer(map); + }))); + }); + } + + @Override + public Flux> xPendingSummary(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getGroupName(), "Group name must not be null!"); + + byte[] k = toByteArray(command.getKey()); + + Mono m = write(k, StringCodec.INSTANCE, RedisCommands.XPENDING, k, command.getGroupName()); + return m.map(v -> { + Range range = Range.open(v.getLowestId().toString(), v.getHighestId().toString()); + PendingMessagesSummary s = new PendingMessagesSummary(command.getGroupName(), v.getTotal(), range, v.getConsumerNames()); + return new ReactiveRedisConnection.CommandResponse<>(command, s); + }); + }); + } + + @Override + public Flux> xPending(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getGroupName(), "Group name must not be null!"); + + byte[] k = toByteArray(command.getKey()); + + List params = new ArrayList<>(); + params.add(k); + + params.add(((Range.Bound)command.getRange().getLowerBound()).getValue().orElse("-")); + params.add(((Range.Bound)command.getRange().getUpperBound()).getValue().orElse("+")); + + if (command.getCount() != null) { + params.add(command.getCount()); + } + if (command.getConsumerName() != null) { + params.add(command.getConsumerName()); + } + + Mono> m = write(k, StringCodec.INSTANCE, RedisCommands.XPENDING_ENTRIES, params.toArray()); + return m.map(list -> { + List msgs = list.stream().map(v -> new PendingMessage(RecordId.of(v.getId().toString()), + Consumer.from(command.getGroupName(), v.getConsumerName()), + Duration.of(v.getIdleTime(), ChronoUnit.MILLIS), + v.getLastTimeDelivered())).collect(Collectors.toList()); + PendingMessages s = new PendingMessages(command.getGroupName(), command.getRange(), msgs); + return new ReactiveRedisConnection.CommandResponse<>(command, s); + }); + }); + } + + private static final RedisCommand> XINFO_STREAM = new RedisCommand<>("XINFO", "STREAM", + new ListMultiDecoder2( + new StreamInfoDecoder(), + new ObjectDecoder(StringCodec.INSTANCE.getValueDecoder()), + new ObjectMapDecoder(false))); + + @Override + public Flux> xInfo(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] k = toByteArray(command.getKey()); + + Mono> m = write(k, ByteArrayCodec.INSTANCE, XINFO_STREAM, k); + return m.map(i -> { + + Map res = new HashMap<>(); + res.put("length", (long) i.getLength()); + res.put("first-entry", i.getFirstEntry().getData()); + res.put("last-entry", i.getLastEntry().getData()); + res.put("radix-tree-keys", i.getRadixTreeKeys()); + res.put("radix-tree-nodes", i.getRadixTreeNodes()); + res.put("groups", (long) i.getGroups()); + res.put("last-generated-id", i.getLastGeneratedId().toString()); + + List list = res.entrySet().stream() + .flatMap(e -> Stream.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + return new ReactiveRedisConnection.CommandResponse<>(command, StreamInfo.XInfoStream.fromList(list)); + }); + }); + } + + @Override + public Flux>> xInfoGroups(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] k = toByteArray(command.getKey()); + + Mono> m = write(k, StringCodec.INSTANCE, RedisCommands.XINFO_GROUPS, k); + return m.map(v -> new ReactiveRedisConnection.CommandResponse<>(command, Flux.fromStream(v.stream()).map(r -> { + Map res = new HashMap<>(); + res.put("name", r.getName()); + res.put("consumers", (long) r.getConsumers()); + res.put("pending", (long) r.getPending()); + res.put("last-delivered-id", r.getLastDeliveredId().toString()); + + List list = res.entrySet().stream() + .flatMap(e -> Stream.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + return StreamInfo.XInfoGroup.fromList(list); + }))); + }); + } + + @Override + public Flux>> xInfoConsumers(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getGroupName(), "Group name must not be null!"); + + byte[] k = toByteArray(command.getKey()); + + Mono> m = write(k, StringCodec.INSTANCE, RedisCommands.XINFO_CONSUMERS, k, command.getGroupName()); + return m.map(v -> new ReactiveRedisConnection.CommandResponse<>(command, Flux.fromStream(v.stream()).map(r -> { + Map res = new HashMap<>(); + res.put("name", r.getName()); + res.put("idle", r.getIdleTime()); + res.put("pending", (long) r.getPending()); + + List list = res.entrySet().stream() + .flatMap(e -> Stream.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + return new StreamInfo.XInfoConsumer(command.getGroupName(), list); + }))); + }); + } + + @Override + public Flux> xAck(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getGroup(), "Group must not be null!"); + Assert.notNull(command.getRecordIds(), "recordIds must not be null!"); + + List params = new ArrayList<>(); + byte[] k = toByteArray(command.getKey()); + params.add(k); + params.add(command.getGroup()); + params.addAll(toStringList(command.getRecordIds())); + + Mono m = write(k, StringCodec.INSTANCE, RedisCommands.XACK, params.toArray()); + return m.map(v -> new ReactiveRedisConnection.NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> xAdd(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getBody(), "Body must not be null!"); + + byte[] k = toByteArray(command.getKey()); + List params = new LinkedList<>(); + params.add(k); + + if (command.getMaxlen() != null) { + params.add("MAXLEN"); + params.add(command.getMaxlen()); + } + + if (!command.getRecord().getId().shouldBeAutoGenerated()) { + params.add(command.getRecord().getId().getValue()); + } else { + params.add("*"); + } + + for (Map.Entry entry : command.getBody().entrySet()) { + params.add(toByteArray(entry.getKey())); + params.add(toByteArray(entry.getValue())); + } + + Mono m = write(k, StringCodec.INSTANCE, RedisCommands.XADD, params.toArray()); + return m.map(v -> new ReactiveRedisConnection.CommandResponse<>(command, RecordId.of(v.toString()))); + }); + } + + @Override + public Flux> xDel(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRecordIds(), "recordIds must not be null!"); + + byte[] k = toByteArray(command.getKey()); + List params = new ArrayList<>(); + params.add(k); + params.addAll(toStringList(command.getRecordIds())); + + Mono m = write(k, StringCodec.INSTANCE, RedisCommands.XDEL, params.toArray()); + return m.map(v -> new ReactiveRedisConnection.CommandResponse<>(command, v)); + }); + } + + @Override + public Flux> xLen(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] k = toByteArray(command.getKey()); + + Mono m = write(k, StringCodec.INSTANCE, RedisCommands.XLEN, k); + return m.map(v -> new ReactiveRedisConnection.NumericResponse<>(command, v)); + }); + } + + @Override + public Flux>> xRange(Publisher publisher) { + return range(RedisCommands.XRANGE, publisher); + } + + private Flux>> range(RedisCommand rangeCommand, Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + Assert.notNull(command.getLimit(), "Limit must not be null!"); + + byte[] k = toByteArray(command.getKey()); + + List params = new LinkedList<>(); + params.add(k); + + if (rangeCommand == RedisCommands.XRANGE) { + params.add(command.getRange().getLowerBound().getValue().orElse("-")); + params.add(command.getRange().getUpperBound().getValue().orElse("+")); + } else { + params.add(command.getRange().getUpperBound().getValue().orElse("+")); + params.add(command.getRange().getLowerBound().getValue().orElse("-")); + } + + + if (command.getLimit().getCount() > 0) { + params.add("COUNT"); + params.add(command.getLimit().getCount()); + } + + Mono>> m = write(k, ByteArrayCodec.INSTANCE, rangeCommand, params.toArray()); + return m.map(v -> new ReactiveRedisConnection.CommandResponse<>(command, Flux.fromStream(v.entrySet().stream()).map(e -> { + Map map = e.getValue().entrySet().stream() + .collect(Collectors.toMap(entry -> ByteBuffer.wrap(entry.getKey()), + entry -> ByteBuffer.wrap(entry.getValue()))); + return StreamRecords.newRecord() + .in(command.getKey()) + .withId(RecordId.of(e.getKey().toString())) + .ofBuffer(map); + }))); + }); + } + + @Override + public Flux>> read(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getStreamOffsets(), "StreamOffsets must not be null!"); + Assert.notNull(command.getReadOptions(), "ReadOptions must not be null!"); + + List params = new ArrayList<>(); + + if (command.getConsumer() != null) { + params.add("GROUP"); + params.add(command.getConsumer().getGroup()); + params.add(command.getConsumer().getName()); + } + + if (command.getReadOptions().getCount() != null && command.getReadOptions().getCount() > 0) { + params.add("COUNT"); + params.add(command.getReadOptions().getCount()); + } + + if (command.getReadOptions().getBlock() != null && command.getReadOptions().getBlock() > 0) { + params.add("BLOCK"); + params.add(command.getReadOptions().getBlock()); + } + + if (command.getConsumer() != null && command.getReadOptions().isNoack()) { + params.add("NOACK"); + } + + params.add("STREAMS"); + for (StreamOffset streamOffset : command.getStreamOffsets()) { + params.add(toByteArray(streamOffset.getKey())); + } + + for (StreamOffset streamOffset : command.getStreamOffsets()) { + params.add(streamOffset.getOffset().getOffset()); + } + + Mono>>> m; + + if (command.getConsumer() == null) { + if (command.getReadOptions().getBlock() != null && command.getReadOptions().getBlock() > 0) { + m = read(toByteArray(command.getStreamOffsets().get(0).getKey()), ByteArrayCodec.INSTANCE, RedisCommands.XREAD_BLOCKING, params.toArray()); + } else { + m = read(toByteArray(command.getStreamOffsets().get(0).getKey()), ByteArrayCodec.INSTANCE, RedisCommands.XREAD, params.toArray()); + } + } else { + if (command.getReadOptions().getBlock() != null && command.getReadOptions().getBlock() > 0) { + m = read(toByteArray(command.getStreamOffsets().get(0).getKey()), ByteArrayCodec.INSTANCE, RedisCommands.XREADGROUP_BLOCKING, params.toArray()); + } else { + m = read(toByteArray(command.getStreamOffsets().get(0).getKey()), ByteArrayCodec.INSTANCE, RedisCommands.XREADGROUP, params.toArray()); + } + } + + return m.map(v -> new ReactiveRedisConnection.CommandResponse<>(command, Flux.fromStream(v.entrySet().stream()) + .map(ee -> { + return ee.getValue().entrySet().stream().map(e -> { + Map map = e.getValue().entrySet().stream() + .collect(Collectors.toMap(entry -> ByteBuffer.wrap(entry.getKey()), + entry -> ByteBuffer.wrap(entry.getValue()))); + return StreamRecords.newRecord() + .in(ee.getKey()) + .withId(RecordId.of(e.getKey().toString())) + .ofBuffer(map); + }); + }).flatMap(Flux::fromStream) + )); + + }); + } + + private static final RedisStrictCommand XGROUP_STRING = new RedisStrictCommand<>("XGROUP"); + + @Override + public Flux> xGroup(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getGroupName(), "GroupName must not be null!"); + + byte[] k = toByteArray(command.getKey()); + + if (command.getAction().equals(GroupCommand.GroupCommandAction.CREATE)) { + Assert.notNull(command.getReadOffset(), "ReadOffset must not be null!"); + + Mono m = write(k, StringCodec.INSTANCE, XGROUP_STRING, "CREATE", k, command.getGroupName(), command.getReadOffset().getOffset(), "MKSTREAM"); + return m.map(v -> new ReactiveRedisConnection.CommandResponse<>(command, v)); + } + + if (command.getAction().equals(GroupCommand.GroupCommandAction.DELETE_CONSUMER)) { + Assert.notNull(command.getConsumerName(), "ConsumerName must not be null!"); + + Mono m = write(k, StringCodec.INSTANCE, RedisCommands.XGROUP_LONG, "DELCONSUMER", k, command.getGroupName(), command.getConsumerName()); + return m.map(v -> new ReactiveRedisConnection.CommandResponse<>(command, v > 0 ? "OK" : "Error")); + } + + if (command.getAction().equals(GroupCommand.GroupCommandAction.DESTROY)) { + Mono m = write(k, StringCodec.INSTANCE, RedisCommands.XGROUP_LONG, "DESTROY", k, command.getGroupName()); + return m.map(v -> new ReactiveRedisConnection.CommandResponse<>(command, v > 0 ? "OK" : "Error")); + } + + throw new IllegalArgumentException("unknown command " + command.getAction()); + }); + } + + @Override + public Flux>> xRevRange(Publisher publisher) { + return range(RedisCommands.XREVRANGE, publisher); + } + + @Override + public Flux> xTrim(Publisher publisher) { + return execute(publisher, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getCount(), "Count must not be null!"); + + byte[] k = toByteArray(command.getKey()); + + Mono m = write(k, StringCodec.INSTANCE, RedisCommands.XTRIM, k, "MAXLEN", command.getCount()); + return m.map(v -> new ReactiveRedisConnection.NumericResponse<>(command, v)); + }); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveStringCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveStringCommands.java new file mode 100644 index 000000000..ad7cd3dfb --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveStringCommands.java @@ -0,0 +1,459 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.client.protocol.convertor.BooleanReplayConvertor; +import org.redisson.reactive.CommandReactiveExecutor; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.ReactiveRedisConnection.AbsentByteBufferResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.ByteBufferResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.RangeCommand; +import org.springframework.data.redis.connection.ReactiveStringCommands; +import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; +import org.springframework.util.Assert; + +import io.netty.buffer.ByteBuf; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveStringCommands extends RedissonBaseReactive implements ReactiveStringCommands { + + RedissonReactiveStringCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + private static final RedisCommand SET = new RedisCommand("SET", new BooleanReplayConvertor()); + + @Override + public Flux> set(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + byte[] key = toByteArray(command.getKey()); + byte[] value = toByteArray(command.getValue()); + + Mono m = Mono.empty(); + + if (!command.getExpiration().isPresent()) { + m = write(key, StringCodec.INSTANCE, SET, key, value); + } else if (command.getExpiration().get().isPersistent()) { + if (!command.getOption().isPresent() || command.getOption().get() == SetOption.UPSERT) { + m = write(key, StringCodec.INSTANCE, SET, key, value); + } + if (command.getOption().get() == SetOption.SET_IF_ABSENT) { + m = write(key, StringCodec.INSTANCE, SET, key, value, "NX"); + } + if (command.getOption().get() == SetOption.SET_IF_PRESENT) { + m = write(key, StringCodec.INSTANCE, SET, key, value, "XX"); + } + } else { + if (!command.getOption().isPresent() || command.getOption().get() == SetOption.UPSERT) { + m = write(key, StringCodec.INSTANCE, SET, key, value, "PX", command.getExpiration().get().getExpirationTimeInMilliseconds()); + } + if (command.getOption().get() == SetOption.SET_IF_ABSENT) { + m = write(key, StringCodec.INSTANCE, SET, key, value, "PX", command.getExpiration().get().getExpirationTimeInMilliseconds(), "NX"); + } + if (command.getOption().get() == SetOption.SET_IF_PRESENT) { + m = write(key, StringCodec.INSTANCE, SET, key, value, "PX", command.getExpiration().get().getExpirationTimeInMilliseconds(), "XX"); + return m.map(v -> new BooleanResponse<>(command, v)); + } + } + return m.map(v -> new BooleanResponse<>(command, v)) + .switchIfEmpty(Mono.just(new BooleanResponse<>(command, Boolean.FALSE))); + }); + } + + @Override + public Flux> get(Publisher keys) { + return execute(keys, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.GET, keyBuf); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))) + .defaultIfEmpty(new AbsentByteBufferResponse<>(command)); + }); + } + + @Override + public Flux> getSet(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + if (command.getExpiration().isPresent() || command.getOption().isPresent()) { + throw new IllegalArgumentException("Command must not define expiration nor option for GETSET."); + } + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + + Mono m = write(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.GETSET, keyBuf, valueBuf); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))); + }); + } + + @Override + public Flux, ByteBuffer>> mGet(Publisher> keysets) { + return execute(keysets, coll -> { + + Assert.notNull(coll, "List must not be null!"); + + Object[] params = coll.stream().map(buf -> toByteArray(buf)).toArray(Object[]::new); + + Mono> m = read(null, ByteArrayCodec.INSTANCE, RedisCommands.MGET, params); + return m.map(v -> { + List values = v.stream().map(array -> { + if (array == null) { + return ByteBuffer.allocate(0); + } + return ByteBuffer.wrap(array); + }).collect(Collectors.toList()); + return new MultiValueResponse<>(coll, values); + }); + }); + } + + @Override + public Flux> setNX(Publisher values) { + return execute(values, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + if (command.getExpiration().isPresent() || command.getOption().isPresent()) { + throw new IllegalArgumentException("Command must not define expiration nor option for GETSET."); + } + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.SETNX, keyBuf, valueBuf); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + private static final RedisCommand SETEX = new RedisCommand("SETEX", new BooleanReplayConvertor()); + + @Override + public Flux> setEX(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + if (!command.getExpiration().isPresent()) { + throw new IllegalArgumentException("Expiration must not be null!"); + } + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + + Mono m = write(keyBuf, StringCodec.INSTANCE, SETEX, + keyBuf, command.getExpiration().get().getExpirationTimeInSeconds(), valueBuf); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + private static final RedisCommand PSETEX = new RedisCommand("PSETEX"); + + @Override + public Flux> pSetEX(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + if (!command.getExpiration().isPresent()) { + throw new IllegalArgumentException("Expiration must not be null!"); + } + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + + Mono m = write(keyBuf, StringCodec.INSTANCE, PSETEX, + keyBuf, command.getExpiration().get().getExpirationTimeInMilliseconds(), valueBuf); + return m.map(v -> new BooleanResponse<>(command, true)); + }); + } + + private static final RedisCommand MSET = new RedisCommand("MSET", new BooleanReplayConvertor()); + + @Override + public Flux> mSet(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKeyValuePairs(), "KeyValuePairs must not be null!"); + + List params = convert(command); + + Mono m = write(params.get(0), StringCodec.INSTANCE, MSET, params.toArray()); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + protected List convert(MSetCommand command) { + List params = new ArrayList(command.getKeyValuePairs().size()); + command.getKeyValuePairs().entrySet().forEach(e -> { + byte[] keyBuf = toByteArray(e.getKey()); + byte[] valueBuf = toByteArray(e.getValue()); + params.add(keyBuf); + params.add(valueBuf); + }); + return params; + } + + @Override + public Flux> mSetNX(Publisher source) { + return execute(source, command -> { + + Assert.notNull(command.getKeyValuePairs(), "KeyValuePairs must not be null!"); + + List params = convert(command); + + Mono m = write(params.get(0), StringCodec.INSTANCE, RedisCommands.MSETNX, params.toArray()); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand APPEND = new RedisStrictCommand("APPEND"); + + @Override + public Flux> append(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + + Mono m = write(keyBuf, StringCodec.INSTANCE, APPEND, keyBuf, valueBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisCommand GETRANGE = new RedisCommand("GETRANGE"); + + @Override + public Flux> getRange(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = read(keyBuf, ByteArrayCodec.INSTANCE, GETRANGE, + keyBuf, command.getRange().getLowerBound().getValue().orElse(0L), + command.getRange().getUpperBound().getValue().orElse(-1L)); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))); + }); + } + + private static final RedisCommand SETRANGE = new RedisCommand("SETRANGE"); + + @Override + public Flux> setRange(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + Assert.notNull(command.getOffset(), "Offset must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + Mono m = write(keyBuf, StringCodec.INSTANCE, SETRANGE, keyBuf, command.getOffset(), valueBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> getBit(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getOffset(), "Offset must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = read(keyBuf, StringCodec.INSTANCE, RedisCommands.GETBIT, keyBuf, command.getOffset()); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + @Override + public Flux> setBit(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getOffset(), "Offset must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.SETBIT, keyBuf, command.getOffset(), command.getValue() ? 1 : 0); + return m.map(v -> new BooleanResponse<>(command, v)); + }); + } + + @Override + + public Flux> bitCount(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + Range range = command.getRange(); + if (range == null) { + range = Range.unbounded(); + } + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m; + if (range == Range.unbounded()) { + m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.BITCOUNT, keyBuf); + } else { + m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.BITCOUNT, + keyBuf, range.getLowerBound().getValue().orElse(0L), + range.getUpperBound().getValue().get()); + } + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> bitField(Publisher commands) { + return null; + } + + private static final RedisStrictCommand BITOP = new RedisStrictCommand("BITOP"); + + @Override + public Flux> bitOp(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getDestinationKey(), "DestinationKey must not be null!"); + Assert.notEmpty(command.getKeys(), "Keys must not be null or empty"); + + if (command.getBitOp() == BitOperation.NOT && command.getKeys().size() > 1) { + throw new UnsupportedOperationException("NOT operation doesn't support more than single source key"); + } + + List params = new ArrayList(command.getKeys().size() + 2); + params.add(command.getBitOp()); + params.add(toByteArray(command.getDestinationKey())); + params.addAll(command.getKeys().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + + Mono m = write(toByteArray(command.getDestinationKey()), StringCodec.INSTANCE, BITOP, params.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand BITPOS = new RedisStrictCommand("BITPOS"); + + @Override + public Flux> bitPos(Publisher commands) { + return execute(commands, command -> { + + List params = new ArrayList<>(); + params.add(toByteArray(command.getKey())); + params.add(command.getBit() ? 1 : 0); + + if (command.getRange() != null) { + if (command.getRange().getLowerBound().getValue().isPresent()) { + params.add(command.getRange().getLowerBound().getValue().get()); + } + if (command.getRange().getUpperBound().getValue().isPresent()) { + if (!command.getRange().getLowerBound().getValue().isPresent()) { + throw new IllegalArgumentException("LowerBound must not be null"); + } + params.add(command.getRange().getUpperBound().getValue().get()); + } + } + + Mono m = read((byte[])params.get(0), StringCodec.INSTANCE, BITPOS, params.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> strLen(Publisher keys) { + return execute(keys, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = read(keyBuf, StringCodec.INSTANCE, RedisCommands.STRLEN, keyBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisCommand GETDEL = new RedisCommand<>("GETDEL"); + + @Override + public Flux> getDel(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, ByteArrayCodec.INSTANCE, GETDEL, keyBuf); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))) + .defaultIfEmpty(new AbsentByteBufferResponse<>(command)); + }); + } + + private static final RedisCommand GETEX = new RedisCommand<>("GETEX"); + + @Override + public Flux> getEx(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, ByteArrayCodec.INSTANCE, GETEX, keyBuf, + "PX", command.getExpiration().getExpirationTimeInMilliseconds()); + return m.map(v -> new ByteBufferResponse<>(command, ByteBuffer.wrap(v))) + .defaultIfEmpty(new AbsentByteBufferResponse<>(command)); + }); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveSubscription.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveSubscription.java new file mode 100644 index 000000000..a1985da20 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveSubscription.java @@ -0,0 +1,313 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.BaseRedisPubSubListener; +import org.redisson.client.ChannelName; +import org.redisson.client.RedisPubSubListener; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.Codec; +import org.redisson.client.protocol.pubsub.PubSubType; +import org.redisson.connection.ConnectionManager; +import org.redisson.pubsub.PubSubConnectionEntry; +import org.redisson.pubsub.PublishSubscribeService; +import org.springframework.data.redis.connection.ReactiveSubscription; +import org.springframework.data.redis.connection.SubscriptionListener; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveSubscription implements ReactiveSubscription { + + public static class ListenableCounter { + + private int state; + private Runnable r; + + public synchronized void acquire() { + state++; + } + + public void release() { + synchronized (this) { + state--; + if (state != 0) { + return; + } + } + + if (r != null) { + r.run(); + r = null; + } + } + + public synchronized void addListener(Runnable r) { + synchronized (this) { + if (state != 0) { + this.r = r; + return; + } + } + + r.run(); + } + + } + + private final Map channels = new ConcurrentHashMap<>(); + private final Map> patterns = new ConcurrentHashMap<>(); + + private final ListenableCounter monosListener = new ListenableCounter(); + + private final RedisPubSubListener subscriptionListener; + private final PublishSubscribeService subscribeService; + + public RedissonReactiveSubscription(ConnectionManager connectionManager, SubscriptionListener subscriptionListener) { + this.subscribeService = connectionManager.getSubscribeService(); + this.subscriptionListener = new RedisPubSubListener() { + + @Override + public boolean onStatus(PubSubType type, CharSequence channel) { + if (type == PubSubType.SUBSCRIBE) { + subscriptionListener.onChannelSubscribed(channel.toString().getBytes(StandardCharsets.UTF_8), 1L); + } else if (type == PubSubType.PSUBSCRIBE) { + subscriptionListener.onPatternSubscribed(channel.toString().getBytes(StandardCharsets.UTF_8), 1L); + } else if (type == PubSubType.UNSUBSCRIBE) { + subscriptionListener.onChannelUnsubscribed(channel.toString().getBytes(StandardCharsets.UTF_8), 1L); + } else if (type == PubSubType.PUNSUBSCRIBE) { + subscriptionListener.onPatternUnsubscribed(channel.toString().getBytes(StandardCharsets.UTF_8), 1L); + } + return true; + } + + @Override + public void onPatternMessage(CharSequence pattern, CharSequence channel, Object message) { + } + + @Override + public void onMessage(CharSequence channel, Object msg) { + } + }; + } + + @Override + public Mono subscribe(ByteBuffer... channels) { + monosListener.acquire(); + return Mono.defer(() -> { + List> futures = new ArrayList<>(); + for (ByteBuffer channel : channels) { + ChannelName cn = toChannelName(channel); + CompletableFuture f = subscribeService.subscribe(ByteArrayCodec.INSTANCE, cn, subscriptionListener); + f = f.whenComplete((res, e) -> RedissonReactiveSubscription.this.channels.put(cn, res)); + futures.add(f); + } + + CompletableFuture future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + future = future.whenComplete((r, e) -> { + monosListener.release(); + }); + return Mono.fromFuture(future); + }); + } + + protected ChannelName toChannelName(ByteBuffer channel) { + return new ChannelName(RedissonBaseReactive.toByteArray(channel)); + } + + @Override + public Mono pSubscribe(ByteBuffer... patterns) { + monosListener.acquire(); + return Mono.defer(() -> { + List> futures = new ArrayList<>(); + for (ByteBuffer channel : patterns) { + ChannelName cn = toChannelName(channel); + CompletableFuture> f = subscribeService.psubscribe(cn, ByteArrayCodec.INSTANCE, subscriptionListener); + f = f.whenComplete((res, e) -> RedissonReactiveSubscription.this.patterns.put(cn, res)); + futures.add(f); + } + + CompletableFuture future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + future = future.whenComplete((r, e) -> { + monosListener.release(); + }); + return Mono.fromFuture(future); + }); + } + + @Override + public Mono unsubscribe() { + return unsubscribe(channels.keySet().stream().map(b -> ByteBuffer.wrap(b.getName())).distinct().toArray(ByteBuffer[]::new)); + } + + @Override + public Mono unsubscribe(ByteBuffer... channels) { + monosListener.acquire(); + return Mono.defer(() -> { + List> futures = new ArrayList<>(channels.length); + for (ByteBuffer channel : channels) { + ChannelName cn = toChannelName(channel); + CompletableFuture f = subscribeService.unsubscribe(cn, PubSubType.UNSUBSCRIBE); + f = f.whenComplete((res, e) -> { + synchronized (RedissonReactiveSubscription.this.channels) { + PubSubConnectionEntry entry = RedissonReactiveSubscription.this.channels.get(cn); + if (!entry.hasListeners(cn)) { + RedissonReactiveSubscription.this.channels.remove(cn); + } + } + }); + futures.add(f); + } + + CompletableFuture future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + future = future.whenComplete((r, e) -> { + monosListener.release(); + }); + return Mono.fromFuture(future); + }); + } + + @Override + public Mono pUnsubscribe() { + return unsubscribe(patterns.keySet().stream().map(b -> ByteBuffer.wrap(b.getName())).distinct().toArray(ByteBuffer[]::new)); + } + + @Override + public Mono pUnsubscribe(ByteBuffer... patterns) { + monosListener.acquire(); + return Mono.defer(() -> { + List> futures = new ArrayList<>(patterns.length); + for (ByteBuffer channel : patterns) { + ChannelName cn = toChannelName(channel); + CompletableFuture f = subscribeService.unsubscribe(cn, PubSubType.PUNSUBSCRIBE); + f = f.whenComplete((res, e) -> { + synchronized (RedissonReactiveSubscription.this.patterns) { + Collection entries = RedissonReactiveSubscription.this.patterns.get(cn); + entries.stream() + .filter(en -> en.hasListeners(cn)) + .forEach(ee -> RedissonReactiveSubscription.this.patterns.remove(cn)); + } + }); + futures.add(f); + } + + CompletableFuture future = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); + future = future.whenComplete((r, e) -> { + monosListener.release(); + }); + return Mono.fromFuture(future); + }); + } + + @Override + public Set getChannels() { + return channels.keySet().stream().map(b -> ByteBuffer.wrap(b.getName())).collect(Collectors.toSet()); + } + + @Override + public Set getPatterns() { + return patterns.keySet().stream().map(b -> ByteBuffer.wrap(b.getName())).collect(Collectors.toSet()); + } + + private final AtomicReference>> flux = new AtomicReference<>(); + private volatile Disposable disposable; + + @Override + public Flux> receive() { + if (flux.get() != null) { + return flux.get(); + } + + Flux> f = Flux.create(emitter -> { + emitter.onRequest(n -> { + + monosListener.addListener(() -> { + BaseRedisPubSubListener listener = new BaseRedisPubSubListener() { + @Override + public void onPatternMessage(CharSequence pattern, CharSequence channel, Object message) { + if (!patterns.containsKey(new ChannelName(pattern.toString()))) { + return; + } + + emitter.next(new PatternMessage<>(ByteBuffer.wrap(pattern.toString().getBytes()), + ByteBuffer.wrap(channel.toString().getBytes()), + ByteBuffer.wrap((byte[])message))); + } + + @Override + public void onMessage(CharSequence channel, Object msg) { + if (!channels.containsKey(new ChannelName(channel.toString()))) { + return; + } + + emitter.next(new ChannelMessage<>(ByteBuffer.wrap(channel.toString().getBytes()), ByteBuffer.wrap((byte[])msg))); + } + }; + + disposable = () -> { + for (Entry entry : channels.entrySet()) { + entry.getValue().removeListener(entry.getKey(), listener); + } + for (Entry> entry : patterns.entrySet()) { + for (PubSubConnectionEntry pubSubConnectionEntry : entry.getValue()) { + pubSubConnectionEntry.removeListener(entry.getKey(), listener); + } + } + }; + + for (Entry entry : channels.entrySet()) { + entry.getValue().addListener(entry.getKey(), listener); + } + for (Entry> entry : patterns.entrySet()) { + for (PubSubConnectionEntry pubSubConnectionEntry : entry.getValue()) { + pubSubConnectionEntry.addListener(entry.getKey(), listener); + } + } + + emitter.onDispose(disposable); + }); + }); + }); + + if (flux.compareAndSet(null, f)) { + return f; + } + return flux.get(); + } + + @Override + public Mono cancel() { + return unsubscribe().then(pUnsubscribe()).then(Mono.fromRunnable(() -> { + if (disposable != null) { + disposable.dispose(); + } + })); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveZSetCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveZSetCommands.java new file mode 100644 index 000000000..d11c14bd5 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonReactiveZSetCommands.java @@ -0,0 +1,888 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.reactivestreams.Publisher; +import org.redisson.ScanResult; +import org.redisson.api.RFuture; +import org.redisson.client.RedisClient; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.DoubleCodec; +import org.redisson.client.codec.LongCodec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.client.protocol.convertor.DoubleNullSafeReplayConvertor; +import org.redisson.client.protocol.decoder.*; +import org.redisson.reactive.CommandReactiveExecutor; +import org.redisson.reactive.SetReactiveIterator; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.redis.connection.ReactiveListCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyScanCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.ReactiveZSetCommands; +import org.springframework.data.redis.connection.zset.DefaultTuple; +import org.springframework.data.redis.connection.zset.Tuple; +import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.redisson.client.protocol.RedisCommands.ZRANDMEMBER; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonReactiveZSetCommands extends RedissonBaseReactive implements ReactiveZSetCommands { + + RedissonReactiveZSetCommands(CommandReactiveExecutor executorService) { + super(executorService); + } + + private static final RedisCommand ZADD_FLOAT = new RedisCommand<>("ZADD", new DoubleNullSafeReplayConvertor()); + + @Override + public Flux> zAdd(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notEmpty(command.getTuples(), "Tuples must not be empty or null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + List params = new ArrayList(command.getTuples().size()*2+1); + params.add(keyBuf); + if (command.isIncr() || command.isUpsert() || command.isReturnTotalChanged()) { + if (command.isUpsert()) { + params.add("NX"); + } else { + params.add("XX"); + } + if (command.isReturnTotalChanged()) { + params.add("CH"); + } + if (command.isIncr()) { + params.add("INCR"); + } + } + + for (Tuple entry : command.getTuples()) { + params.add(BigDecimal.valueOf(entry.getScore()).toPlainString()); + params.add(entry.getValue()); + } + + Mono m; + if (command.isIncr()) { + m = write(keyBuf, DoubleCodec.INSTANCE, ZADD_FLOAT, params.toArray()); + } else { + m = write(keyBuf, StringCodec.INSTANCE, RedisCommands.ZADD, params.toArray()); + } + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> zRem(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValues(), "Values must not be null!"); + + List args = new ArrayList(command.getValues().size() + 1); + args.add(toByteArray(command.getKey())); + args.addAll(command.getValues().stream().map(v -> toByteArray(v)).collect(Collectors.toList())); + + Mono m = write((byte[])args.get(0), StringCodec.INSTANCE, RedisCommands.ZREM_LONG, args.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> zIncrBy(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Member must not be null!"); + Assert.notNull(command.getIncrement(), "Increment value must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + Mono m = write(keyBuf, DoubleCodec.INSTANCE, RedisCommands.ZINCRBY, keyBuf, new BigDecimal(command.getIncrement().doubleValue()).toPlainString(), valueBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> zRank(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Member must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + RedisCommand cmd = RedisCommands.ZRANK; + if (command.getDirection() == Direction.DESC) { + cmd = RedisCommands.ZREVRANK; + } + Mono m = read(keyBuf, DoubleCodec.INSTANCE, cmd, keyBuf, valueBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisCommand> ZRANGE_ENTRY = new RedisCommand>("ZRANGE", new ScoredSortedSetReplayDecoder()); + private static final RedisCommand> ZRANGE = new RedisCommand>("ZRANGE", new ObjectSetReplayDecoder()); + private static final RedisCommand> ZREVRANGE_ENTRY = new RedisCommand>("ZREVRANGE", new ScoredSortedSetReplayDecoder()); + private static final RedisCommand> ZREVRANGE = new RedisCommand>("ZREVRANGE", new ObjectSetReplayDecoder()); + + @Override + public Flux>> zRange(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + long start = command.getRange().getLowerBound().getValue().orElse(0L); + long end = command.getRange().getUpperBound().getValue().get(); + + Flux flux; + if (command.getDirection() == Direction.ASC) { + if (command.isWithScores()) { + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, ZRANGE_ENTRY, + keyBuf, start, end, "WITHSCORES"); + flux = m.flatMapMany(e -> Flux.fromIterable(e)); + } else { + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, ZRANGE, keyBuf, start, end); + flux = m.flatMapMany(e -> Flux.fromIterable(e).map(b -> new DefaultTuple(b, Double.NaN))); + } + } else { + if (command.isWithScores()) { + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, ZREVRANGE_ENTRY, + keyBuf, start, end, "WITHSCORES"); + flux = m.flatMapMany(e -> Flux.fromIterable(e)); + } else { + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, ZREVRANGE, keyBuf, start, end); + flux = m.flatMapMany(e -> Flux.fromIterable(e).map(b -> new DefaultTuple(b, Double.NaN))); + } + } + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + private static final RedisCommand> ZRANGEBYSCORE = new RedisCommand>("ZRANGEBYSCORE", new ScoredSortedSetReplayDecoder()); + private static final RedisCommand> ZREVRANGEBYSCORE = new RedisCommand>("ZREVRANGEBYSCORE", new ScoredSortedSetReplayDecoder()); + + @Override + public Flux>> zRangeByScore( + Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + String start = toLowerBound(command.getRange()); + String end = toUpperBound(command.getRange()); + + List args = new ArrayList(); + args.add(keyBuf); + if (command.getDirection() == Direction.ASC) { + args.add(start); + } else { + args.add(end); + } + if (command.getDirection() == Direction.ASC) { + args.add(end); + } else { + args.add(start); + } + if (command.isWithScores()) { + args.add("WITHSCORES"); + } + if (command.getLimit().isPresent() && !command.getLimit().get().isUnlimited()) { + args.add("LIMIT"); + args.add(command.getLimit().get().getOffset()); + args.add(command.getLimit().get().getCount()); + } + + Flux flux; + if (command.getDirection() == Direction.ASC) { + if (command.isWithScores()) { + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, ZRANGEBYSCORE, args.toArray()); + flux = m.flatMapMany(e -> Flux.fromIterable(e)); + } else { + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.ZRANGEBYSCORE, args.toArray()); + flux = m.flatMapMany(e -> Flux.fromIterable(e).map(b -> new DefaultTuple(b, Double.NaN))); + } + } else { + if (command.isWithScores()) { + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, ZREVRANGEBYSCORE, args.toArray()); + flux = m.flatMapMany(e -> Flux.fromIterable(e)); + } else { + Mono> m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.ZREVRANGEBYSCORE, args.toArray()); + flux = m.flatMapMany(e -> Flux.fromIterable(e).map(b -> new DefaultTuple(b, Double.NaN))); + } + } + + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + private static final RedisCommand> ZSCAN = new RedisCommand<>("ZSCAN", new ListMultiDecoder2(new ScoredSortedSetScanDecoder(), new ScoredSortedSetScanReplayDecoder())); + + @Override + public Flux>> zScan(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getOptions(), "ScanOptions must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Flux flux = Flux.create(new SetReactiveIterator() { + @Override + protected RFuture> scanIterator(RedisClient client, long nextIterPos) { + if (command.getOptions().getPattern() == null) { + return executorService.readAsync(client, keyBuf, ByteArrayCodec.INSTANCE, ZSCAN, + keyBuf, nextIterPos, "COUNT", Optional.ofNullable(command.getOptions().getCount()).orElse(10L)); + } + + return executorService.readAsync(client, keyBuf, ByteArrayCodec.INSTANCE, ZSCAN, + keyBuf, nextIterPos, "MATCH", command.getOptions().getPattern(), + "COUNT", Optional.ofNullable(command.getOptions().getCount()).orElse(10L)); + } + }); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + private static final RedisStrictCommand ZCOUNT = new RedisStrictCommand("ZCOUNT"); + + String toLowerBound(Range range) { + StringBuilder s = new StringBuilder(); + if (!range.getLowerBound().isInclusive()) { + s.append("("); + } + if (!range.getLowerBound().getValue().isPresent() || range.getLowerBound().getValue().get().toString().isEmpty()) { + s.append("-inf"); + } else { + s.append(range.getLowerBound().getValue().get()); + } + return s.toString(); + } + + String toUpperBound(Range range) { + StringBuilder s = new StringBuilder(); + if (!range.getUpperBound().isInclusive()) { + s.append("("); + } + if (!range.getUpperBound().getValue().isPresent() || range.getUpperBound().getValue().get().toString().isEmpty()) { + s.append("+inf"); + } else { + s.append(range.getUpperBound().getValue().get()); + } + return s.toString(); + } + + String toLexLowerBound(Range range, Object defaultValue) { + StringBuilder s = new StringBuilder(); + if (range.getLowerBound().isInclusive()) { + s.append("["); + } else { + s.append("("); + } + if (!range.getLowerBound().getValue().isPresent() || range.getLowerBound().getValue().get().toString().isEmpty()) { + s.append(defaultValue); + } else { + s.append(range.getLowerBound().getValue().get()); + } + return s.toString(); + } + + String toLexUpperBound(Range range, Object defaultValue) { + StringBuilder s = new StringBuilder(); + if (range.getUpperBound().isInclusive()) { + s.append("["); + } else { + s.append("("); + } + if (!range.getUpperBound().getValue().isPresent() || range.getUpperBound().getValue().get().toString().isEmpty()) { + s.append(defaultValue); + } else { + s.append(range.getUpperBound().getValue().get()); + } + return s.toString(); + } + + @Override + public Flux> zCount(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = read(keyBuf, StringCodec.INSTANCE, ZCOUNT, + keyBuf, toLowerBound(command.getRange()), + toUpperBound(command.getRange())); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> zCard(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = read(keyBuf, StringCodec.INSTANCE, RedisCommands.ZCARD, keyBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> zScore(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + byte[] valueBuf = toByteArray(command.getValue()); + Mono m = read(keyBuf, StringCodec.INSTANCE, RedisCommands.ZSCORE, keyBuf, valueBuf); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand ZREMRANGEBYRANK = new RedisStrictCommand("ZREMRANGEBYRANK"); + + @Override + public Flux> zRemRangeByRank( + Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, ZREMRANGEBYRANK, + keyBuf, command.getRange().getLowerBound().getValue().orElse(0L), + command.getRange().getUpperBound().getValue().get()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand ZREMRANGEBYSCORE = new RedisStrictCommand("ZREMRANGEBYSCORE"); + + @Override + public Flux> zRemRangeByScore( + Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + Mono m = write(keyBuf, StringCodec.INSTANCE, ZREMRANGEBYSCORE, + keyBuf, toLowerBound(command.getRange()), + toUpperBound(command.getRange())); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand ZUNIONSTORE = new RedisStrictCommand("ZUNIONSTORE"); + + @Override + public Flux> zUnionStore(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Destination key must not be null!"); + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null or empty!"); + + byte[] keyBuf = toByteArray(command.getKey()); + List args = new ArrayList(command.getSourceKeys().size() * 2 + 5); + args.add(keyBuf); + args.add(command.getSourceKeys().size()); + args.addAll(command.getSourceKeys().stream().map(e -> toByteArray(e)).collect(Collectors.toList())); + if (!command.getWeights().isEmpty()) { + args.add("WEIGHTS"); + for (Double weight : command.getWeights()) { + args.add(BigDecimal.valueOf(weight).toPlainString()); + } + } + if (command.getAggregateFunction().isPresent()) { + args.add("AGGREGATE"); + args.add(command.getAggregateFunction().get().name()); + } + Mono m = write(keyBuf, LongCodec.INSTANCE, ZUNIONSTORE, args.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand ZINTERSTORE = new RedisStrictCommand("ZINTERSTORE"); + + @Override + public Flux> zInterStore(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Destination key must not be null!"); + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null or empty!"); + + byte[] keyBuf = toByteArray(command.getKey()); + List args = new ArrayList(command.getSourceKeys().size() * 2 + 5); + args.add(keyBuf); + args.add(command.getSourceKeys().size()); + args.addAll(command.getSourceKeys().stream().map(e -> toByteArray(e)).collect(Collectors.toList())); + if (!command.getWeights().isEmpty()) { + args.add("WEIGHTS"); + for (Double weight : command.getWeights()) { + args.add(BigDecimal.valueOf(weight).toPlainString()); + } + } + if (command.getAggregateFunction().isPresent()) { + args.add("AGGREGATE"); + args.add(command.getAggregateFunction().get().name()); + } + Mono m = write(keyBuf, LongCodec.INSTANCE, ZINTERSTORE, args.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisCommand> ZRANGEBYLEX = new RedisCommand>("ZRANGEBYLEX", new ObjectSetReplayDecoder()); + private static final RedisCommand> ZREVRANGEBYLEX = new RedisCommand>("ZREVRANGEBYLEX", new ObjectSetReplayDecoder()); + + @Override + public Flux>> zRangeByLex( + Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + String start = null; + String end = null; + if (command.getDirection() == Direction.ASC) { + start = toLexLowerBound(command.getRange(), "-"); + end = toLexUpperBound(command.getRange(), "+"); + } else { + start = toLexUpperBound(command.getRange(), "-"); + end = toLexLowerBound(command.getRange(), "+"); + } + + Mono> m; + if (!command.getLimit().isUnlimited()) { + if (command.getDirection() == Direction.ASC) { + m = read(keyBuf, ByteArrayCodec.INSTANCE, ZRANGEBYLEX, + keyBuf, start, end, "LIMIT", command.getLimit().getOffset(), command.getLimit().getCount()); + } else { + m = read(keyBuf, ByteArrayCodec.INSTANCE, ZREVRANGEBYLEX, + keyBuf, start, end, "LIMIT", command.getLimit().getOffset(), command.getLimit().getCount()); + } + } else { + if (command.getDirection() == Direction.ASC) { + m = read(keyBuf, ByteArrayCodec.INSTANCE, ZRANGEBYLEX, + keyBuf, start, end); + } else { + m = read(keyBuf, ByteArrayCodec.INSTANCE, ZREVRANGEBYLEX, + keyBuf, start, end); + } + } + Flux flux = m.flatMapMany(e -> Flux.fromIterable(e).map(v -> ByteBuffer.wrap(v))); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + public Flux> lPos(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getElement(), "Element must not be null!"); + + List params = new ArrayList(); + byte[] keyBuf = toByteArray(command.getKey()); + params.add(keyBuf); + params.add(toByteArray(command.getElement())); + if (command.getRank() != null) { + params.add("RANK"); + params.add(command.getRank()); + } + if (command.getCount() != null) { + params.add("COUNT"); + params.add(command.getCount()); + } + + Mono m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.LPOS, params.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux> zLexCount(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + String start = toLexLowerBound(command.getRange(), "-"); + String end = toLexUpperBound(command.getRange(), "+"); + + Mono m = read(keyBuf, ByteArrayCodec.INSTANCE, RedisCommands.ZLEXCOUNT, keyBuf, start, end); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisStrictCommand ZREMRANGEBYLEX = new RedisStrictCommand<>("ZREMRANGEBYLEX"); + + @Override + public Flux> zRemRangeByLex(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + String start = toLexLowerBound(command.getRange(), "-"); + String end = toLexUpperBound(command.getRange(), "+"); + + Mono m = write(keyBuf, StringCodec.INSTANCE, ZREMRANGEBYLEX, keyBuf, start, end); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + private static final RedisCommand> ZPOPMIN = new RedisCommand<>("ZPOPMIN", new ScoredSortedSetReplayDecoder()); + private static final RedisCommand> ZPOPMAX = new RedisCommand<>("ZPOPMAX", new ScoredSortedSetReplayDecoder()); + + @Override + public Flux>> zPop(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + RedisCommand> cmd = ZPOPMAX; + if (command.getDirection() == PopDirection.MIN) { + cmd = ZPOPMIN; + } + + Mono> m = write(keyBuf, ByteArrayCodec.INSTANCE, cmd, keyBuf, command.getCount()); + Flux flux = m.flatMapMany(e -> Flux.fromIterable(e)); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + private static final RedisCommand> BZPOPMIN = new RedisCommand<>("BZPOPMIN", new ScoredSortedSetReplayDecoder()); + private static final RedisCommand> BZPOPMAX = new RedisCommand<>("BZPOPMAX", new ScoredSortedSetReplayDecoder()); + + @Override + public Flux>> bZPop(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getTimeout(), "Timeout must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + RedisCommand> cmd = BZPOPMAX; + if (command.getDirection() == PopDirection.MIN) { + cmd = BZPOPMIN; + } + + long timeout = command.getTimeUnit().toSeconds(command.getTimeout()); + + Mono> m = write(keyBuf, ByteArrayCodec.INSTANCE, cmd, keyBuf, command.getCount(), timeout); + Flux flux = m.flatMapMany(e -> Flux.fromIterable(e)); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + @Override + public Flux>> zRandMember(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + Mono> m = write(keyBuf, ByteArrayCodec.INSTANCE, ZRANDMEMBER, keyBuf, command.getCount()); + Flux flux = m.flatMapMany(e -> Flux.fromIterable(e).map(v -> ByteBuffer.wrap(v))); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + private static final RedisCommand> ZRANDMEMBER_SCORE = new RedisCommand<>("ZRANDMEMBER", new ScoredSortedSetReplayDecoder()); + + @Override + public Flux>> zRandMemberWithScore(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + + Mono> m = write(keyBuf, ByteArrayCodec.INSTANCE, ZRANDMEMBER_SCORE, keyBuf, command.getCount(), "WITHSCORES"); + Flux flux = m.flatMapMany(e -> Flux.fromIterable(e)); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + @Override + public Flux>> zDiff(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKeys(), "Key must not be null!"); + + List args = new ArrayList<>(command.getKeys().size() + 1); + args.add(command.getKeys().size()); + for (ByteBuffer key : command.getKeys()) { + args.add(toByteArray(key)); + } + + Mono> m = write(toByteArray(command.getKeys().get(0)), ByteArrayCodec.INSTANCE, RedisCommands.ZDIFF, args.toArray()); + Flux flux = m.flatMapMany(e -> Flux.fromIterable(e).map(v -> ByteBuffer.wrap(v))); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + private static final RedisCommand> ZDIFF_SCORE = new RedisCommand<>("ZDIFF", new ScoredSortedSetReplayDecoder()); + + @Override + public Flux>> zDiffWithScores(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + List args = new ArrayList<>(command.getKeys().size() + 2); + args.add(command.getKeys().size()); + for (ByteBuffer key : command.getKeys()) { + args.add(toByteArray(key)); + } + args.add("WITHSCORES"); + + Mono> m = write(toByteArray(command.getKeys().get(0)), ByteArrayCodec.INSTANCE, ZDIFF_SCORE, args.toArray()); + Flux flux = m.flatMapMany(e -> Flux.fromIterable(e)); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + private static final RedisStrictCommand ZDIFFSTORE = new RedisStrictCommand<>("ZDIFFSTORE"); + + @Override + public Flux> zDiffStore(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getSourceKeys(), "Source keys must not be null!"); + + List args = new ArrayList<>(command.getSourceKeys().size() + 2); + byte[] keyBuf = toByteArray(command.getKey()); + args.add(keyBuf); + args.add(command.getSourceKeys().size()); + for (ByteBuffer key : command.getSourceKeys()) { + args.add(toByteArray(key)); + } + + Mono m = write(keyBuf, StringCodec.INSTANCE, ZDIFFSTORE, args.toArray()); + return m.map(v -> new NumericResponse<>(command, v)); + }); + } + + @Override + public Flux>> zUnion(Publisher commands) { + return execute(commands, command -> { + + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null or empty!"); + + List args = new ArrayList<>(command.getSourceKeys().size() * 2 + 5); + args.add(command.getSourceKeys().size()); + args.addAll(command.getSourceKeys().stream().map(e -> toByteArray(e)).collect(Collectors.toList())); + if (!command.getWeights().isEmpty()) { + args.add("WEIGHTS"); + for (Double weight : command.getWeights()) { + args.add(BigDecimal.valueOf(weight).toPlainString()); + } + } + if (command.getAggregateFunction().isPresent()) { + args.add("AGGREGATE"); + args.add(command.getAggregateFunction().get().name()); + } + + Mono> m = write(toByteArray(command.getSourceKeys().get(0)), ByteArrayCodec.INSTANCE, RedisCommands.ZUNION, args.toArray()); + Flux flux = m.flatMapMany(e -> Flux.fromIterable(e).map(v -> ByteBuffer.wrap(v))); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + private static final RedisCommand> ZUNION_SCORE = new RedisCommand<>("ZUNION", new ScoredSortedSetReplayDecoder()); + + @Override + public Flux>> zUnionWithScores(Publisher commands) { + return execute(commands, command -> { + + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null or empty!"); + + List args = new ArrayList<>(command.getSourceKeys().size() * 2 + 5); + args.add(command.getSourceKeys().size()); + args.addAll(command.getSourceKeys().stream().map(e -> toByteArray(e)).collect(Collectors.toList())); + if (!command.getWeights().isEmpty()) { + args.add("WEIGHTS"); + for (Double weight : command.getWeights()) { + args.add(BigDecimal.valueOf(weight).toPlainString()); + } + } + if (command.getAggregateFunction().isPresent()) { + args.add("AGGREGATE"); + args.add(command.getAggregateFunction().get().name()); + } + args.add("WITHSCORES"); + + Mono> m = write(toByteArray(command.getSourceKeys().get(0)), ByteArrayCodec.INSTANCE, ZUNION_SCORE, args.toArray()); + Flux flux = m.flatMapMany(e -> Flux.fromIterable(e)); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + @Override + public Flux>> zInter(Publisher commands) { + return execute(commands, command -> { + + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null or empty!"); + + List args = new ArrayList<>(command.getSourceKeys().size() * 2 + 5); + args.add(command.getSourceKeys().size()); + args.addAll(command.getSourceKeys().stream().map(e -> toByteArray(e)).collect(Collectors.toList())); + if (!command.getWeights().isEmpty()) { + args.add("WEIGHTS"); + for (Double weight : command.getWeights()) { + args.add(BigDecimal.valueOf(weight).toPlainString()); + } + } + if (command.getAggregateFunction().isPresent()) { + args.add("AGGREGATE"); + args.add(command.getAggregateFunction().get().name()); + } + + Mono> m = write(toByteArray(command.getSourceKeys().get(0)), ByteArrayCodec.INSTANCE, RedisCommands.ZINTER, args.toArray()); + Flux flux = m.flatMapMany(e -> Flux.fromIterable(e).map(v -> ByteBuffer.wrap(v))); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + private static final RedisCommand> ZINTER_SCORE = new RedisCommand<>("ZINTER", new ScoredSortedSetReplayDecoder()); + + @Override + public Flux>> zInterWithScores(Publisher commands) { + return execute(commands, command -> { + + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null or empty!"); + + List args = new ArrayList<>(command.getSourceKeys().size() * 2 + 5); + args.add(command.getSourceKeys().size()); + args.addAll(command.getSourceKeys().stream().map(e -> toByteArray(e)).collect(Collectors.toList())); + if (!command.getWeights().isEmpty()) { + args.add("WEIGHTS"); + for (Double weight : command.getWeights()) { + args.add(BigDecimal.valueOf(weight).toPlainString()); + } + } + if (command.getAggregateFunction().isPresent()) { + args.add("AGGREGATE"); + args.add(command.getAggregateFunction().get().name()); + } + args.add("WITHSCORES"); + + Mono> m = write(toByteArray(command.getSourceKeys().get(0)), ByteArrayCodec.INSTANCE, ZINTER_SCORE, args.toArray()); + Flux flux = m.flatMapMany(e -> Flux.fromIterable(e)); + return Mono.just(new CommandResponse<>(command, flux)); + }); + } + + private static final RedisCommand> ZMSCORE = new RedisCommand<>("ZMSCORE", new ObjectListReplayDecoder<>()); + + @Override + public Flux> zMScore(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValues(), "Values must not be null!"); + + byte[] keyBuf = toByteArray(command.getKey()); + List args = new ArrayList<>(command.getValues().size() + 1); + args.add(keyBuf); + args.addAll(command.getValues().stream().map(buf -> toByteArray(buf)).collect(Collectors.toList())); + + Mono> m = read(keyBuf, DoubleCodec.INSTANCE, ZMSCORE, args.toArray()); + return m.map(v -> new ReactiveRedisConnection.MultiValueResponse<>(command, v)); + }); + } + + @Override + public Flux>> zRangeStore(Publisher commands) { + return execute(commands, command -> { + + Assert.notNull(command.getKey(), "Source key must not be null"); + Assert.notNull(command.getDestKey(), "Destination key must not be null"); + Assert.notNull(command.getRange(), "Range must not be null"); + Assert.notNull(command.getLimit(), "Limit must not be null"); + +// Limit limit = LettuceConverters.toLimit(command.getLimit()); +// Mono result; +// +// if (command.getDirection() == Direction.ASC) { +// +// switch (command.getRangeMode()) { +// case ByScore -> result = cmd.zrangestorebyscore(command.getDestKey(), command.getKey(), +// (Range) LettuceConverters.toRange(command.getRange()), limit); +// case ByLex -> result = cmd.zrangestorebylex(command.getDestKey(), command.getKey(), +// RangeConverter.toRange(command.getRange()), limit); +// default -> throw new IllegalStateException("Unsupported value: " + command.getRangeMode()); +// } +// } else { +// switch (command.getRangeMode()) { +// case ByScore -> result = cmd.zrevrangestorebyscore(command.getDestKey(), command.getKey(), +// (Range) LettuceConverters.toRange(command.getRange()), limit); +// case ByLex -> result = cmd.zrevrangestorebylex(command.getDestKey(), command.getKey(), +// RangeConverter.toRange(command.getRange()), limit); +// default -> throw new IllegalStateException("Unsupported value: " + command.getRangeMode()); +// } +// } + + return Mono.empty(); +// return Mono.just(new CommandResponse<>(command, result)); + }); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonSentinelConnection.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonSentinelConnection.java new file mode 100644 index 000000000..7b53cb5cf --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonSentinelConnection.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.redisson.client.RedisConnection; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommands; +import org.springframework.data.redis.connection.NamedNode; +import org.springframework.data.redis.connection.RedisSentinelConnection; +import org.springframework.data.redis.connection.RedisServer; +import org.springframework.data.redis.connection.convert.Converters; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonSentinelConnection implements RedisSentinelConnection { + + private final RedisConnection connection; + + public RedissonSentinelConnection(RedisConnection connection) { + this.connection = connection; + } + + @Override + public void failover(NamedNode master) { + connection.sync(RedisCommands.SENTINEL_FAILOVER, master.getName()); + } + + private static List toRedisServersList(List> source) { + List servers = new ArrayList(source.size()); + for (Map info : source) { + servers.add(RedisServer.newServerFrom(Converters.toProperties(info))); + } + return servers; + } + + @Override + public Collection masters() { + List> masters = connection.sync(StringCodec.INSTANCE, RedisCommands.SENTINEL_MASTERS); + return toRedisServersList(masters); + } + + @Override + public Collection replicas(NamedNode master) { + List> slaves = connection.sync(StringCodec.INSTANCE, RedisCommands.SENTINEL_SLAVES, master.getName()); + return toRedisServersList(slaves); + } + + @Override + public void remove(NamedNode master) { + connection.sync(RedisCommands.SENTINEL_REMOVE, master.getName()); + } + + @Override + public void monitor(RedisServer master) { + connection.sync(RedisCommands.SENTINEL_MONITOR, master.getName(), master.getHost(), + master.getPort().intValue(), master.getQuorum().intValue()); + } + + @Override + public void close() throws IOException { + connection.closeAsync(); + } + + @Override + public boolean isOpen() { + return !connection.isClosed(); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonStreamCommands.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonStreamCommands.java new file mode 100644 index 000000000..4cd5f0aff --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonStreamCommands.java @@ -0,0 +1,635 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.api.*; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.Codec; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.RedisCommand; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.client.protocol.convertor.StreamIdConvertor; +import org.redisson.client.protocol.decoder.*; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.Limit; +import org.springframework.data.redis.connection.RedisStreamCommands; +import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.connection.stream.StreamInfo; +import org.springframework.util.Assert; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonStreamCommands implements RedisStreamCommands { + + private final RedissonConnection connection; + + public RedissonStreamCommands(RedissonConnection connection) { + this.connection = connection; + } + + private static List toStringList(RecordId... recordIds) { + if (recordIds.length == 1) { + return Arrays.asList(recordIds[0].getValue()); + } + + return Arrays.stream(recordIds).map(RecordId::getValue).collect(Collectors.toList()); + } + + @Override + public RecordId xAdd(MapRecord record) { + return xAdd(record, XAddOptions.none()); + } + + private static final RedisStrictCommand XCLAIM_JUSTID = new RedisStrictCommand("XCLAIM", obj -> RecordId.of(obj.toString())); + + @Override + public List xClaimJustId(byte[] key, String group, String newOwner, XClaimOptions options) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(group, "Group name must not be null!"); + Assert.notNull(newOwner, "NewOwner must not be null!"); + Assert.notEmpty(options.getIds(), "Ids collection must not be empty!"); + + List params = new ArrayList<>(); + params.add(key); + params.add(group); + params.add(newOwner); + params.add(Objects.requireNonNull(options.getIdleTime()).toMillis()); + params.addAll(Arrays.asList(options.getIdsAsStringArray())); + params.add("JUSTID"); + + return connection.write(key, StringCodec.INSTANCE, XCLAIM_JUSTID, params.toArray()); + } + + @Override + public List xClaim(byte[] key, String group, String newOwner, XClaimOptions options) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(group, "Group name must not be null!"); + Assert.notNull(newOwner, "NewOwner must not be null!"); + Assert.notEmpty(options.getIds(), "Ids collection must not be empty!"); + + List params = new ArrayList<>(); + params.add(key); + params.add(group); + params.add(newOwner); + params.add(Objects.requireNonNull(options.getIdleTime()).toMillis()); + params.addAll(Arrays.asList(options.getIdsAsStringArray())); + + return connection.write(key, ByteArrayCodec.INSTANCE, new RedisCommand>("XCLAIM", + new ListMultiDecoder2( + new ByteRecordReplayDecoder(key), + new ObjectDecoder(new StreamIdDecoder()), + new MapEntriesDecoder(new StreamObjectMapReplayDecoder()))), params.toArray()); + } + + @Override + public String xGroupCreate(byte[] key, String groupName, ReadOffset readOffset, boolean mkStream) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(groupName, "GroupName must not be null!"); + Assert.notNull(readOffset, "ReadOffset must not be null!"); + + List params = new ArrayList<>(); + params.add("CREATE"); + params.add(key); + params.add(groupName); + params.add(readOffset.getOffset()); + if (mkStream) { + params.add("MKSTREAM"); + } + + return connection.write(key, StringCodec.INSTANCE, XGROUP_STRING, params.toArray()); + } + + private static class XInfoStreamReplayDecoder implements MultiDecoder { + + @Override + public StreamInfo.XInfoStream decode(List parts, State state) { + Map res = new HashMap<>(); + res.put("length", parts.get(1)); + res.put("radix-tree-keys", parts.get(3)); + res.put("radix-tree-nodes", parts.get(5)); + res.put("groups", parts.get(7)); + res.put("last-generated-id", parts.get(9).toString()); + + List firstEntry = (List) parts.get(11); + if (firstEntry != null) { + StreamMessageId firstId = StreamIdConvertor.INSTANCE.convert(firstEntry.get(0)); + Map firstData = (Map) firstEntry.get(1); + res.put("first-entry", firstData); + } + + List lastEntry = (List) parts.get(13); + if (lastEntry != null) { + StreamMessageId lastId = StreamIdConvertor.INSTANCE.convert(lastEntry.get(0)); + Map lastData = (Map) lastEntry.get(1); + res.put("last-entry", lastData); + } + + List list = res.entrySet().stream() + .flatMap(e -> Stream.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + return StreamInfo.XInfoStream.fromList(list); + } + } + + private static final RedisCommand> XINFO_STREAM = new RedisCommand<>("XINFO", "STREAM", + new ListMultiDecoder2( + new XInfoStreamReplayDecoder(), + new ObjectDecoder(StringCodec.INSTANCE.getValueDecoder()), + new ObjectMapDecoder(false))); + + @Override + public StreamInfo.XInfoStream xInfo(byte[] key) { + Assert.notNull(key, "Key must not be null!"); + + return connection.write(key, ByteArrayCodec.INSTANCE, XINFO_STREAM, key); + } + + private static class XInfoGroupsReplayDecoder implements MultiDecoder { + + @Override + public StreamInfo.XInfoGroups decode(List parts, State state) { + List result = new ArrayList<>(); + for (List part: (List>) (Object)parts) { + Map res = new HashMap<>(); + res.put("name", part.get(1)); + res.put("consumers", part.get(3)); + res.put("pending", part.get(5)); + res.put("last-delivered-id", part.get(7)); + List list = res.entrySet().stream() + .flatMap(e -> Stream.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + result.add(list); + } + + return StreamInfo.XInfoGroups.fromList(result); + } + } + + RedisCommand XINFO_GROUPS = new RedisCommand<>("XINFO", "GROUPS", + new ListMultiDecoder2(new XInfoGroupsReplayDecoder(), + new ObjectListReplayDecoder(), new ObjectListReplayDecoder()) + ); + + @Override + public StreamInfo.XInfoGroups xInfoGroups(byte[] key) { + return connection.write(key, StringCodec.INSTANCE, XINFO_GROUPS, key); + } + + private static class XInfoConsumersReplayDecoder implements MultiDecoder { + + private final String groupName; + + public XInfoConsumersReplayDecoder(String groupName) { + this.groupName = groupName; + } + + @Override + public StreamInfo.XInfoConsumers decode(List parts, State state) { + List result = new ArrayList<>(); + for (List part: (List>) (Object)parts) { + Map res = new HashMap<>(); + res.put("name", part.get(1)); + res.put("pending", part.get(3)); + res.put("idle", part.get(5)); + List list = res.entrySet().stream() + .flatMap(e -> Stream.of(e.getKey(), e.getValue())) + .collect(Collectors.toList()); + result.add(list); + } + + return StreamInfo.XInfoConsumers.fromList(groupName, result); + } + } + + @Override + public StreamInfo.XInfoConsumers xInfoConsumers(byte[] key, String groupName) { + return connection.write(key, StringCodec.INSTANCE, new RedisCommand("XINFO", "CONSUMERS", + new ListMultiDecoder2(new XInfoConsumersReplayDecoder(groupName), + new ObjectListReplayDecoder(), new ObjectListReplayDecoder())), key, groupName); + } + + private static class PendingMessagesSummaryReplayDecoder implements MultiDecoder { + + private final String groupName; + + public PendingMessagesSummaryReplayDecoder(String groupName) { + this.groupName = groupName; + } + + @Override + public PendingMessagesSummary decode(List parts, State state) { + if (parts.isEmpty()) { + return null; + } + + List> customerParts = (List>) parts.get(3); + if (customerParts.isEmpty()) { + return new PendingMessagesSummary(groupName, 0, Range.unbounded(), Collections.emptyMap()); + } + + Map map = customerParts.stream().collect(Collectors.toMap(e -> e.get(0), e -> Long.valueOf(e.get(1)), + (u, v) -> { throw new IllegalStateException("Duplicate key: " + u); }, + LinkedHashMap::new)); + Range range = Range.open(parts.get(1).toString(), parts.get(2).toString()); + return new PendingMessagesSummary(groupName, (Long) parts.get(0), range, map); + } + } + + @Override + public PendingMessagesSummary xPending(byte[] key, String groupName) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(groupName, "Group name must not be null!"); + + return connection.write(key, StringCodec.INSTANCE, new RedisCommand("XPENDING", + new ListMultiDecoder2(new PendingMessagesSummaryReplayDecoder(groupName), + new ObjectListReplayDecoder(), new ObjectListReplayDecoder())), key, groupName); + } + + private static class PendingMessageReplayDecoder implements MultiDecoder { + + private String groupName; + + public PendingMessageReplayDecoder(String groupName) { + this.groupName = groupName; + } + + @Override + public PendingMessage decode(List parts, State state) { + PendingMessage pm = new PendingMessage(RecordId.of(parts.get(0).toString()), + Consumer.from(groupName, parts.get(1).toString()), + Duration.of(Long.valueOf(parts.get(2).toString()), ChronoUnit.MILLIS), + Long.valueOf(parts.get(3).toString())); + return pm; + } + } + + private static class PendingMessagesReplayDecoder implements MultiDecoder { + + private final String groupName; + private final Range range; + + public PendingMessagesReplayDecoder(String groupName, Range range) { + this.groupName = groupName; + this.range = range; + } + + @Override + public PendingMessages decode(List parts, State state) { + List pendingMessages = (List) (Object) parts; + return new PendingMessages(groupName, range, pendingMessages); + } + } + + @Override + public PendingMessages xPending(byte[] key, String groupName, XPendingOptions options) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(groupName, "Group name must not be null!"); + + List params = new ArrayList<>(); + params.add(key); + params.add(groupName); + + params.add(((Range.Bound)options.getRange().getLowerBound()).getValue().orElse("-")); + params.add(((Range.Bound)options.getRange().getUpperBound()).getValue().orElse("+")); + + if (options.getCount() != null) { + params.add(options.getCount()); + } else { + params.add(10); + } + if (options.getConsumerName() != null) { + params.add(options.getConsumerName()); + } + + return connection.write(key, StringCodec.INSTANCE, new RedisCommand<>("XPENDING", + new ListMultiDecoder2( + new PendingMessagesReplayDecoder(groupName, options.getRange()), + new PendingMessageReplayDecoder(groupName))), + params.toArray()); + } + + @Override + public Long xAck(byte[] key, String group, RecordId... recordIds) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(group, "Group must not be null!"); + Assert.notNull(recordIds, "recordIds must not be null!"); + + List params = new ArrayList<>(); + params.add(key); + params.add(group); + params.addAll(toStringList(recordIds)); + + return connection.write(key, StringCodec.INSTANCE, RedisCommands.XACK, params.toArray()); + } + + private static final RedisStrictCommand XADD = new RedisStrictCommand("XADD", obj -> RecordId.of(obj.toString())); + + @Override + public RecordId xAdd(MapRecord record, XAddOptions options) { + Assert.notNull(record, "record must not be null!"); + + List params = new LinkedList<>(); + params.add(record.getStream()); + + if (options.getMaxlen() != null) { + params.add("MAXLEN"); + params.add(options.getMaxlen()); + } + + if (!record.getId().shouldBeAutoGenerated()) { + params.add(record.getId().getValue()); + } else { + params.add("*"); + } + + record.getValue().forEach((key, value) -> { + params.add(key); + params.add(value); + }); + + return connection.write(record.getStream(), StringCodec.INSTANCE, XADD, params.toArray()); + } + + @Override + public Long xDel(byte[] key, RecordId... recordIds) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(recordIds, "recordIds must not be null!"); + + List params = new ArrayList<>(); + params.add(key); + params.addAll(toStringList(recordIds)); + + return connection.write(key, StringCodec.INSTANCE, RedisCommands.XDEL, params.toArray()); + } + + private static final RedisStrictCommand XGROUP_STRING = new RedisStrictCommand<>("XGROUP"); + + @Override + public String xGroupCreate(byte[] key, String groupName, ReadOffset readOffset) { + return xGroupCreate(key, groupName, readOffset, false); + } + + private static final RedisStrictCommand XGROUP_BOOLEAN = new RedisStrictCommand("XGROUP", obj -> ((Long)obj) > 0); + + @Override + public Boolean xGroupDelConsumer(byte[] key, Consumer consumer) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(consumer, "Consumer must not be null!"); + Assert.notNull(consumer.getName(), "Consumer name must not be null!"); + Assert.notNull(consumer.getGroup(), "Consumer group must not be null!"); + + return connection.write(key, StringCodec.INSTANCE, XGROUP_BOOLEAN, "DELCONSUMER", key, consumer.getGroup(), consumer.getName()); + } + + @Override + public Boolean xGroupDestroy(byte[] key, String groupName) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(groupName, "GroupName must not be null!"); + + return connection.write(key, StringCodec.INSTANCE, XGROUP_BOOLEAN, "DESTROY", key, groupName); + } + + @Override + public Long xLen(byte[] key) { + Assert.notNull(key, "Key must not be null!"); + + return connection.write(key, StringCodec.INSTANCE, RedisCommands.XLEN, key); + } + + private List range(RedisCommand rangeCommand, byte[] key, Range range, Limit limit) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + Assert.notNull(limit, "Limit must not be null!"); + + List params = new LinkedList<>(); + params.add(key); + + if (rangeCommand.getName().equals(RedisCommands.XRANGE.getName())) { + params.add(range.getLowerBound().getValue().orElse("-")); + params.add(range.getUpperBound().getValue().orElse("+")); + } else { + params.add(range.getUpperBound().getValue().orElse("+")); + params.add(range.getLowerBound().getValue().orElse("-")); + } + + if (limit.getCount() > 0) { + params.add("COUNT"); + params.add(limit.getCount()); + } + + return connection.write(key, ByteArrayCodec.INSTANCE, rangeCommand, params.toArray()); + } + + private static class ByteRecordReplayDecoder implements MultiDecoder> { + + private final byte[] key; + + ByteRecordReplayDecoder(byte[] key) { + this.key = key; + } + + @Override + public List decode(List parts, State state) { + List> list = (List>) (Object) parts; + List result = new ArrayList<>(parts.size()/2); + for (List entry : list) { + ByteRecord record = StreamRecords.newRecord() + .in(key) + .withId(RecordId.of(entry.get(0).toString())) + .ofBytes((Map) entry.get(1)); + result.add(record); + } + return result; + } + } + + @Override + public List xRange(byte[] key, Range range, Limit limit) { + return range(new RedisCommand<>("XRANGE", + new ListMultiDecoder2( + new ByteRecordReplayDecoder(key), + new ObjectDecoder(new StreamIdDecoder()), + new MapEntriesDecoder(new StreamObjectMapReplayDecoder()))), + key, range, limit); + } + + private static class ByteRecordReplayDecoder2 implements MultiDecoder> { + + @Override + public List decode(List parts, State state) { + List> list = (List>) (Object) parts; + List result = new ArrayList<>(parts.size()/2); + + for (List entries : list) { + List> streamEntries = (List>) entries.get(1); + if (streamEntries.isEmpty()) { + continue; + } + + String name = (String) entries.get(0); + for (List se : streamEntries) { + ByteRecord record = StreamRecords.newRecord() + .in(name.getBytes()) + .withId(RecordId.of(se.get(0).toString())) + .ofBytes((Map) se.get(1)); + result.add(record); + } + } + return result; + } + } + + + private static final RedisCommand> XREAD = new RedisCommand<>("XREAD", + new ListMultiDecoder2( + new ByteRecordReplayDecoder2(), + new ObjectDecoder(StringCodec.INSTANCE.getValueDecoder()), + new ObjectDecoder(new StreamIdDecoder()), + new ObjectDecoder(new StreamIdDecoder()), + new MapEntriesDecoder(new StreamObjectMapReplayDecoder()))); + + private static final RedisCommand> XREAD_BLOCKING = + new RedisCommand<>("XREAD", XREAD.getReplayMultiDecoder()); + + private static final RedisCommand> XREADGROUP = + new RedisCommand<>("XREADGROUP", XREAD.getReplayMultiDecoder()); + + private static final RedisCommand> XREADGROUP_BLOCKING = + new RedisCommand<>("XREADGROUP", XREADGROUP.getReplayMultiDecoder()); + + + static { + RedisCommands.BLOCKING_COMMANDS.add(XREAD_BLOCKING); + RedisCommands.BLOCKING_COMMANDS.add(XREADGROUP_BLOCKING); + } + + @Override + public List xRead(StreamReadOptions readOptions, StreamOffset... streams) { + Assert.notNull(readOptions, "ReadOptions must not be null!"); + Assert.notNull(streams, "StreamOffsets must not be null!"); + + List params = new ArrayList<>(); + + if (readOptions.getCount() != null && readOptions.getCount() > 0) { + params.add("COUNT"); + params.add(readOptions.getCount()); + } + + if (readOptions.getBlock() != null && readOptions.getBlock() > 0) { + params.add("BLOCK"); + params.add(readOptions.getBlock()); + } + + params.add("STREAMS"); + for (StreamOffset streamOffset : streams) { + params.add(streamOffset.getKey()); + } + + for (StreamOffset streamOffset : streams) { + params.add(streamOffset.getOffset().getOffset()); + } + + if (readOptions.getBlock() != null && readOptions.getBlock() > 0) { + return connection.read(streams[0].getKey(), ByteArrayCodec.INSTANCE, XREAD_BLOCKING, params.toArray()); + } + return connection.read(streams[0].getKey(), ByteArrayCodec.INSTANCE, XREAD, params.toArray()); + } + + @Override + public List xReadGroup(Consumer consumer, StreamReadOptions readOptions, StreamOffset... streams) { + Assert.notNull(readOptions, "Consumer must not be null!"); + Assert.notNull(readOptions, "ReadOptions must not be null!"); + Assert.notNull(streams, "StreamOffsets must not be null!"); + + List params = new ArrayList<>(); + + params.add("GROUP"); + params.add(consumer.getGroup()); + params.add(consumer.getName()); + + if (readOptions.getCount() != null && readOptions.getCount() > 0) { + params.add("COUNT"); + params.add(readOptions.getCount()); + } + + if (readOptions.getBlock() != null && readOptions.getBlock() > 0) { + params.add("BLOCK"); + params.add(readOptions.getBlock()); + } + + if (readOptions.isNoack()) { + params.add("NOACK"); + } + + params.add("STREAMS"); + for (StreamOffset streamOffset : streams) { + params.add(streamOffset.getKey()); + } + + for (StreamOffset streamOffset : streams) { + params.add(streamOffset.getOffset().getOffset()); + } + + if (readOptions.getBlock() != null && readOptions.getBlock() > 0) { + return connection.write(streams[0].getKey(), ByteArrayCodec.INSTANCE, XREADGROUP_BLOCKING, params.toArray()); + } + return connection.write(streams[0].getKey(), ByteArrayCodec.INSTANCE, XREADGROUP, params.toArray()); + } + + @Override + public List xRevRange(byte[] key, Range range, Limit limit) { + return range(new RedisCommand<>("XREVRANGE", + new ListMultiDecoder2( + new ByteRecordReplayDecoder(key), + new ObjectDecoder(new StreamIdDecoder()), + new MapEntriesDecoder(new StreamObjectMapReplayDecoder()))), + key, range, limit); + } + + @Override + public Long xTrim(byte[] key, long count) { + return xTrim(key, count, false); + } + + @Override + public Long xTrim(byte[] key, long count, boolean approximateTrimming) { + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(count, "Count must not be null!"); + + List params = new ArrayList<>(4); + params.add(key); + params.add("MAXLEN"); + if (approximateTrimming) { + params.add("~"); + } + params.add(count); + + return connection.write(key, StringCodec.INSTANCE, RedisCommands.XTRIM, params.toArray()); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonSubscription.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonSubscription.java new file mode 100644 index 000000000..3003b0814 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/RedissonSubscription.java @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import org.redisson.client.BaseRedisPubSubListener; +import org.redisson.client.ChannelName; +import org.redisson.client.codec.ByteArrayCodec; +import org.redisson.client.codec.Codec; +import org.redisson.client.protocol.pubsub.PubSubType; +import org.redisson.command.CommandAsyncExecutor; +import org.redisson.pubsub.PubSubConnectionEntry; +import org.redisson.pubsub.PublishSubscribeService; +import org.springframework.data.redis.connection.DefaultMessage; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.connection.SubscriptionListener; +import org.springframework.data.redis.connection.util.AbstractSubscription; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * + * @author Nikita Koksharov + * + */ +public class RedissonSubscription extends AbstractSubscription { + + private final CommandAsyncExecutor commandExecutor; + private final PublishSubscribeService subscribeService; + + public RedissonSubscription(CommandAsyncExecutor commandExecutor, PublishSubscribeService subscribeService, MessageListener listener) { + super(listener, null, null); + this.commandExecutor = commandExecutor; + this.subscribeService = subscribeService; + } + + @Override + protected void doSubscribe(byte[]... channels) { + List> list = new ArrayList<>(); + Queue subscribed = new ConcurrentLinkedQueue<>(); + for (byte[] channel : channels) { + if (subscribeService.getPubSubEntry(new ChannelName(channel)) != null) { + continue; + } + + CompletableFuture f = subscribeService.subscribe(ByteArrayCodec.INSTANCE, new ChannelName(channel), new BaseRedisPubSubListener() { + @Override + public void onMessage(CharSequence ch, Object message) { + if (!Arrays.equals(((ChannelName) ch).getName(), channel)) { + return; + } + + byte[] m = toBytes(message); + DefaultMessage msg = new DefaultMessage(((ChannelName) ch).getName(), m); + getListener().onMessage(msg, null); + } + + @Override + public boolean onStatus(PubSubType type, CharSequence ch) { + if (!Arrays.equals(((ChannelName) ch).getName(), channel)) { + return false; + } + + if (getListener() instanceof SubscriptionListener) { + subscribed.add(channel); + } + return super.onStatus(type, ch); + } + + }); + list.add(f); + } + for (CompletableFuture future : list) { + commandExecutor.get(future); + } + for (byte[] channel : subscribed) { + ((SubscriptionListener) getListener()).onChannelSubscribed(channel, 1); + } + } + + @Override + protected void doUnsubscribe(boolean all, byte[]... channels) { + for (byte[] channel : channels) { + CompletableFuture f = subscribeService.unsubscribe(new ChannelName(channel), PubSubType.UNSUBSCRIBE); + if (getListener() instanceof SubscriptionListener) { + f.whenComplete((r, e) -> { + if (r != null) { + ((SubscriptionListener) getListener()).onChannelUnsubscribed(channel, 1); + } + }); + } + } + } + + @Override + protected void doPsubscribe(byte[]... patterns) { + List> list = new ArrayList<>(); + Queue subscribed = new ConcurrentLinkedQueue<>(); + for (byte[] channel : patterns) { + if (subscribeService.getPubSubEntry(new ChannelName(channel)) != null) { + continue; + } + + CompletableFuture> f = subscribeService.psubscribe(new ChannelName(channel), ByteArrayCodec.INSTANCE, new BaseRedisPubSubListener() { + @Override + public void onPatternMessage(CharSequence pattern, CharSequence ch, Object message) { + if (!Arrays.equals(((ChannelName) pattern).getName(), channel)) { + return; + } + + byte[] m = toBytes(message); + DefaultMessage msg = new DefaultMessage(((ChannelName)ch).getName(), m); + getListener().onMessage(msg, ((ChannelName)pattern).getName()); + } + + @Override + public boolean onStatus(PubSubType type, CharSequence pattern) { + if (!Arrays.equals(((ChannelName) pattern).getName(), channel)) { + return false; + } + + if (getListener() instanceof SubscriptionListener) { + subscribed.add(channel); + } + return super.onStatus(type, pattern); + } + }); + list.add(f); + } + for (CompletableFuture future : list) { + commandExecutor.get(future); + } + for (byte[] channel : subscribed) { + ((SubscriptionListener) getListener()).onPatternSubscribed(channel, 1); + } + } + + private byte[] toBytes(Object message) { + if (message instanceof String) { + return ((String) message).getBytes(); + } + return (byte[]) message; + } + + @Override + protected void doPUnsubscribe(boolean all, byte[]... patterns) { + for (byte[] pattern : patterns) { + CompletableFuture f = subscribeService.unsubscribe(new ChannelName(pattern), PubSubType.PUNSUBSCRIBE); + if (getListener() instanceof SubscriptionListener) { + f.whenComplete((r, e) -> { + if (r != null) { + ((SubscriptionListener) getListener()).onPatternUnsubscribed(pattern, 1); + } + }); + } + } + } + + @Override + protected void doClose() { + doUnsubscribe(false, getChannels().toArray(new byte[getChannels().size()][])); + doPUnsubscribe(false, getPatterns().toArray(new byte[getPatterns().size()][])); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ScoredSortedListReplayDecoder.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ScoredSortedListReplayDecoder.java new file mode 100644 index 000000000..18cfc83e6 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ScoredSortedListReplayDecoder.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.util.ArrayList; +import java.util.List; + +import org.redisson.client.codec.Codec; +import org.redisson.client.codec.DoubleCodec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; +import org.springframework.data.redis.connection.zset.DefaultTuple; +import org.springframework.data.redis.connection.zset.Tuple; + +/** + * + * @author Nikita Koksharov + * + */ +public class ScoredSortedListReplayDecoder implements MultiDecoder> { + + @Override + public Decoder getDecoder(Codec codec, int paramNum, State state) { + if (paramNum % 2 != 0) { + return DoubleCodec.INSTANCE.getValueDecoder(); + } + return MultiDecoder.super.getDecoder(codec, paramNum, state); + } + + @Override + public List decode(List parts, State state) { + List result = new ArrayList(); + for (int i = 0; i < parts.size(); i += 2) { + result.add(new DefaultTuple((byte[])parts.get(i), ((Number)parts.get(i+1)).doubleValue())); + } + return result; + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ScoredSortedSetReplayDecoder.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ScoredSortedSetReplayDecoder.java new file mode 100644 index 000000000..0c1fc3c51 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/ScoredSortedSetReplayDecoder.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.redisson.client.codec.Codec; +import org.redisson.client.codec.DoubleCodec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; +import org.springframework.data.redis.connection.zset.DefaultTuple; +import org.springframework.data.redis.connection.zset.Tuple; + +/** + * + * @author Nikita Koksharov + * + */ +public class ScoredSortedSetReplayDecoder implements MultiDecoder> { + + @Override + public Decoder getDecoder(Codec codec, int paramNum, State state) { + if (paramNum % 2 != 0) { + return DoubleCodec.INSTANCE.getValueDecoder(); + } + return MultiDecoder.super.getDecoder(codec, paramNum, state); + } + + @Override + public Set decode(List parts, State state) { + Set result = new LinkedHashSet(); + for (int i = 0; i < parts.size(); i += 2) { + result.add(new DefaultTuple((byte[])parts.get(i), ((Number)parts.get(i+1)).doubleValue())); + } + return result; + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SecondsConvertor.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SecondsConvertor.java new file mode 100644 index 000000000..888b4780f --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SecondsConvertor.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.util.concurrent.TimeUnit; + +import org.redisson.client.protocol.convertor.Convertor; + +/** + * + * @author Nikita Koksharov + * + */ +public class SecondsConvertor implements Convertor { + + private final TimeUnit unit; + private final TimeUnit source; + + public SecondsConvertor(TimeUnit unit, TimeUnit source) { + super(); + this.unit = unit; + this.source = source; + } + + @Override + public Long convert(Object obj) { + return unit.convert((Long)obj, source); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SetReplayDecoder.java b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SetReplayDecoder.java new file mode 100644 index 000000000..c382e264a --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/main/java/org/redisson/spring/data/connection/SetReplayDecoder.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2013-2022 Nikita Koksharov + * + * 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.redisson.spring.data.connection; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.redisson.client.codec.Codec; +import org.redisson.client.handler.State; +import org.redisson.client.protocol.Decoder; +import org.redisson.client.protocol.decoder.MultiDecoder; + +/** + * + * @author Nikita Koksharov + * + */ +public class SetReplayDecoder implements MultiDecoder> { + + private final Decoder decoder; + + public SetReplayDecoder(Decoder decoder) { + super(); + this.decoder = decoder; + } + + @Override + public Decoder getDecoder(Codec codec, int paramNum, State state) { + return decoder; + } + + @Override + public Set decode(List parts, State state) { + return new LinkedHashSet(parts); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/BaseTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/BaseTest.java new file mode 100644 index 000000000..4e73334f1 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/BaseTest.java @@ -0,0 +1,86 @@ +package org.redisson; + +import java.io.IOException; + +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; + +public abstract class BaseTest { + + protected RedissonClient redisson; + protected static RedissonClient defaultRedisson; + + @BeforeClass + public static void beforeClass() throws IOException, InterruptedException { + if (!RedissonRuntimeEnvironment.isTravis) { + RedisRunner.startDefaultRedisServerInstance(); + defaultRedisson = createInstance(); + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + defaultRedisson.shutdown(); + try { + RedisRunner.shutDownDefaultRedisServerInstance(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + }); + } + } + + @Before + public void before() throws IOException, InterruptedException { + if (RedissonRuntimeEnvironment.isTravis) { + RedisRunner.startDefaultRedisServerInstance(); + redisson = createInstance(); + } else { + if (redisson == null) { + redisson = defaultRedisson; + } + if (flushBetweenTests()) { + redisson.getKeys().flushall(); + } + } + } + + @After + public void after() throws InterruptedException { + if (RedissonRuntimeEnvironment.isTravis) { + redisson.shutdown(); + RedisRunner.shutDownDefaultRedisServerInstance(); + } + } + + public static Config createConfig() { +// String redisAddress = System.getProperty("redisAddress"); +// if (redisAddress == null) { +// redisAddress = "127.0.0.1:6379"; +// } + Config config = new Config(); +// config.setCodec(new MsgPackJacksonCodec()); +// config.useSentinelServers().setMasterName("mymaster").addSentinelAddress("127.0.0.1:26379", "127.0.0.1:26389"); +// config.useClusterServers().addNodeAddress("127.0.0.1:7004", "127.0.0.1:7001", "127.0.0.1:7000"); + config.useSingleServer() + .setAddress(RedisRunner.getDefaultRedisServerBindAddressAndPort()); +// .setPassword("mypass1"); +// config.useMasterSlaveConnection() +// .setMasterAddress("127.0.0.1:6379") +// .addSlaveAddress("127.0.0.1:6399") +// .addSlaveAddress("127.0.0.1:6389"); + return config; + } + + public static RedissonClient createInstance() { + Config config = createConfig(); + return Redisson.create(config); + } + + protected boolean flushBetweenTests() { + return true; + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/ClusterRunner.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/ClusterRunner.java new file mode 100644 index 000000000..69af59386 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/ClusterRunner.java @@ -0,0 +1,156 @@ +package org.redisson; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.redisson.misc.BiHashMap; + +/** + * + * @author Rui Gu (https://github.com/jackygurui) + */ +public class ClusterRunner { + + private final LinkedHashMap nodes = new LinkedHashMap<>(); + private final LinkedHashMap slaveMasters = new LinkedHashMap<>(); + + public ClusterRunner addNode(RedisRunner runner) { + nodes.putIfAbsent(runner, getRandomId()); + if (!runner.hasOption(RedisRunner.REDIS_OPTIONS.CLUSTER_ENABLED)) { + runner.clusterEnabled(true); + } + if (!runner.hasOption(RedisRunner.REDIS_OPTIONS.CLUSTER_NODE_TIMEOUT)) { + runner.clusterNodeTimeout(5000); + } + if (!runner.hasOption(RedisRunner.REDIS_OPTIONS.PORT)) { + runner.randomPort(1); + runner.port(RedisRunner.findFreePort()); + } + if (!runner.hasOption(RedisRunner.REDIS_OPTIONS.BIND)) { + runner.bind("127.0.0.1"); + } + return this; + } + + public ClusterRunner addNode(RedisRunner master, RedisRunner... slaves) { + addNode(master); + for (RedisRunner slave : slaves) { + addNode(slave); + slaveMasters.put(nodes.get(slave), nodes.get(master)); + } + return this; + } + + public synchronized ClusterProcesses run() throws IOException, InterruptedException, RedisRunner.FailedToStartRedisException { + BiHashMap processes = new BiHashMap<>(); + for (RedisRunner runner : nodes.keySet()) { + List options = getClusterConfig(runner); + String confFile = runner.dir() + File.separator + nodes.get(runner) + ".conf"; + System.out.println("WRITING CONFIG: for " + nodes.get(runner)); + try (PrintWriter printer = new PrintWriter(new FileWriter(confFile))) { + options.stream().forEach((line) -> { + printer.println(line); + System.out.println(line); + }); + } + processes.put(nodes.get(runner), runner.clusterConfigFile(confFile).run()); + } + Thread.sleep(1000); + for (RedisRunner.RedisProcess process : processes.valueSet()) { + if (!process.isAlive()) { + throw new RedisRunner.FailedToStartRedisException(); + } + } + return new ClusterProcesses(processes); + } + + private List getClusterConfig(RedisRunner runner) { + String me = runner.getInitialBindAddr() + ":" + runner.getPort(); + List nodeConfig = new ArrayList<>(); + int c = 0; + for (RedisRunner node : nodes.keySet()) { + String nodeId = nodes.get(node); + StringBuilder sb = new StringBuilder(); + String nodeAddr = node.getInitialBindAddr() + ":" + node.getPort(); + sb.append(nodeId).append(" "); + sb.append(nodeAddr).append(" "); + sb.append(me.equals(nodeAddr) + ? "myself," + : ""); + boolean isMaster = !slaveMasters.containsKey(nodeId); + if (isMaster) { + sb.append("master -"); + } else { + sb.append("slave ").append(slaveMasters.get(nodeId)); + } + sb.append(" "); + sb.append("0").append(" "); + sb.append(me.equals(nodeAddr) + ? "0" + : "1").append(" "); + sb.append(c + 1).append(" "); + sb.append("connected "); + if (isMaster) { + sb.append(getSlots(c, nodes.size() - slaveMasters.size())); + c++; + } + nodeConfig.add(sb.toString()); + } + nodeConfig.add("vars currentEpoch 0 lastVoteEpoch 0"); + return nodeConfig; + } + + private static String getSlots(int index, int groupNum) { + final double t = 16383; + int start = index == 0 ? 0 : (int) (t / groupNum * index); + int end = index == groupNum - 1 ? (int) t : (int) (t / groupNum * (index + 1)) - 1; + return start + "-" + end; + } + + private static String getRandomId() { + final SecureRandom r = new SecureRandom(); + return new BigInteger(160, r).toString(16); + } + + public static class ClusterProcesses { + private final BiHashMap processes; + + private ClusterProcesses(BiHashMap processes) { + this.processes = processes; + } + + public RedisRunner.RedisProcess getProcess(String nodeId) { + return processes.get(nodeId); + } + + public String getNodeId(RedisRunner.RedisProcess process) { + return processes.reverseGet(process); + } + + public Set getNodes() { + return processes.valueSet(); + } + + public Set getNodeIds() { + return processes.keySet(); + } + + public synchronized Map shutdown() { + return processes + .entrySet() + .stream() + .collect(Collectors.toMap( + e -> e.getKey(), + e -> e.getValue().stop())); + } + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedisRunner.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedisRunner.java new file mode 100644 index 000000000..5f6a07eda --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedisRunner.java @@ -0,0 +1,1070 @@ +package org.redisson; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.Inet4Address; +import java.net.ServerSocket; +import java.net.URL; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; +import org.redisson.client.RedisClient; +import org.redisson.client.RedisClientConfig; +import org.redisson.client.RedisConnection; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.client.protocol.RedisStrictCommand; +import org.redisson.client.protocol.convertor.VoidReplayConvertor; + +/** + * + * @author Rui Gu (https://github.com/jackygurui) + */ +public class RedisRunner { + + public enum REDIS_OPTIONS { + + BINARY_PATH, + DAEMONIZE, + PIDFILE, + PORT, + TCP_BACKLOG, + BIND(true), + UNIXSOCKET, + UNIXSOCKETPERM, + TIMEOUT, + TCP_KEEPALIVE, + LOGLEVEL, + LOGFILE, + SYSLOG_ENABLED, + SYSLOG_IDENT, + SYSLOG_FACILITY, + DATABASES, + SAVE(true), + STOP_WRITES_ON_BGSAVE_ERROR, + RDBCOMPRESSION, + RDBCHECKSUM, + DBFILENAME, + DIR, + SLAVEOF, + MASTERAUTH, + SLAVE_SERVE_STALE_DATA, + SLAVE_READ_ONLY, + REPL_DISKLESS_SYNC, + REPL_DISKLESS_SYNC_DELAY, + REPL_PING_SLAVE_PERIOD, + REPL_TIMEOUT, + REPL_DISABLE_TCP_NODELAY, + REPL_BACKLOG_SIZE, + REPL_BACKLOG_TTL, + SLAVE_PRIORITY, + MIN_SLAVES_TO_WRITE, + MIN_SLAVES_MAX_LAG, + REQUIREPASS, + RENAME_COMMAND(true), + MAXCLIENTS, + MAXMEMORY, + MAXMEMORY_POLICY, + MAXMEMORY_SAMPLE, + APPENDONLY, + APPENDFILENAME, + APPENDFSYNC, + NO_APPENDFSYNC_ON_REWRITE, + AUTO_AOF_REWRITE_PERCENTAGE, + AUTO_AOF_REWRITE_MIN_SIZE, + AOF_LOAD_TRUNCATED, + LUA_TIME_LIMIT, + CLUSTER_ENABLED, + CLUSTER_CONFIG_FILE, + CLUSTER_NODE_TIMEOUT, + CLUSTER_SLAVE_VALIDITY_FACTOR, + CLUSTER_MIGRATION_BARRIER, + CLUSTER_REQUIRE_FULL_COVERAGE, + SLOWLOG_LOG_SLOWER_THAN, + SLOWLOG_MAX_LEN, + LATENCY_MONITOR_THRESHOLD, + NOTIFY_KEYSPACE_EVENTS, + HASH_MAX_ZIPLIST_ENTRIES, + HASH_MAX_ZIPLIST_VALUE, + LIST_MAX_ZIPLIST_ENTRIES, + LIST_MAX_ZIPLIST_VALUE, + SET_MAX_INTSET_ENTRIES, + ZSET_MAX_ZIPLIST_ENTRIES, + ZSET_MAX_ZIPLIST_VALUE, + HLL_SPARSE_MAX_BYTES, + ACTIVEREHASHING, + CLIENT_OUTPUT_BUFFER_LIMIT$NORMAL, + CLIENT_OUTPUT_BUFFER_LIMIT$SLAVE, + CLIENT_OUTPUT_BUFFER_LIMIT$PUBSUB, + HZ, + AOF_REWRITE_INCREMENTAL_FSYNC, + PROTECTED_MODE, + SENTINEL, + SENTINEL$ANNOUNCE_IP, + SENTINEL$ANNOUNCE_PORT, + SENTINEL$MONITOR(true), + SENTINEL$AUTH_PASS(true), + SENTINEL$DOWN_AFTER_MILLISECONDS(true), + SENTINEL$PARALLEL_SYNCS(true), + SENTINEL$FAILOVER_TIMEOUT(true), + SENTINEL$NOTIFICATION_SCRIPT(true), + SENTINEL$CLIENT_RECONFIG_SCRIPT(true) + ; + + private final boolean allowMutiple; + + private REDIS_OPTIONS() { + this.allowMutiple = false; + } + + private REDIS_OPTIONS(boolean allowMutiple) { + this.allowMutiple = allowMutiple; + } + + public boolean isAllowMultiple() { + return allowMutiple; + } + } + + public enum LOGLEVEL_OPTIONS { + + DEBUG, + VERBOSE, + NOTICE, + WARNING + } + + public enum SYSLOG_FACILITY_OPTIONS { + + USER, + LOCAL0, + LOCAL1, + LOCAL2, + LOCAL3, + LOCAL4, + LOCAL5, + LOCAL6, + LOCAL7 + } + + public enum MAX_MEMORY_POLICY_OPTIONS { + + VOLATILE_LRU, + ALLKEYS_LRU, + VOLATILE_RANDOM, + ALLKEYS_RANDOM, + VOLATILE_TTL, + NOEVICTION + } + + public enum APPEND_FSYNC_MODE_OPTIONS { + + ALWAYS, + EVERYSEC, + NO + } + + public enum KEYSPACE_EVENTS_OPTIONS { + + K, + E, + g, + $, + l, + s, + h, + z, + x, + e, + A + } + + private final LinkedHashMap options = new LinkedHashMap<>(); + protected static RedisRunner.RedisProcess defaultRedisInstance; + private static int defaultRedisInstanceExitCode; + + private String path = ""; + private String defaultDir = Paths.get("").toString(); + private boolean nosave = false; + private boolean randomDir = false; + private ArrayList bindAddr = new ArrayList<>(); + private int port = 6379; + private int retryCount = Integer.MAX_VALUE; + private boolean randomPort = false; + private String sentinelFile; + private String clusterFile; + + { + this.options.put(REDIS_OPTIONS.BINARY_PATH, RedissonRuntimeEnvironment.redisBinaryPath); + } + + /** + * To change the redisBinary system property for running the test, + * use argLine option from surefire plugin: + * + * $ mvn -DargLine="-DredisBinary=`which redis-server`" -Punit-test clean \ + * verify + * + * @param configPath + * @return Process running redis instance + * @throws IOException + * @throws InterruptedException + * @see + * + * http://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#argLine + */ + public static RedisProcess runRedisWithConfigFile(String configPath) throws IOException, InterruptedException { + URL resource = RedisRunner.class.getResource(configPath); + return runWithOptions(new RedisRunner(), RedissonRuntimeEnvironment.redisBinaryPath, resource.getFile()); + } + + private static RedisProcess runWithOptions(RedisRunner runner, String... options) throws IOException, InterruptedException { + List launchOptions = Arrays.stream(options) + .map(x -> Arrays.asList(x.split(" "))).flatMap(x -> x.stream()) + .collect(Collectors.toList()); + System.out.println("REDIS LAUNCH OPTIONS: " + Arrays.toString(launchOptions.toArray())); + ProcessBuilder master = new ProcessBuilder(launchOptions) + .redirectErrorStream(true) + .directory(new File(RedissonRuntimeEnvironment.tempDir)); + Process p = master.start(); + new Thread(() -> { + BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); + String line; + try { + while (p.isAlive() && (line = reader.readLine()) != null && !RedissonRuntimeEnvironment.isTravis) { + System.out.println("REDIS PROCESS: " + line); + } + } catch (IOException ex) { + System.out.println("Exception: " + ex.getLocalizedMessage()); + } + }).start(); + Thread.sleep(1500); + return new RedisProcess(p, runner); + } + + public RedisProcess run() throws IOException, InterruptedException, FailedToStartRedisException { + if (!options.containsKey(REDIS_OPTIONS.DIR)) { + addConfigOption(REDIS_OPTIONS.DIR, defaultDir); + } + if (randomPort) { + for (int i = 0; i < retryCount; i++) { + this.port = findFreePort(); + addConfigOption(REDIS_OPTIONS.PORT, this.port); + try { + return runAndCheck(); + } catch (FailedToStartRedisException e) { + } + } + throw new FailedToStartRedisException(); + } else { + return runAndCheck(); + } + } + + public RedisProcess runAndCheck() throws IOException, InterruptedException, FailedToStartRedisException { + List args = new ArrayList(options.values()); + if (sentinelFile != null && sentinelFile.length() > 0) { + String confFile = defaultDir + File.separator + sentinelFile; + try (PrintWriter printer = new PrintWriter(new FileWriter(confFile))) { + args.stream().forEach((arg) -> { + if (arg.contains("--")) { + printer.println(arg.replace("--", "")); + } + }); + } + args = args.subList(0, 1); + args.add(confFile); + args.add("--sentinel"); + } + RedisProcess rp = runWithOptions(this, args.toArray(new String[0])); + if (!isCluster() + && rp.redisProcess.waitFor(1000, TimeUnit.MILLISECONDS)) { + throw new FailedToStartRedisException(); + } + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + rp.stop(); + })); + return rp; + } + + public boolean hasOption(REDIS_OPTIONS option) { + return options.containsKey(option); + } + + private void addConfigOption(REDIS_OPTIONS option, Object... args) { + StringBuilder sb = new StringBuilder("--") + .append(option.toString() + .replaceAll("_", "-") + .replaceAll("\\$", " ") + .toLowerCase()) + .append(" ") + .append(Arrays.stream(args).map(Object::toString) + .collect(Collectors.joining(" "))); + this.options.put(option, + option.isAllowMultiple() + ? sb.insert(0, this.options.getOrDefault(option, "")).toString() + : sb.toString()); + } + + private String convertBoolean(boolean b) { + return b ? "yes" : "no"; + } + + public RedisRunner daemonize(boolean daemonize) { + addConfigOption(REDIS_OPTIONS.DAEMONIZE, convertBoolean(daemonize)); + return this; + } + + public RedisRunner pidfile(String pidfile) { + addConfigOption(REDIS_OPTIONS.PIDFILE, pidfile); + return this; + } + + public RedisRunner port(int port) { + this.port = port; + this.randomPort = false; + addConfigOption(REDIS_OPTIONS.PORT, port); + return this; + } + + public RedisRunner randomPort() { + return randomPort(Integer.MAX_VALUE); + } + + public RedisRunner randomPort(int retryCount) { + this.randomPort = true; + this.retryCount = retryCount; + options.remove(REDIS_OPTIONS.PORT); + return this; + } + + public int getPort() { + return this.port; + } + + public RedisRunner tcpBacklog(long tcpBacklog) { + addConfigOption(REDIS_OPTIONS.TCP_BACKLOG, tcpBacklog); + return this; + } + + public RedisRunner bind(String bind) { + this.bindAddr.add(bind); + addConfigOption(REDIS_OPTIONS.BIND, bind); + return this; + } + + public ArrayList getBindAddr() { + return this.bindAddr; + } + + public RedisRunner unixsocket(String unixsocket) { + addConfigOption(REDIS_OPTIONS.UNIXSOCKET, unixsocket); + return this; + } + + public RedisRunner unixsocketperm(int unixsocketperm) { + addConfigOption(REDIS_OPTIONS.UNIXSOCKETPERM, unixsocketperm); + return this; + } + + public RedisRunner timeout(long timeout) { + addConfigOption(REDIS_OPTIONS.TIMEOUT, timeout); + return this; + } + + public RedisRunner tcpKeepalive(long tcpKeepalive) { + addConfigOption(REDIS_OPTIONS.TCP_KEEPALIVE, tcpKeepalive); + return this; + } + + public RedisRunner loglevel(LOGLEVEL_OPTIONS loglevel) { + addConfigOption(REDIS_OPTIONS.LOGLEVEL, loglevel.toString()); + return this; + } + + public RedisRunner logfile(String logfile) { + addConfigOption(REDIS_OPTIONS.LOGLEVEL, logfile); + return this; + } + + public RedisRunner syslogEnabled(boolean syslogEnabled) { + addConfigOption(REDIS_OPTIONS.SYSLOG_ENABLED, convertBoolean(syslogEnabled)); + return this; + } + + public RedisRunner syslogIdent(String syslogIdent) { + addConfigOption(REDIS_OPTIONS.SYSLOG_IDENT, syslogIdent); + return this; + } + + public RedisRunner syslogFacility(SYSLOG_FACILITY_OPTIONS syslogFacility) { + addConfigOption(REDIS_OPTIONS.SYSLOG_IDENT, syslogFacility.toString()); + return this; + } + + public RedisRunner databases(int databases) { + addConfigOption(REDIS_OPTIONS.DATABASES, databases); + return this; + } + + public RedisRunner save(long seconds, long changes) { + if (!nosave) { + addConfigOption(REDIS_OPTIONS.SAVE, seconds, changes); + } + return this; + } + + /** + * Phantom option + * + * @return RedisRunner + */ + public RedisRunner nosave() { + this.nosave = true; + options.remove(REDIS_OPTIONS.SAVE); +// addConfigOption(REDIS_OPTIONS.SAVE, "''"); + return this; + } + + public RedisRunner stopWritesOnBgsaveError(boolean stopWritesOnBgsaveError) { + addConfigOption(REDIS_OPTIONS.STOP_WRITES_ON_BGSAVE_ERROR, convertBoolean(stopWritesOnBgsaveError)); + return this; + } + + public RedisRunner rdbcompression(boolean rdbcompression) { + addConfigOption(REDIS_OPTIONS.RDBCOMPRESSION, convertBoolean(rdbcompression)); + return this; + } + + public RedisRunner rdbchecksum(boolean rdbchecksum) { + addConfigOption(REDIS_OPTIONS.RDBCHECKSUM, convertBoolean(rdbchecksum)); + return this; + } + + public RedisRunner dbfilename(String dbfilename) { + addConfigOption(REDIS_OPTIONS.DBFILENAME, dbfilename); + return this; + } + + public RedisRunner dir(String dir) { + if (!randomDir) { + addConfigOption(REDIS_OPTIONS.DIR, dir); + this.path = dir; + } + return this; + } + + /** + * Phantom option + * + * @return RedisRunner + */ + public RedisRunner randomDir() { + this.randomDir = true; + options.remove(REDIS_OPTIONS.DIR); + makeRandomDefaultDir(); + + + addConfigOption(REDIS_OPTIONS.DIR, "\"" + defaultDir + "\""); + return this; + } + + public RedisRunner slaveof(Inet4Address masterip, int port) { + addConfigOption(REDIS_OPTIONS.SLAVEOF, masterip.getHostAddress(), port); + return this; + } + + public RedisRunner slaveof(String masterip, int port) { + addConfigOption(REDIS_OPTIONS.SLAVEOF, masterip, port); + return this; + } + + public RedisRunner masterauth(String masterauth) { + addConfigOption(REDIS_OPTIONS.MASTERAUTH, masterauth); + return this; + } + + public RedisRunner slaveServeStaleData(boolean slaveServeStaleData) { + addConfigOption(REDIS_OPTIONS.SLAVE_SERVE_STALE_DATA, convertBoolean(slaveServeStaleData)); + return this; + } + + public RedisRunner slaveReadOnly(boolean slaveReadOnly) { + addConfigOption(REDIS_OPTIONS.SLAVE_READ_ONLY, convertBoolean(slaveReadOnly)); + return this; + } + + public RedisRunner replDisklessSync(boolean replDisklessSync) { + addConfigOption(REDIS_OPTIONS.REPL_DISKLESS_SYNC, convertBoolean(replDisklessSync)); + return this; + } + + public RedisRunner replDisklessSyncDelay(long replDisklessSyncDelay) { + addConfigOption(REDIS_OPTIONS.REPL_DISKLESS_SYNC_DELAY, replDisklessSyncDelay); + return this; + } + + public RedisRunner replPingSlavePeriod(long replPingSlavePeriod) { + addConfigOption(REDIS_OPTIONS.REPL_PING_SLAVE_PERIOD, replPingSlavePeriod); + return this; + } + + public RedisRunner replTimeout(long replTimeout) { + addConfigOption(REDIS_OPTIONS.REPL_TIMEOUT, replTimeout); + return this; + } + + public RedisRunner replDisableTcpNodelay(boolean replDisableTcpNodelay) { + addConfigOption(REDIS_OPTIONS.REPL_DISABLE_TCP_NODELAY, convertBoolean(replDisableTcpNodelay)); + return this; + } + + public RedisRunner replBacklogSize(String replBacklogSize) { + addConfigOption(REDIS_OPTIONS.REPL_BACKLOG_SIZE, replBacklogSize); + return this; + } + + public RedisRunner replBacklogTtl(long replBacklogTtl) { + addConfigOption(REDIS_OPTIONS.REPL_BACKLOG_TTL, replBacklogTtl); + return this; + } + + public RedisRunner slavePriority(long slavePriority) { + addConfigOption(REDIS_OPTIONS.SLAVE_PRIORITY, slavePriority); + return this; + } + + public RedisRunner minSlaveToWrite(long minSlaveToWrite) { + addConfigOption(REDIS_OPTIONS.MIN_SLAVES_TO_WRITE, minSlaveToWrite); + return this; + } + + public RedisRunner minSlaveMaxLag(long minSlaveMaxLag) { + addConfigOption(REDIS_OPTIONS.MIN_SLAVES_MAX_LAG, minSlaveMaxLag); + return this; + } + + public RedisRunner requirepass(String requirepass) { + addConfigOption(REDIS_OPTIONS.REQUIREPASS, requirepass); + return this; + } + + public RedisRunner renameCommand(String renameCommand) { + addConfigOption(REDIS_OPTIONS.RENAME_COMMAND, renameCommand); + return this; + } + + public RedisRunner maxclients(long maxclients) { + addConfigOption(REDIS_OPTIONS.MAXCLIENTS, maxclients); + return this; + } + + public RedisRunner maxmemory(String maxmemory) { + addConfigOption(REDIS_OPTIONS.MAXMEMORY, maxmemory); + return this; + } + + public RedisRunner maxmemoryPolicy(MAX_MEMORY_POLICY_OPTIONS maxmemoryPolicy) { + addConfigOption(REDIS_OPTIONS.MAXMEMORY, maxmemoryPolicy.toString()); + return this; + } + + public RedisRunner maxmemorySamples(long maxmemorySamples) { + addConfigOption(REDIS_OPTIONS.MAXMEMORY, maxmemorySamples); + return this; + } + + public RedisRunner appendonly(boolean appendonly) { + addConfigOption(REDIS_OPTIONS.APPENDONLY, convertBoolean(appendonly)); + return this; + } + + public RedisRunner appendfilename(String appendfilename) { + addConfigOption(REDIS_OPTIONS.APPENDFILENAME, appendfilename); + return this; + } + + public RedisRunner appendfsync(APPEND_FSYNC_MODE_OPTIONS appendfsync) { + addConfigOption(REDIS_OPTIONS.APPENDFSYNC, appendfsync.toString()); + return this; + } + + public RedisRunner noAppendfsyncOnRewrite(boolean noAppendfsyncOnRewrite) { + addConfigOption(REDIS_OPTIONS.NO_APPENDFSYNC_ON_REWRITE, convertBoolean(noAppendfsyncOnRewrite)); + return this; + } + + public RedisRunner autoAofRewritePercentage(int autoAofRewritePercentage) { + addConfigOption(REDIS_OPTIONS.AUTO_AOF_REWRITE_PERCENTAGE, autoAofRewritePercentage); + return this; + } + + public RedisRunner autoAofRewriteMinSize(String autoAofRewriteMinSize) { + addConfigOption(REDIS_OPTIONS.AUTO_AOF_REWRITE_MIN_SIZE, autoAofRewriteMinSize); + return this; + } + + public RedisRunner aofLoadTruncated(boolean aofLoadTruncated) { + addConfigOption(REDIS_OPTIONS.AOF_LOAD_TRUNCATED, convertBoolean(aofLoadTruncated)); + return this; + } + + public RedisRunner luaTimeLimit(long luaTimeLimit) { + addConfigOption(REDIS_OPTIONS.AOF_LOAD_TRUNCATED, luaTimeLimit); + return this; + } + + public RedisRunner clusterEnabled(boolean clusterEnabled) { + addConfigOption(REDIS_OPTIONS.CLUSTER_ENABLED, convertBoolean(clusterEnabled)); + return this; + } + + public RedisRunner clusterConfigFile(String clusterConfigFile) { + addConfigOption(REDIS_OPTIONS.CLUSTER_CONFIG_FILE, clusterConfigFile); + this.clusterFile = clusterConfigFile; + return this; + } + + public RedisRunner clusterNodeTimeout(long clusterNodeTimeout) { + addConfigOption(REDIS_OPTIONS.CLUSTER_NODE_TIMEOUT, clusterNodeTimeout); + return this; + } + + public RedisRunner clusterSlaveValidityFactor(long clusterSlaveValidityFactor) { + addConfigOption(REDIS_OPTIONS.CLUSTER_SLAVE_VALIDITY_FACTOR, clusterSlaveValidityFactor); + return this; + } + + public RedisRunner clusterMigrationBarrier(long clusterMigrationBarrier) { + addConfigOption(REDIS_OPTIONS.CLUSTER_MIGRATION_BARRIER, clusterMigrationBarrier); + return this; + } + + public RedisRunner clusterRequireFullCoverage(boolean clusterRequireFullCoverage) { + addConfigOption(REDIS_OPTIONS.CLUSTER_REQUIRE_FULL_COVERAGE, convertBoolean(clusterRequireFullCoverage)); + return this; + } + + public RedisRunner slowlogLogSlowerThan(long slowlogLogSlowerThan) { + addConfigOption(REDIS_OPTIONS.SLOWLOG_LOG_SLOWER_THAN, slowlogLogSlowerThan); + return this; + } + + public RedisRunner slowlogMaxLen(long slowlogMaxLen) { + addConfigOption(REDIS_OPTIONS.SLOWLOG_MAX_LEN, slowlogMaxLen); + return this; + } + + public RedisRunner latencyMonitorThreshold(long latencyMonitorThreshold) { + addConfigOption(REDIS_OPTIONS.LATENCY_MONITOR_THRESHOLD, latencyMonitorThreshold); + return this; + } + + public RedisRunner notifyKeyspaceEvents(KEYSPACE_EVENTS_OPTIONS... notifyKeyspaceEvents) { + String existing = this.options.getOrDefault(REDIS_OPTIONS.NOTIFY_KEYSPACE_EVENTS, ""); + + String events = Arrays.stream(notifyKeyspaceEvents) + .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString(); + + addConfigOption(REDIS_OPTIONS.NOTIFY_KEYSPACE_EVENTS, + existing.contains(events) + ? existing + : (existing + events)); + return this; + } + + public RedisRunner hashMaxZiplistEntries(long hashMaxZiplistEntries) { + addConfigOption(REDIS_OPTIONS.HASH_MAX_ZIPLIST_ENTRIES, hashMaxZiplistEntries); + return this; + } + + public RedisRunner hashMaxZiplistValue(long hashMaxZiplistValue) { + addConfigOption(REDIS_OPTIONS.HASH_MAX_ZIPLIST_VALUE, hashMaxZiplistValue); + return this; + } + + public RedisRunner listMaxZiplistEntries(long listMaxZiplistEntries) { + addConfigOption(REDIS_OPTIONS.LIST_MAX_ZIPLIST_ENTRIES, listMaxZiplistEntries); + return this; + } + + public RedisRunner listMaxZiplistValue(long listMaxZiplistValue) { + addConfigOption(REDIS_OPTIONS.LIST_MAX_ZIPLIST_VALUE, listMaxZiplistValue); + return this; + } + + public RedisRunner setMaxIntsetEntries(long setMaxIntsetEntries) { + addConfigOption(REDIS_OPTIONS.SET_MAX_INTSET_ENTRIES, setMaxIntsetEntries); + return this; + } + + public RedisRunner zsetMaxZiplistEntries(long zsetMaxZiplistEntries) { + addConfigOption(REDIS_OPTIONS.ZSET_MAX_ZIPLIST_ENTRIES, zsetMaxZiplistEntries); + return this; + } + + public RedisRunner zsetMaxZiplistValue(long zsetMaxZiplistValue) { + addConfigOption(REDIS_OPTIONS.ZSET_MAX_ZIPLIST_VALUE, zsetMaxZiplistValue); + return this; + } + + public RedisRunner hllSparseMaxBytes(long hllSparseMaxBytes) { + addConfigOption(REDIS_OPTIONS.HLL_SPARSE_MAX_BYTES, hllSparseMaxBytes); + return this; + } + + public RedisRunner activerehashing(boolean activerehashing) { + addConfigOption(REDIS_OPTIONS.ACTIVEREHASHING, convertBoolean(activerehashing)); + return this; + } + + public RedisRunner clientOutputBufferLimit$Normal(String hardLimit, String softLimit, long softSeconds) { + addConfigOption(REDIS_OPTIONS.CLIENT_OUTPUT_BUFFER_LIMIT$NORMAL, hardLimit, softLimit, softSeconds); + return this; + } + + public RedisRunner clientOutputBufferLimit$Slave(String hardLimit, String softLimit, long softSeconds) { + addConfigOption(REDIS_OPTIONS.CLIENT_OUTPUT_BUFFER_LIMIT$SLAVE, hardLimit, softLimit, softSeconds); + return this; + } + + public RedisRunner clientOutputBufferLimit$Pubsub(String hardLimit, String softLimit, long softSeconds) { + addConfigOption(REDIS_OPTIONS.CLIENT_OUTPUT_BUFFER_LIMIT$PUBSUB, hardLimit, softLimit, softSeconds); + return this; + } + + public RedisRunner hz(int hz) { + addConfigOption(REDIS_OPTIONS.HZ, hz); + return this; + } + + public RedisRunner aofRewriteIncrementalFsync(boolean aofRewriteIncrementalFsync) { + addConfigOption(REDIS_OPTIONS.AOF_REWRITE_INCREMENTAL_FSYNC, convertBoolean(aofRewriteIncrementalFsync)); + return this; + } + + public RedisRunner protectedMode(boolean protectedMode) { + addConfigOption(REDIS_OPTIONS.PROTECTED_MODE, convertBoolean(protectedMode)); + return this; + } + + public RedisRunner sentinel() { + sentinelFile = "sentinel_conf_" + UUID.randomUUID() + ".conf"; + return this; + } + + public RedisRunner sentinelAnnounceIP(String sentinelAnnounceIP) { + addConfigOption(REDIS_OPTIONS.SENTINEL$ANNOUNCE_IP, sentinelAnnounceIP); + return this; + } + + public RedisRunner sentinelAnnouncePort(int sentinelAnnouncePort) { + addConfigOption(REDIS_OPTIONS.SENTINEL$ANNOUNCE_PORT, sentinelAnnouncePort); + return this; + } + + public RedisRunner sentinelMonitor(String masterName, String ip, int port, int quorum) { + addConfigOption(REDIS_OPTIONS.SENTINEL$MONITOR, masterName, ip, port, quorum); + return this; + } + + public RedisRunner sentinelAuthPass(String masterName, String password) { + addConfigOption(REDIS_OPTIONS.SENTINEL$AUTH_PASS, masterName, password); + return this; + } + + public RedisRunner sentinelDownAfterMilliseconds(String masterName, long downAfterMilliseconds) { + addConfigOption(REDIS_OPTIONS.SENTINEL$DOWN_AFTER_MILLISECONDS, masterName, downAfterMilliseconds); + return this; + } + + public RedisRunner sentinelParallelSyncs(String masterName, int numSlaves) { + addConfigOption(REDIS_OPTIONS.SENTINEL$PARALLEL_SYNCS, masterName, numSlaves); + return this; + } + + public RedisRunner sentinelFailoverTimeout(String masterName, long failoverTimeout) { + addConfigOption(REDIS_OPTIONS.SENTINEL$FAILOVER_TIMEOUT, masterName, failoverTimeout); + return this; + } + + public RedisRunner sentinelNotificationScript(String masterName, String scriptPath) { + addConfigOption(REDIS_OPTIONS.SENTINEL$NOTIFICATION_SCRIPT, masterName, scriptPath); + return this; + } + + public RedisRunner sentinelClientReconfigScript(String masterName, String scriptPath) { + addConfigOption(REDIS_OPTIONS.SENTINEL$CLIENT_RECONFIG_SCRIPT, masterName, scriptPath); + return this; + } + + public boolean isSentinel() { + return this.sentinelFile != null; + } + + public boolean isCluster() { + return this.clusterFile != null; + } + + public boolean isRandomDir() { + return this.randomDir; + } + + public boolean isNosave() { + return this.nosave; + } + + public String defaultDir() { + return this.defaultDir; + } + + public String dir() { + return isRandomDir() ? defaultDir() : this.path; + } + + public String getInitialBindAddr() { + return bindAddr.size() > 0 ? bindAddr.get(0) : "localhost"; + } + + public boolean deleteDBfileDir() { + File f = new File(defaultDir); + if (f.exists()) { + System.out.println("REDIS RUNNER: Deleting directory " + f.getAbsolutePath()); + return f.delete(); + } + return false; + } + + public boolean deleteSentinelFile() { + File f = new File(defaultDir + File.separator + sentinelFile); + if (f.exists()) { + System.out.println("REDIS RUNNER: Deleting sentinel config file " + f.getAbsolutePath()); + return f.delete(); + } + return false; + } + + public boolean deleteClusterFile() { + File f = new File(clusterFile); + if (f.exists() && isRandomDir()) { + System.out.println("REDIS RUNNER: Deleting cluster config file " + f.getAbsolutePath()); + return f.delete(); + } + return false; + } + + private void makeRandomDefaultDir() { + File f = new File(RedissonRuntimeEnvironment.tempDir + File.separator + UUID.randomUUID()); + if (f.exists()) { + makeRandomDefaultDir(); + } else { + System.out.println("REDIS RUNNER: Making directory " + f.getAbsolutePath()); + f.mkdirs(); + this.defaultDir = f.getAbsolutePath(); + if (RedissonRuntimeEnvironment.isWindows) { + defaultDir = defaultDir.replace("\\", "\\\\"); + } + } + } + + public static final class RedisProcess { + + private final Process redisProcess; + private final RedisRunner runner; + private RedisVersion redisVersion; + + private RedisProcess(Process redisProcess, RedisRunner runner) { + this.redisProcess = redisProcess; + this.runner = runner; + } + + public int stop() { + if (runner.isNosave() && !runner.isRandomDir()) { + RedisClient c = createDefaultRedisClientInstance(); + RedisConnection connection = c.connect(); + try { + connection.async(new RedisStrictCommand("SHUTDOWN", "NOSAVE", new VoidReplayConvertor())) + .toCompletableFuture().get(3, TimeUnit.SECONDS); + } catch (InterruptedException interruptedException) { + //shutdown via command failed, lets wait and kill it later. + } catch (ExecutionException | TimeoutException e) { + // skip + } + c.shutdown(); + connection.closeAsync().syncUninterruptibly(); + } + Process p = redisProcess; + p.destroy(); + boolean normalTermination = false; + try { + normalTermination = p.waitFor(5, TimeUnit.SECONDS); + } catch (InterruptedException ex) { + //OK lets hurry up by force kill; + } + if (!normalTermination) { + p = p.destroyForcibly(); + } + cleanup(); + int exitCode = p.exitValue(); + return exitCode == 1 && RedissonRuntimeEnvironment.isWindows ? 0 : exitCode; + } + + private void cleanup() { + if (runner.isSentinel()) { + runner.deleteSentinelFile(); + } + if (runner.isCluster()) { + runner.deleteClusterFile(); + } + if (runner.isRandomDir()) { + runner.deleteDBfileDir(); + } + } + + public String getDefaultDir() { + return runner.getDefaultDir(); + } + + public Process getRedisProcess() { + return redisProcess; + } + + public RedisClient createRedisClientInstance() { + if (redisProcess.isAlive()) { + RedisClientConfig config = new RedisClientConfig(); + config.setAddress(runner.getInitialBindAddr(), runner.getPort()); + return RedisClient.create(config); + } + throw new IllegalStateException("Redis server instance is not running."); + } + + public RedisVersion getRedisVersion() { + if (redisVersion == null) { + RedisConnection c = createRedisClientInstance().connect(); + Map serverMap = c.sync(RedisCommands.INFO_SERVER); + redisVersion = new RedisVersion(serverMap.get("redis_version")); + c.closeAsync(); + } + return redisVersion; + } + + public int getRedisServerPort() { + return runner.getPort(); + } + + public String getRedisServerBindAddress() { + return runner.getInitialBindAddr(); + } + + public String getRedisServerAddressAndPort() { + return "redis://" + getRedisServerBindAddress() + ":" + getRedisServerPort(); + } + + public boolean isAlive() { + return redisProcess.isAlive(); + } + } + + public static RedisRunner.RedisProcess startDefaultRedisServerInstance() throws IOException, InterruptedException, FailedToStartRedisException { + if (defaultRedisInstance == null) { + System.out.println("REDIS RUNNER: Starting up default instance..."); + defaultRedisInstance = new RedisRunner().nosave().randomDir().randomPort().run(); + } + return defaultRedisInstance; + } + + public static int shutDownDefaultRedisServerInstance() throws InterruptedException { + if (defaultRedisInstance != null) { + System.out.println("REDIS RUNNER: Shutting down default instance..."); + try { + defaultRedisInstanceExitCode = defaultRedisInstance.stop(); + } finally { + defaultRedisInstance = null; + } + } else { + System.out.println("REDIS RUNNER: Default instance is already down with an exit code " + defaultRedisInstanceExitCode); + } + return defaultRedisInstanceExitCode; + } + + public static boolean isDefaultRedisServerInstanceRunning() { + return defaultRedisInstance != null && defaultRedisInstance.redisProcess.isAlive(); + } + + public static RedisClient createDefaultRedisClientInstance() { + return defaultRedisInstance.createRedisClientInstance(); + } + + public String getDefaultDir() { + return defaultDir; + } + + public static RedisRunner.RedisProcess getDefaultRedisServerInstance() { + return defaultRedisInstance; + } + + public static String getDefaultRedisServerBindAddressAndPort() { + return "redis://" + defaultRedisInstance.getRedisServerBindAddress() + + ":" + + defaultRedisInstance.getRedisServerPort(); + } + + public static int findFreePort() { + ServerSocket socket = null; + try { + socket = new ServerSocket(0); + socket.setReuseAddress(true); + int port = socket.getLocalPort(); + if (port > 55535 && isFreePort(port - 10000)) { + return port - 10000; + } else { + return port; + } + } catch (IOException e) { + } finally { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + } + } + } + throw new IllegalStateException("Could not find a free TCP/IP port."); + } + + public static boolean isFreePort(int port) { + ServerSocket socket = null; + try { + socket = new ServerSocket(port); + socket.setReuseAddress(true); + return true; + } catch (IOException e) { + } finally { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + } + } + } + return false; + } + + public static class FailedToStartRedisException extends RuntimeException { + + public FailedToStartRedisException() { + } + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedisVersion.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedisVersion.java new file mode 100644 index 000000000..1b4db4746 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedisVersion.java @@ -0,0 +1,58 @@ +package org.redisson; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * + * @author Rui Gu (https://github.com/jackygurui) + */ +public class RedisVersion implements Comparable{ + + private final String fullVersion; + private final Integer majorVersion; + private final Integer minorVersion; + private final Integer patchVersion; + + public RedisVersion(String fullVersion) { + this.fullVersion = fullVersion; + Matcher matcher = Pattern.compile("^([\\d]+)\\.([\\d]+)\\.([\\d]+)$").matcher(fullVersion); + matcher.find(); + majorVersion = Integer.parseInt(matcher.group(1)); + minorVersion = Integer.parseInt(matcher.group(2)); + patchVersion = Integer.parseInt(matcher.group(3)); + } + + public String getFullVersion() { + return fullVersion; + } + + public int getMajorVersion() { + return majorVersion; + } + + public int getMinorVersion() { + return minorVersion; + } + + public int getPatchVersion() { + return patchVersion; + } + + @Override + public int compareTo(RedisVersion o) { + int ma = this.majorVersion.compareTo(o.majorVersion); + int mi = this.minorVersion.compareTo(o.minorVersion); + int pa = this.patchVersion.compareTo(o.patchVersion); + return ma != 0 ? ma : mi != 0 ? mi : pa; + } + + public int compareTo(String redisVersion) { + return this.compareTo(new RedisVersion(redisVersion)); + } + + public static int compareTo(String redisVersion1, String redisVersion2) { + return new RedisVersion(redisVersion1).compareTo(redisVersion2); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedissonRuntimeEnvironment.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedissonRuntimeEnvironment.java new file mode 100644 index 000000000..6d426c601 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/RedissonRuntimeEnvironment.java @@ -0,0 +1,21 @@ +package org.redisson; + +import java.util.Locale; + +/** + * + * @author Rui Gu (https://github.com/jackygurui) + */ +public class RedissonRuntimeEnvironment { + + public static final boolean isTravis = "true".equalsIgnoreCase(System.getProperty("travisEnv")); + public static final String redisBinaryPath = System.getProperty("redisBinary", "C:\\redis\\redis-server.exe"); + public static final String tempDir = System.getProperty("java.io.tmpdir"); + public static final String OS; + public static final boolean isWindows; + + static { + OS = System.getProperty("os.name", "generic"); + isWindows = OS.toLowerCase(Locale.ENGLISH).contains("win"); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/BaseConnectionTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/BaseConnectionTest.java new file mode 100644 index 000000000..770f1d283 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/BaseConnectionTest.java @@ -0,0 +1,16 @@ +package org.redisson.spring.data.connection; + +import org.junit.Before; +import org.redisson.BaseTest; +import org.springframework.data.redis.connection.RedisConnection; + +public abstract class BaseConnectionTest extends BaseTest { + + RedisConnection connection; + + @Before + public void init() { + connection = new RedissonConnection(redisson); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonClusterConnectionRenameTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonClusterConnectionRenameTest.java new file mode 100644 index 000000000..ed8a07bb7 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonClusterConnectionRenameTest.java @@ -0,0 +1,166 @@ +package org.redisson.spring.data.connection; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.redisson.ClusterRunner; +import org.redisson.ClusterRunner.ClusterProcesses; +import org.redisson.RedisRunner; +import org.redisson.RedisRunner.FailedToStartRedisException; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.SubscriptionMode; +import org.redisson.connection.balancer.RandomLoadBalancer; +import org.springframework.dao.InvalidDataAccessResourceUsageException; + +import java.io.IOException; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.redisson.connection.MasterSlaveConnectionManager.MAX_SLOT; + +@RunWith(Parameterized.class) +public class RedissonClusterConnectionRenameTest { + + @Parameterized.Parameters(name= "{index} - same slot = {0}") + public static Iterable data() { + return Arrays.asList(new Object[][] { + {false}, + {true} + }); + } + + @Parameterized.Parameter(0) + public boolean sameSlot; + + static RedissonClient redisson; + static RedissonClusterConnection connection; + static ClusterProcesses process; + + byte[] originalKey = "key".getBytes(); + byte[] newKey = "unset".getBytes(); + byte[] value = "value".getBytes(); + + @BeforeClass + public static void before() throws FailedToStartRedisException, IOException, InterruptedException { + RedisRunner master1 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner master2 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner master3 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner slave1 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner slave2 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner slave3 = new RedisRunner().randomPort().randomDir().nosave(); + + + ClusterRunner clusterRunner = new ClusterRunner() + .addNode(master1, slave1) + .addNode(master2, slave2) + .addNode(master3, slave3); + process = clusterRunner.run(); + + Config config = new Config(); + config.useClusterServers() + .setSubscriptionMode(SubscriptionMode.SLAVE) + .setLoadBalancer(new RandomLoadBalancer()) + .addNodeAddress(process.getNodes().stream().findAny().get().getRedisServerAddressAndPort()); + + redisson = Redisson.create(config); + connection = new RedissonClusterConnection(redisson); + } + + @AfterClass + public static void after() { + process.shutdown(); + redisson.shutdown(); + } + + @After + public void cleanup() { + connection.del(originalKey); + connection.del(newKey); + } + + @Test + public void testRename() { + connection.set(originalKey, value); + connection.expire(originalKey, 1000); + + Integer originalSlot = connection.clusterGetSlotForKey(originalKey); + newKey = getNewKeyForSlot(originalKey, getTargetSlot(originalSlot)); + + connection.rename(originalKey, newKey); + + assertThat(connection.get(newKey)).isEqualTo(value); + assertThat(connection.ttl(newKey)).isGreaterThan(0); + } + + @Test + public void testRename_pipeline() { + connection.set(originalKey, value); + + Integer originalSlot = connection.clusterGetSlotForKey(originalKey); + newKey = getNewKeyForSlot(originalKey, getTargetSlot(originalSlot)); + + connection.openPipeline(); + assertThatThrownBy(() -> connection.rename(originalKey, newKey)).isInstanceOf(InvalidDataAccessResourceUsageException.class); + connection.closePipeline(); + } + + protected byte[] getNewKeyForSlot(byte[] originalKey, Integer targetSlot) { + int counter = 0; + + byte[] newKey = (new String(originalKey) + counter).getBytes(); + + Integer newKeySlot = connection.clusterGetSlotForKey(newKey); + + while(!newKeySlot.equals(targetSlot)) { + counter++; + newKey = (new String(originalKey) + counter).getBytes(); + newKeySlot = connection.clusterGetSlotForKey(newKey); + } + + return newKey; + } + + @Test + public void testRenameNX() { + connection.set(originalKey, value); + connection.expire(originalKey, 1000); + + Integer originalSlot = connection.clusterGetSlotForKey(originalKey); + newKey = getNewKeyForSlot(originalKey, getTargetSlot(originalSlot)); + + Boolean result = connection.renameNX(originalKey, newKey); + + assertThat(connection.get(newKey)).isEqualTo(value); + assertThat(connection.ttl(newKey)).isGreaterThan(0); + assertThat(result).isTrue(); + + connection.set(originalKey, value); + + result = connection.renameNX(originalKey, newKey); + + assertThat(result).isFalse(); + } + + @Test + public void testRenameNX_pipeline() { + connection.set(originalKey, value); + + Integer originalSlot = connection.clusterGetSlotForKey(originalKey); + newKey = getNewKeyForSlot(originalKey, getTargetSlot(originalSlot)); + + connection.openPipeline(); + assertThatThrownBy(() -> connection.renameNX(originalKey, newKey)).isInstanceOf(InvalidDataAccessResourceUsageException.class); + connection.closePipeline(); + } + + private Integer getTargetSlot(Integer originalSlot) { + return sameSlot ? originalSlot : MAX_SLOT - originalSlot - 1; + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonClusterConnectionTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonClusterConnectionTest.java new file mode 100644 index 000000000..eb8f15c15 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonClusterConnectionTest.java @@ -0,0 +1,293 @@ +package org.redisson.spring.data.connection; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.redisson.ClusterRunner; +import org.redisson.ClusterRunner.ClusterProcesses; +import org.redisson.RedisRunner; +import org.redisson.RedisRunner.FailedToStartRedisException; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.SubscriptionMode; +import org.redisson.connection.MasterSlaveConnectionManager; +import org.redisson.connection.balancer.RandomLoadBalancer; +import org.springframework.data.redis.connection.ClusterInfo; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisNode.NodeType; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.RedisClientInfo; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RedissonClusterConnectionTest { + + static RedissonClient redisson; + static RedissonClusterConnection connection; + static ClusterProcesses process; + + @BeforeClass + public static void before() throws FailedToStartRedisException, IOException, InterruptedException { + RedisRunner master1 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner master2 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner master3 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner slave1 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner slave2 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner slave3 = new RedisRunner().randomPort().randomDir().nosave(); + + + ClusterRunner clusterRunner = new ClusterRunner() + .addNode(master1, slave1) + .addNode(master2, slave2) + .addNode(master3, slave3); + process = clusterRunner.run(); + + Config config = new Config(); + config.useClusterServers() + .setSubscriptionMode(SubscriptionMode.SLAVE) + .setLoadBalancer(new RandomLoadBalancer()) + .addNodeAddress(process.getNodes().stream().findAny().get().getRedisServerAddressAndPort()); + + redisson = Redisson.create(config); + connection = new RedissonClusterConnection(redisson); + } + + @AfterClass + public static void after() { + process.shutdown(); + redisson.shutdown(); + } + + @Test + public void testDel() { + List keys = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + byte[] key = ("test" + i).getBytes(); + keys.add(key); + connection.set(key, ("test" + i).getBytes()); + } + assertThat(connection.del(keys.toArray(new byte[0][]))).isEqualTo(10); + } + + @Test + public void testScan() { + for (int i = 0; i < 1000; i++) { + connection.set(("" + i).getBytes(StandardCharsets.UTF_8), ("" + i).getBytes(StandardCharsets.UTF_8)); + } + + Cursor b = connection.scan(ScanOptions.scanOptions().build()); + int counter = 0; + while (b.hasNext()) { + b.next(); + counter++; + } + assertThat(counter).isEqualTo(1000); + } + + @Test + public void testMSet() { + Map map = new HashMap<>(); + for (int i = 0; i < 10; i++) { + map.put(("test" + i).getBytes(), ("test" + i*100).getBytes()); + } + connection.mSet(map); + for (Map.Entry entry : map.entrySet()) { + assertThat(connection.get(entry.getKey())).isEqualTo(entry.getValue()); + } + } + + @Test + public void testMGet() { + Map map = new HashMap<>(); + for (int i = 0; i < 10; i++) { + map.put(("test" + i).getBytes(), ("test" + i*100).getBytes()); + } + connection.mSet(map); + List r = connection.mGet(map.keySet().toArray(new byte[0][])); + assertThat(r).containsExactly(map.values().toArray(new byte[0][])); + } + + @Test + public void testClusterGetNodes() { + Iterable nodes = connection.clusterGetNodes(); + assertThat(nodes).hasSize(6); + for (RedisClusterNode redisClusterNode : nodes) { + assertThat(redisClusterNode.getLinkState()).isNotNull(); + assertThat(redisClusterNode.getFlags()).isNotEmpty(); + assertThat(redisClusterNode.getHost()).isNotNull(); + assertThat(redisClusterNode.getPort()).isNotNull(); + assertThat(redisClusterNode.getId()).isNotNull(); + assertThat(redisClusterNode.getType()).isNotNull(); + if (redisClusterNode.getType() == NodeType.MASTER) { + assertThat(redisClusterNode.getSlotRange().getSlots()).isNotEmpty(); + } else { + assertThat(redisClusterNode.getMasterId()).isNotNull(); + } + } + } + + @Test + public void testClusterGetNodesMaster() { + Iterable nodes = connection.clusterGetNodes(); + for (RedisClusterNode redisClusterNode : nodes) { + if (redisClusterNode.getType() == NodeType.MASTER) { + Collection slaves = connection.clusterGetReplicas(redisClusterNode); + assertThat(slaves).hasSize(1); + } + } + } + + @Test + public void testClusterGetMasterSlaveMap() { + Map> map = connection.clusterGetMasterReplicaMap(); + assertThat(map).hasSize(3); + for (Collection slaves : map.values()) { + assertThat(slaves).hasSize(1); + } + } + + @Test + public void testClusterGetSlotForKey() { + Integer slot = connection.clusterGetSlotForKey("123".getBytes()); + assertThat(slot).isNotNull(); + } + + @Test + public void testClusterGetNodeForSlot() { + RedisClusterNode node1 = connection.clusterGetNodeForSlot(1); + RedisClusterNode node2 = connection.clusterGetNodeForSlot(16000); + assertThat(node1.getId()).isNotEqualTo(node2.getId()); + } + + @Test + public void testClusterGetNodeForKey() { + RedisClusterNode node = connection.clusterGetNodeForKey("123".getBytes()); + assertThat(node).isNotNull(); + } + + @Test + public void testClusterGetClusterInfo() { + ClusterInfo info = connection.clusterGetClusterInfo(); + assertThat(info.getSlotsFail()).isEqualTo(0); + assertThat(info.getSlotsOk()).isEqualTo(MasterSlaveConnectionManager.MAX_SLOT); + assertThat(info.getSlotsAssigned()).isEqualTo(MasterSlaveConnectionManager.MAX_SLOT); + } + + @Test + public void testClusterAddRemoveSlots() { + RedisClusterNode master = getFirstMaster(); + Integer slot = master.getSlotRange().getSlots().iterator().next(); + connection.clusterDeleteSlots(master, slot); + connection.clusterAddSlots(master, slot); + } + + @Test + public void testClusterCountKeysInSlot() { + Long t = connection.clusterCountKeysInSlot(1); + assertThat(t).isZero(); + } + + @Test + public void testClusterMeetForget() { + RedisClusterNode master = getFirstMaster(); + connection.clusterForget(master); + connection.clusterMeet(master); + } + + @Test + public void testClusterGetKeysInSlot() { + List keys = connection.clusterGetKeysInSlot(12, 10); + assertThat(keys).isEmpty(); + } + + @Test + public void testClusterPing() { + RedisClusterNode master = getFirstMaster(); + String res = connection.ping(master); + assertThat(res).isEqualTo("PONG"); + } + + @Test + public void testDbSize() { + RedisClusterNode master = getFirstMaster(); + Long size = connection.dbSize(master); + assertThat(size).isZero(); + } + + @Test + public void testInfo() { + RedisClusterNode master = getFirstMaster(); + Properties info = connection.info(master); + assertThat(info.size()).isGreaterThan(10); + } + + @Test + public void testDelPipeline() { + byte[] k = "key".getBytes(); + byte[] v = "val".getBytes(); + connection.set(k, v); + + connection.openPipeline(); + connection.get(k); + connection.del(k); + List results = connection.closePipeline(); + byte[] val = (byte[])results.get(0); + assertThat(val).isEqualTo(v); + Long res = (Long) results.get(1); + assertThat(res).isEqualTo(1); + } + + @Test + public void testResetConfigStats() { + RedisClusterNode master = getFirstMaster(); + connection.resetConfigStats(master); + } + + @Test + public void testTime() { + RedisClusterNode master = getFirstMaster(); + Long time = connection.time(master); + assertThat(time).isGreaterThan(1000); + } + + @Test + public void testGetClientList() { + RedisClusterNode master = getFirstMaster(); + List list = connection.getClientList(master); + assertThat(list.size()).isGreaterThan(10); + } + + @Test + public void testSetConfig() { + RedisClusterNode master = getFirstMaster(); + connection.setConfig(master, "timeout", "10"); + } + + @Test + public void testGetConfig() { + RedisClusterNode master = getFirstMaster(); + Properties config = connection.getConfig(master, "*"); + assertThat(config.size()).isGreaterThan(20); + } + + protected RedisClusterNode getFirstMaster() { + Map> map = connection.clusterGetMasterReplicaMap(); + RedisClusterNode master = map.keySet().iterator().next(); + return master; + } + + @Test + public void testConnectionFactoryReturnsClusterConnection() { + RedisConnectionFactory connectionFactory = new RedissonConnectionFactory(redisson); + + assertThat(connectionFactory.getConnection()).isInstanceOf(RedissonClusterConnection.class); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonConnectionTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonConnectionTest.java new file mode 100644 index 000000000..297a87ba5 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonConnectionTest.java @@ -0,0 +1,125 @@ +package org.redisson.spring.data.connection; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.GeoResults; +import org.springframework.data.geo.Point; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; +import org.springframework.data.redis.connection.RedisZSetCommands; +import org.springframework.data.redis.connection.zset.Tuple; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.SetOperations; +import org.springframework.data.redis.core.types.Expiration; + +import java.util.Set; + +public class RedissonConnectionTest extends BaseConnectionTest { + + @Test + public void testExecute() { + Long s = (Long) connection.execute("ttl", "key".getBytes()); + assertThat(s).isEqualTo(-2); + connection.execute("flushDb"); + } + + @Test + public void testRandomMembers() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(new RedissonConnectionFactory(redisson)); + redisTemplate.afterPropertiesSet(); + + + SetOperations ops = redisTemplate.opsForSet(); + ops.add("val", 1, 2, 3, 4); + Set values = redisTemplate.opsForSet().distinctRandomMembers("val", 1L); + assertThat(values).containsAnyOf(1, 2, 3, 4); + + Integer v = redisTemplate.opsForSet().randomMember("val"); + assertThat(v).isNotNull(); + } + + @Test + public void testRangeByLex() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(new RedissonConnectionFactory(redisson)); + redisTemplate.afterPropertiesSet(); + + RedisZSetCommands.Range range = new RedisZSetCommands.Range(); + range.lt("c"); + Set zSetValue = redisTemplate.opsForZSet().rangeByLex("val", range); + assertThat(zSetValue).isEmpty(); + } + + @Test + public void testGeo() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(new RedissonConnectionFactory(redisson)); + redisTemplate.afterPropertiesSet(); + + String key = "test_geo_key"; + Point point = new Point(116.401001, 40.119499); + redisTemplate.opsForGeo().add(key, point, "a"); + + point = new Point(111.545998, 36.133499); + redisTemplate.opsForGeo().add(key, point, "b"); + + point = new Point(111.483002, 36.030998); + redisTemplate.opsForGeo().add(key, point, "c"); + Circle within = new Circle(116.401001, 40.119499, 80000); + RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates(); + GeoResults> res = redisTemplate.opsForGeo().radius(key, within, args); + assertThat(res.getContent().get(0).getContent().getName()).isEqualTo("a"); + } + + @Test + public void testZSet() { + connection.zAdd(new byte[] {1}, -1, new byte[] {1}); + connection.zAdd(new byte[] {1}, 2, new byte[] {2}); + connection.zAdd(new byte[] {1}, 10, new byte[] {3}); + + assertThat(connection.zRangeByScore(new byte[] {1}, Double.NEGATIVE_INFINITY, 5)) + .containsOnly(new byte[] {1}, new byte[] {2}); + } + + @Test + public void testEcho() { + assertThat(connection.echo("test".getBytes())).isEqualTo("test".getBytes()); + } + + @Test + public void testSetGet() { + connection.set("key".getBytes(), "value".getBytes()); + assertThat(connection.get("key".getBytes())).isEqualTo("value".getBytes()); + } + + @Test + public void testSetExpiration() { + assertThat(connection.set("key".getBytes(), "value".getBytes(), Expiration.milliseconds(111122), SetOption.SET_IF_ABSENT)).isTrue(); + assertThat(connection.get("key".getBytes())).isEqualTo("value".getBytes()); + } + + @Test + public void testHSetGet() { + assertThat(connection.hSet("key".getBytes(), "field".getBytes(), "value".getBytes())).isTrue(); + assertThat(connection.hGet("key".getBytes(), "field".getBytes())).isEqualTo("value".getBytes()); + } + + @Test + public void testZScan() { + connection.zAdd("key".getBytes(), 1, "value1".getBytes()); + connection.zAdd("key".getBytes(), 2, "value2".getBytes()); + + Cursor t = connection.zScan("key".getBytes(), ScanOptions.scanOptions().build()); + assertThat(t.hasNext()).isTrue(); + assertThat(t.next().getValue()).isEqualTo("value1".getBytes()); + assertThat(t.hasNext()).isTrue(); + assertThat(t.next().getValue()).isEqualTo("value2".getBytes()); + } + + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonMultiConnectionTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonMultiConnectionTest.java new file mode 100644 index 000000000..91c83f751 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonMultiConnectionTest.java @@ -0,0 +1,96 @@ +package org.redisson.spring.data.connection; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.Test; +import org.redisson.BaseTest; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.SessionCallback; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +public class RedissonMultiConnectionTest extends BaseConnectionTest { + + @Test + public void testBroken() throws InterruptedException { + StringRedisTemplate stringRedisTemplate = new StringRedisTemplate(new RedissonConnectionFactory(redisson)); + ExecutorService e = Executors.newFixedThreadPool(32); + AtomicBoolean hasErrors = new AtomicBoolean(); + for (int i = 0; i < 10; i++) { + e.submit(() -> { + stringRedisTemplate.execute(new SessionCallback() { + @Override + public Void execute(RedisOperations operations) throws DataAccessException { + try { + ValueOperations valueOps = operations.opsForValue(); + operations.multi(); + valueOps.set("test3", "value"); + } catch (Exception e) { + e.printStackTrace(); + hasErrors.set(true); + } + return null; + } + }); + stringRedisTemplate.execute(new SessionCallback() { + @Override + public Void execute(RedisOperations operations) throws DataAccessException { + try { + ValueOperations valueOps = operations.opsForValue(); + valueOps.set("test1", "value"); + assertThat(valueOps.get("test1")).isEqualTo("value"); + } catch (Exception e) { + e.printStackTrace(); + hasErrors.set(true); + } + return null; + } + }); + }); + } + e.shutdown(); + e.awaitTermination(1, TimeUnit.MINUTES); + assertThat(hasErrors).isFalse(); + } + + @Test + public void testEcho() { + RedissonConnection connection = new RedissonConnection(redisson); + connection.multi(); + assertThat(connection.echo("test".getBytes())).isNull(); + assertThat(connection.exec().iterator().next()).isEqualTo("test".getBytes()); + } + + @Test + public void testSetGet() { + RedissonConnection connection = new RedissonConnection(redisson); + connection.multi(); + assertThat(connection.isQueueing()).isTrue(); + connection.set("key".getBytes(), "value".getBytes()); + assertThat(connection.get("key".getBytes())).isNull(); + + List result = connection.exec(); + assertThat(connection.isQueueing()).isFalse(); + assertThat(result.get(0)).isEqualTo("value".getBytes()); + } + + @Test + public void testHSetGet() { + RedissonConnection connection = new RedissonConnection(redisson); + connection.multi(); + assertThat(connection.hSet("key".getBytes(), "field".getBytes(), "value".getBytes())).isNull(); + assertThat(connection.hGet("key".getBytes(), "field".getBytes())).isNull(); + + List result = connection.exec(); + assertThat((Boolean)result.get(0)).isTrue(); + assertThat(result.get(1)).isEqualTo("value".getBytes()); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonPipelineConnectionTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonPipelineConnectionTest.java new file mode 100644 index 000000000..643e7423a --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonPipelineConnectionTest.java @@ -0,0 +1,64 @@ +package org.redisson.spring.data.connection; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.Test; +import org.redisson.BaseTest; + +public class RedissonPipelineConnectionTest extends BaseConnectionTest { + + @Test + public void testDel() { + RedissonConnection connection = new RedissonConnection(redisson); + byte[] key = "my_key".getBytes(); + byte[] value = "my_value".getBytes(); + connection.set(key, value); + + connection.openPipeline(); + connection.get(key); + connection.del(key); + + List results = connection.closePipeline(); + byte[] val = (byte[])results.get(0); + assertThat(val).isEqualTo(value); + Long res = (Long) results.get(1); + assertThat(res).isEqualTo(1); + } + + @Test + public void testEcho() { + RedissonConnection connection = new RedissonConnection(redisson); + connection.openPipeline(); + assertThat(connection.echo("test".getBytes())).isNull(); + assertThat(connection.closePipeline().iterator().next()).isEqualTo("test".getBytes()); + } + + @Test + public void testSetGet() { + RedissonConnection connection = new RedissonConnection(redisson); + connection.openPipeline(); + assertThat(connection.isPipelined()).isTrue(); + connection.set("key".getBytes(), "value".getBytes()); + assertThat(connection.get("key".getBytes())).isNull(); + + List result = connection.closePipeline(); + assertThat(connection.isPipelined()).isFalse(); + assertThat(result.get(0)).isEqualTo("value".getBytes()); + } + + @Test + public void testHSetGet() { + RedissonConnection connection = new RedissonConnection(redisson); + connection.openPipeline(); + assertThat(connection.hSet("key".getBytes(), "field".getBytes(), "value".getBytes())).isNull(); + assertThat(connection.hGet("key".getBytes(), "field".getBytes())).isNull(); + + List result = connection.closePipeline(); + assertThat((Boolean)result.get(0)).isTrue(); + assertThat(result.get(1)).isEqualTo("value".getBytes()); + } + + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonReactiveClusterKeyCommandsTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonReactiveClusterKeyCommandsTest.java new file mode 100644 index 000000000..c77beb3ef --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonReactiveClusterKeyCommandsTest.java @@ -0,0 +1,193 @@ +package org.redisson.spring.data.connection; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.redisson.*; +import org.redisson.ClusterRunner.ClusterProcesses; +import org.redisson.RedisRunner.FailedToStartRedisException; +import org.redisson.api.RedissonClient; +import org.redisson.client.codec.StringCodec; +import org.redisson.client.protocol.RedisCommands; +import org.redisson.config.Config; +import org.redisson.config.SubscriptionMode; +import org.redisson.connection.balancer.RandomLoadBalancer; +import org.redisson.reactive.CommandReactiveService; +import org.springframework.data.redis.RedisSystemException; + + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.redisson.connection.MasterSlaveConnectionManager.MAX_SLOT; + +@RunWith(Parameterized.class) +public class RedissonReactiveClusterKeyCommandsTest { + + @Parameterized.Parameters(name= "{index} - same slot = {0}; has ttl = {1}") + public static Iterable data() { + return Arrays.asList(new Object[][] { + {false, false}, + {true, false}, + {false, true}, + {true, true} + }); + } + + @Parameterized.Parameter(0) + public boolean sameSlot; + + @Parameterized.Parameter(1) + public boolean hasTtl; + + static RedissonClient redisson; + static RedissonReactiveRedisClusterConnection connection; + static ClusterProcesses process; + + ByteBuffer originalKey = ByteBuffer.wrap("key".getBytes()); + ByteBuffer newKey = ByteBuffer.wrap("unset".getBytes()); + ByteBuffer value = ByteBuffer.wrap("value".getBytes()); + + @BeforeClass + public static void before() throws FailedToStartRedisException, IOException, InterruptedException { + RedisRunner master1 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner master2 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner master3 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner slave1 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner slave2 = new RedisRunner().randomPort().randomDir().nosave(); + RedisRunner slave3 = new RedisRunner().randomPort().randomDir().nosave(); + + + ClusterRunner clusterRunner = new ClusterRunner() + .addNode(master1, slave1) + .addNode(master2, slave2) + .addNode(master3, slave3); + process = clusterRunner.run(); + + Config config = new Config(); + config.useClusterServers() + .setSubscriptionMode(SubscriptionMode.SLAVE) + .setLoadBalancer(new RandomLoadBalancer()) + .addNodeAddress(process.getNodes().stream().findAny().get().getRedisServerAddressAndPort()); + + redisson = Redisson.create(config); + connection = new RedissonReactiveRedisClusterConnection(((RedissonReactive)redisson.reactive()).getCommandExecutor()); + } + + @AfterClass + public static void after() { + process.shutdown(); + redisson.shutdown(); + } + + @After + public void cleanup() { + connection.keyCommands().del(originalKey) + .and(connection.keyCommands().del(newKey)) + .block(); + } + + @Test + public void testRename() { + connection.stringCommands().set(originalKey, value).block(); + + if (hasTtl) { + connection.keyCommands().expire(originalKey, Duration.ofSeconds(1000)).block(); + } + + Integer originalSlot = getSlotForKey(originalKey); + newKey = getNewKeyForSlot(new String(originalKey.array()), getTargetSlot(originalSlot)); + + Boolean response = connection.keyCommands().rename(originalKey, newKey).block(); + + assertThat(response).isTrue(); + + final ByteBuffer newKeyValue = connection.stringCommands().get(newKey).block(); + assertThat(newKeyValue).isEqualTo(value); + if (hasTtl) { + assertThat(connection.keyCommands().ttl(newKey).block()).isGreaterThan(0); + } else { + assertThat(connection.keyCommands().ttl(newKey).block()).isEqualTo(-1); + } + } + + @Test + public void testRename_keyNotExist() { + Integer originalSlot = getSlotForKey(originalKey); + newKey = getNewKeyForSlot(new String(originalKey.array()), getTargetSlot(originalSlot)); + + if (sameSlot) { + // This is a quirk of the implementation - since same-slot renames use the non-cluster version, + // the result is a Redis error. This behavior matches other spring-data-redis implementations + assertThatThrownBy(() -> connection.keyCommands().rename(originalKey, newKey).block()) + .isInstanceOf(RedisSystemException.class); + + } else { + Boolean response = connection.keyCommands().rename(originalKey, newKey).block(); + + assertThat(response).isTrue(); + + final ByteBuffer newKeyValue = connection.stringCommands().get(newKey).block(); + assertThat(newKeyValue).isEqualTo(null); + } + } + + protected ByteBuffer getNewKeyForSlot(String originalKey, Integer targetSlot) { + int counter = 0; + + ByteBuffer newKey = ByteBuffer.wrap((originalKey + counter).getBytes()); + + Integer newKeySlot = getSlotForKey(newKey); + + while(!newKeySlot.equals(targetSlot)) { + counter++; + newKey = ByteBuffer.wrap((originalKey + counter).getBytes()); + newKeySlot = getSlotForKey(newKey); + } + + return newKey; + } + + @Test + public void testRenameNX() { + connection.stringCommands().set(originalKey, value).block(); + if (hasTtl) { + connection.keyCommands().expire(originalKey, Duration.ofSeconds(1000)).block(); + } + + Integer originalSlot = getSlotForKey(originalKey); + newKey = getNewKeyForSlot(new String(originalKey.array()), getTargetSlot(originalSlot)); + + Boolean result = connection.keyCommands().renameNX(originalKey, newKey).block(); + + assertThat(result).isTrue(); + assertThat(connection.stringCommands().get(newKey).block()).isEqualTo(value); + if (hasTtl) { + assertThat(connection.keyCommands().ttl(newKey).block()).isGreaterThan(0); + } else { + assertThat(connection.keyCommands().ttl(newKey).block()).isEqualTo(-1); + } + + connection.stringCommands().set(originalKey, value).block(); + + result = connection.keyCommands().renameNX(originalKey, newKey).block(); + + assertThat(result).isFalse(); + } + + private Integer getTargetSlot(Integer originalSlot) { + return sameSlot ? originalSlot : MAX_SLOT - originalSlot - 1; + } + + private Integer getSlotForKey(ByteBuffer key) { + return (Integer) connection.read(null, StringCodec.INSTANCE, RedisCommands.KEYSLOT, key.array()).block(); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonScriptReactiveTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonScriptReactiveTest.java new file mode 100644 index 000000000..2f68fbb93 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonScriptReactiveTest.java @@ -0,0 +1,33 @@ +package org.redisson.spring.data.connection; + +import org.junit.Test; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.ReturnType; +import reactor.core.publisher.Flux; + +import java.nio.ByteBuffer; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RedissonScriptReactiveTest extends BaseConnectionTest { + + @Test + public void testEval() { + RedissonConnectionFactory factory = new RedissonConnectionFactory(redisson); + ReactiveRedisConnection cc = factory.getReactiveConnection(); + + String s = "local ret = {}" + + "local mysqlKeys = {}" + + "table.insert(ret, 'test1')" + + "table.insert(ret, 'test2')" + + "table.insert(ret, 'test3')" + + "table.insert(ret, mysqlKeys)" + + "return ret"; + Flux> ss = cc.scriptingCommands().eval(ByteBuffer.wrap(s.getBytes()), ReturnType.MULTI, 0); + List r = ss.blockFirst(); + assertThat(r.get(2)).isEqualTo(ByteBuffer.wrap("test3".getBytes())); + assertThat((List) r.get(3)).isEmpty(); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSentinelConnectionTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSentinelConnectionTest.java new file mode 100644 index 000000000..b728c865b --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSentinelConnectionTest.java @@ -0,0 +1,132 @@ +package org.redisson.spring.data.connection; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.Collection; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.redisson.RedisRunner; +import org.redisson.RedisRunner.FailedToStartRedisException; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.connection.balancer.RandomLoadBalancer; +import org.springframework.data.redis.connection.RedisSentinelConnection; +import org.springframework.data.redis.connection.RedisServer; + +public class RedissonSentinelConnectionTest { + + RedissonClient redisson; + RedisSentinelConnection connection; + RedisRunner.RedisProcess master; + RedisRunner.RedisProcess slave1; + RedisRunner.RedisProcess slave2; + RedisRunner.RedisProcess sentinel1; + RedisRunner.RedisProcess sentinel2; + RedisRunner.RedisProcess sentinel3; + + @Before + public void before() throws FailedToStartRedisException, IOException, InterruptedException { + master = new RedisRunner() + .nosave() + .randomDir() + .run(); + slave1 = new RedisRunner() + .port(6380) + .nosave() + .randomDir() + .slaveof("127.0.0.1", 6379) + .run(); + slave2 = new RedisRunner() + .port(6381) + .nosave() + .randomDir() + .slaveof("127.0.0.1", 6379) + .run(); + sentinel1 = new RedisRunner() + .nosave() + .randomDir() + .port(26379) + .sentinel() + .sentinelMonitor("myMaster", "127.0.0.1", 6379, 2) + .run(); + sentinel2 = new RedisRunner() + .nosave() + .randomDir() + .port(26380) + .sentinel() + .sentinelMonitor("myMaster", "127.0.0.1", 6379, 2) + .run(); + sentinel3 = new RedisRunner() + .nosave() + .randomDir() + .port(26381) + .sentinel() + .sentinelMonitor("myMaster", "127.0.0.1", 6379, 2) + .run(); + + Thread.sleep(5000); + + Config config = new Config(); + config.useSentinelServers() + .setLoadBalancer(new RandomLoadBalancer()) + .addSentinelAddress(sentinel3.getRedisServerAddressAndPort()).setMasterName("myMaster"); + redisson = Redisson.create(config); + + RedissonConnectionFactory factory = new RedissonConnectionFactory(redisson); + connection = factory.getSentinelConnection(); + } + + @After + public void after() { + sentinel1.stop(); + sentinel2.stop(); + sentinel3.stop(); + master.stop(); + slave1.stop(); + slave2.stop(); + + redisson.shutdown(); + } + + @Test + public void testMasters() { + Collection masters = connection.masters(); + assertThat(masters).hasSize(1); + } + + @Test + public void testSlaves() { + Collection masters = connection.masters(); + Collection slaves = connection.replicas(masters.iterator().next()); + assertThat(slaves).hasSize(2); + } + + @Test + public void testRemove() { + Collection masters = connection.masters(); + connection.remove(masters.iterator().next()); + } + + @Test + public void testMonitor() { + Collection masters = connection.masters(); + RedisServer master = masters.iterator().next(); + master.setName(master.getName() + ":"); + connection.monitor(master); + } + + @Test + public void testFailover() throws InterruptedException { + Collection masters = connection.masters(); + connection.failover(masters.iterator().next()); + + Thread.sleep(10000); + + RedisServer newMaster = connection.masters().iterator().next(); + assertThat(masters.iterator().next().getPort()).isNotEqualTo(newMaster.getPort()); + } +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonStreamTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonStreamTest.java new file mode 100644 index 000000000..dce81f229 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonStreamTest.java @@ -0,0 +1,76 @@ +package org.redisson.spring.data.connection; + +import org.junit.Test; +import org.springframework.data.redis.connection.stream.*; + +import java.util.Collections; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Nikita Koksharov + */ +public class RedissonStreamTest extends BaseConnectionTest { + + @Test + public void testPending() { + connection.streamCommands().xGroupCreate("test".getBytes(), "testGroup", ReadOffset.latest(), true); + + PendingMessages p = connection.streamCommands().xPending("test".getBytes(), Consumer.from("testGroup", "test1")); + assertThat(p.size()).isEqualTo(0); + + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("1".getBytes(), "1".getBytes())); + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("2".getBytes(), "2".getBytes())); + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("3".getBytes(), "3".getBytes())); + + List l = connection.streamCommands().xReadGroup(Consumer.from("testGroup", "test1"), StreamOffset.create("test".getBytes(), ReadOffset.from(">"))); + assertThat(l.size()).isEqualTo(3); + + PendingMessages p2 = connection.streamCommands().xPending("test".getBytes(), Consumer.from("testGroup", "test1")); + assertThat(p2.size()).isEqualTo(3); + } + + @Test + public void testGroups() { + connection.streamCommands().xGroupCreate("test".getBytes(), "testGroup", ReadOffset.latest(), true); + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("1".getBytes(), "1".getBytes())); + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("2".getBytes(), "2".getBytes())); + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("3".getBytes(), "3".getBytes())); + + StreamInfo.XInfoGroups groups = connection.streamCommands().xInfoGroups("test".getBytes()); + assertThat(groups.size()).isEqualTo(1); + assertThat(groups.get(0).groupName()).isEqualTo("testGroup"); + assertThat(groups.get(0).pendingCount()).isEqualTo(0); + assertThat(groups.get(0).consumerCount()).isEqualTo(0); + assertThat(groups.get(0).lastDeliveredId()).isEqualTo("0-0"); + } + + @Test + public void testConsumers() { + connection.streamCommands().xGroupCreate("test".getBytes(), "testGroup", ReadOffset.latest(), true); + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("1".getBytes(), "1".getBytes())); + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("2".getBytes(), "2".getBytes())); + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("3".getBytes(), "3".getBytes())); + + connection.streamCommands().xGroupCreate("test".getBytes(), "testGroup2", ReadOffset.latest(), true); + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("1".getBytes(), "1".getBytes())); + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("2".getBytes(), "2".getBytes())); + connection.streamCommands().xAdd("test".getBytes(), Collections.singletonMap("3".getBytes(), "3".getBytes())); + + List list = connection.streamCommands().xReadGroup(Consumer.from("testGroup", "consumer1"), + StreamOffset.create("test".getBytes(), ReadOffset.lastConsumed())); + assertThat(list.size()).isEqualTo(6); + + StreamInfo.XInfoStream info = connection.streamCommands().xInfo("test".getBytes()); + assertThat(info.streamLength()).isEqualTo(6); + + StreamInfo.XInfoConsumers s1 = connection.streamCommands().xInfoConsumers("test".getBytes(), "testGroup"); + assertThat(s1.getConsumerCount()).isEqualTo(1); + assertThat(s1.get(0).consumerName()).isEqualTo("consumer1"); + assertThat(s1.get(0).pendingCount()).isEqualTo(6); + assertThat(s1.get(0).idleTimeMs()).isLessThan(100L); + assertThat(s1.get(0).groupName()).isEqualTo("testGroup"); + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSubscribeReactiveTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSubscribeReactiveTest.java new file mode 100644 index 000000000..90319d433 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSubscribeReactiveTest.java @@ -0,0 +1,113 @@ +package org.redisson.spring.data.connection; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import org.awaitility.Awaitility; +import org.awaitility.Duration; +import org.junit.Test; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.ReactiveSubscription; + +import org.springframework.data.redis.core.ReactiveStringRedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RedissonSubscribeReactiveTest extends BaseConnectionTest { + + @Test + public void testPubSub() { + RedissonConnectionFactory factory = new RedissonConnectionFactory(redisson); + AtomicLong counter = new AtomicLong(); + + ReactiveStringRedisTemplate template = new ReactiveStringRedisTemplate(factory); + template.listenTo(ChannelTopic.of("test")).flatMap(message -> { + counter.incrementAndGet(); + return Mono.empty(); + }).subscribe(); + + for (int i = 0; i < 40; i++) { + ReactiveRedisConnection connection = factory.getReactiveConnection(); + connection.pubSubCommands().publish(ByteBuffer.wrap("test".getBytes()), ByteBuffer.wrap("msg".getBytes())).block(); + } + + Awaitility.await().atMost(Duration.ONE_SECOND).untilAsserted(() -> { + assertThat(counter.get()).isEqualTo(40); + }); + } + + @Test + public void testTemplate() { + RedissonConnectionFactory factory = new RedissonConnectionFactory(redisson); + AtomicLong counter = new AtomicLong(); + + ReactiveStringRedisTemplate template = new ReactiveStringRedisTemplate(factory); + template.listenTo(ChannelTopic.of("test")).flatMap(message -> { + counter.incrementAndGet(); + return Mono.empty(); + }).subscribe(); + + template.listenTo(ChannelTopic.of("test2")).flatMap(message -> { + counter.incrementAndGet(); + return Mono.empty(); + }).subscribe(); + + ReactiveRedisConnection connection = factory.getReactiveConnection(); + connection.pubSubCommands().publish(ByteBuffer.wrap("test".getBytes()), ByteBuffer.wrap("msg".getBytes())).block(); + + Awaitility.await().atMost(Duration.ONE_SECOND) + .until(() -> counter.get() == 1); + } + + @Test + public void testSubscribe() { + RedissonConnectionFactory factory = new RedissonConnectionFactory(redisson); + ReactiveRedisConnection connection = factory.getReactiveConnection(); + Mono s = connection.pubSubCommands().createSubscription(); + AtomicReference msg = new AtomicReference(); + ReactiveSubscription ss = s.block(); + + ss.subscribe(ByteBuffer.wrap("test".getBytes())).block(); + ss.receive().doOnEach(message -> { + msg.set(message.get().getMessage().array()); + }).subscribe(); + + connection.pubSubCommands().publish(ByteBuffer.wrap("test".getBytes()), ByteBuffer.wrap("msg".getBytes())).block(); + + Awaitility.await().atMost(Duration.ONE_SECOND) + .until(() -> Arrays.equals("msg".getBytes(), msg.get())); + + ss.unsubscribe(); + + connection.pubSubCommands().publish(ByteBuffer.wrap("test".getBytes()), ByteBuffer.wrap("msg".getBytes())).block(); + } + + @Test + public void testUnSubscribe() { + RedissonConnectionFactory factory = new RedissonConnectionFactory(redisson); + ReactiveRedisConnection connection = factory.getReactiveConnection(); + Mono s = connection.pubSubCommands().createSubscription(); + AtomicReference msg = new AtomicReference(); + ReactiveSubscription ss = s.block(); + + ss.subscribe(ByteBuffer.wrap("test".getBytes())).block(); + ss.receive().doOnEach(message -> { + msg.set(message.get().getMessage().array()); + }).subscribe(); + + connection.pubSubCommands().publish(ByteBuffer.wrap("test".getBytes()), ByteBuffer.wrap("msg".getBytes())).block(); + Awaitility.await().atMost(Duration.ONE_SECOND) + .until(() -> Arrays.equals("msg".getBytes(), msg.get())); + + ss.unsubscribe(); + + connection.pubSubCommands().publish(ByteBuffer.wrap("test".getBytes()), ByteBuffer.wrap("msg".getBytes())).block(); + + + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSubscribeTest.java b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSubscribeTest.java new file mode 100644 index 000000000..6cfdcc19d --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/java/org/redisson/spring/data/connection/RedissonSubscribeTest.java @@ -0,0 +1,144 @@ +package org.redisson.spring.data.connection; + +import org.awaitility.Awaitility; +import org.awaitility.Duration; +import org.junit.Test; +import org.redisson.RedisRunner; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.PatternTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RedissonSubscribeTest extends BaseConnectionTest { + + @Test + public void testListenersDuplication() { + Queue msg = new ConcurrentLinkedQueue<>(); + MessageListener aListener = (message, pattern) -> { + msg.add(message.getBody()); + }; + + RedissonConnectionFactory factory = new RedissonConnectionFactory(redisson); + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(factory); + container.addMessageListener(aListener, + Arrays.asList(new ChannelTopic("a"), new ChannelTopic("b"))); + container.addMessageListener(aListener, + Arrays.asList(new PatternTopic("c*"))); + container.afterPropertiesSet(); + container.start(); + + RedisConnection c = factory.getConnection(); + c.publish("a".getBytes(), "msg".getBytes()); + + Awaitility.await().atMost(Duration.ONE_SECOND) + .untilAsserted(() -> { + assertThat(msg).containsExactly("msg".getBytes()); + }); + } + + @Test + public void testPatterTopic() throws IOException, InterruptedException { + RedisRunner.RedisProcess instance = new RedisRunner() + .nosave() + .randomPort() + .randomDir() + .notifyKeyspaceEvents( + RedisRunner.KEYSPACE_EVENTS_OPTIONS.K, + RedisRunner.KEYSPACE_EVENTS_OPTIONS.g, + RedisRunner.KEYSPACE_EVENTS_OPTIONS.E, + RedisRunner.KEYSPACE_EVENTS_OPTIONS.$) + .run(); + + Config config = new Config(); + config.useSingleServer().setAddress(instance.getRedisServerAddressAndPort()).setPingConnectionInterval(0); + RedissonClient redisson = Redisson.create(config); + + RedissonConnectionFactory factory = new RedissonConnectionFactory(redisson); + + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(factory); + AtomicInteger counterTest = new AtomicInteger(); + container.addMessageListener(new MessageListener() { + @Override + public void onMessage(Message message, byte[] pattern) { + counterTest.incrementAndGet(); + } + }, new PatternTopic("__keyspace@0__:mykey")); + container.addMessageListener(new MessageListener() { + @Override + public void onMessage(Message message, byte[] pattern) { + counterTest.incrementAndGet(); + } + }, new PatternTopic("__keyevent@0__:del")); + container.afterPropertiesSet(); + container.start(); + assertThat(container.isRunning()).isTrue(); + + RedisConnection c = factory.getConnection(); + c.set("mykey".getBytes(), "2".getBytes()); + c.del("mykey".getBytes()); + + Awaitility.await().atMost(Duration.FIVE_SECONDS).until(() -> { + return counterTest.get() == 3; + }); + + container.stop(); + redisson.shutdown(); + } + + @Test + public void testSubscribe() { + RedissonConnection connection = new RedissonConnection(redisson); + AtomicReference msg = new AtomicReference(); + connection.subscribe(new MessageListener() { + @Override + public void onMessage(Message message, byte[] pattern) { + msg.set(message.getBody()); + } + }, "test".getBytes()); + + connection.publish("test".getBytes(), "msg".getBytes()); + Awaitility.await().atMost(Duration.ONE_SECOND) + .until(() -> Arrays.equals("msg".getBytes(), msg.get())); + + connection.getSubscription().unsubscribe(); + + connection.publish("test".getBytes(), "msg".getBytes()); + } + + @Test + public void testUnSubscribe() { + RedissonConnection connection = new RedissonConnection(redisson); + AtomicReference msg = new AtomicReference(); + connection.subscribe(new MessageListener() { + @Override + public void onMessage(Message message, byte[] pattern) { + msg.set(message.getBody()); + } + }, "test".getBytes()); + + connection.publish("test".getBytes(), "msg".getBytes()); + Awaitility.await().atMost(Duration.ONE_SECOND) + .until(() -> Arrays.equals("msg".getBytes(), msg.get())); + + connection.getSubscription().unsubscribe(); + + + } + +} diff --git a/redisson-spring-data/redisson-spring-data-30/src/test/resources/logback.xml b/redisson-spring-data/redisson-spring-data-30/src/test/resources/logback.xml new file mode 100644 index 000000000..177f314b4 --- /dev/null +++ b/redisson-spring-data/redisson-spring-data-30/src/test/resources/logback.xml @@ -0,0 +1,36 @@ + + + + + + + %d{yyyy.MM.dd HH:mm:ss.SSS} %-5level %c{0} : %msg%n + + + + + + + + + + + + +