GEODE-8865: Create additional dunit and integration tests for Redis HMGET (#5945)
diff --git a/geode-docs/tools_modules/redis_api_for_geode.html.md.erb b/geode-docs/tools_modules/redis_api_for_geode.html.md.erb
index 2887109..c4327a4 100644
--- a/geode-docs/tools_modules/redis_api_for_geode.html.md.erb
+++ b/geode-docs/tools_modules/redis_api_for_geode.html.md.erb
@@ -68,7 +68,7 @@
**Note**: These commands are supported for Redis 5.
- **Connection**: AUTH, PING, QUIT
-- **Hashes**: HGETALL, HMSET, HSET, HVALS
+- **Hashes**: HGETALL, HMGET, HMSET, HSET, HVALS
- **Keys**: DEL, EXISTS, EXPIRE, EXPIREAT, KEYS, PERSIST, PEXPIRE, PEXPIREAT, PTTL, RENAME, TTL,
TYPE
- **Publish/Subscribe**: PUBLISH, PSUBSCRIBE, PUNSUBSCRIBE, SUBSCRIBE, UNSUBSCRIBE
@@ -80,7 +80,7 @@
exactly as expected.
- **Connection**: ECHO, SELECT
-- **Hashes**: HDEL, HEXISTS, HGET, HINCRBY, HINCRBYFLOAT, HKEYS, HLEN, HMGET, HSCAN, HSETNX,
+- **Hashes**: HDEL, HEXISTS, HGET, HINCRBY, HINCRBYFLOAT, HKEYS, HLEN, HSCAN, HSETNX,
HSTRLEN
- **Keys**: SCAN, UNLINK
- **Server**: DBSIZE, FLUSHALL (no async option), FLUSHDB (no async option), INFO, SHUTDOWN,
diff --git a/geode-redis/README.md b/geode-redis/README.md
index 72d4a36..f690001 100644
--- a/geode-redis/README.md
+++ b/geode-redis/README.md
@@ -165,52 +165,52 @@
| EXPIREAT | DECRBY | ACL LIST |
| GET | ECHO | ACL LOAD |
| HGETALL | FLUSHALL | ACL LOG |
-| HMSET | FLUSHDB | ACL SAVE |
-| HSET | GETBIT | ACL SETUSER |
-| HVALS | GETRANGE | ACL USERS |
-| KEYS | GETSET | ACL WHOAMI |
-| PERSIST | HDEL | BGREWRITEAOF |
-| PEXPIRE | HEXISTS | BGSAVE |
-| PEXPIREAT | HGET | BITFIELD |
-| PING | HINCRBY | BLPOP |
-| PSUBSCRIBE | HINCRBYFLOAT | BRPOP |
-| PTTL | HKEYS | BRPOPLPUSH |
-| PUBLISH | HLEN | BZPOPMAX |
-| PUNSUBSCRIBE | HMGET | BZPOPMIN |
-| QUIT | HSCAN | CLIENT CACHING |
-| RENAME | HSETNX | CLIENT GETNAME |
-| SADD | HSTRLEN | CLIENT GETREDIR |
-| SET | INCR | CLIENT ID |
-| SMEMBERS | INCRBY | CLIENT KILL |
-| SREM | INCRBYFLOAT | CLIENT LIST |
-| SUBSCRIBE | INFO | CLIENT PAUSE |
-| TTL | MGET | CLIENT REPLY |
-| TYPE | MSET | CLIENT SETNAME |
-| UNSUBSCRIBE | MSETNX | CLIENT TRACKING |
-| | PSETEX | CLIENT UNBLOCK |
-| | SCAN | CLUSTER ADDSLOTS |
-| | SCARD | CLUSTER BUMPEPOCH |
-| | SDIFF | CLUSTER COUNT-FAILURE-REPORTS |
-| | SDIFFSTORE | CLUSTER COUNTKEYSINSLOT |
-| | SELECT | CLUSTER DELSLOTS |
-| | SETBIT | CLUSTER FAILOVER |
-| | SETEX | CLUSTER FLUSHSLOTS |
-| | SETNX | CLUSTER FORGET |
-| | SETRANGE | CLUSTER GETKEYSINSLOT |
-| | SHUTDOWN | CLUSTER INFO |
-| | SINTER | CLUSTER KEYSLOT |
-| | SINTERSTORE | CLUSTER MEET |
-| | SISMEMBER | CLUSTER MYID |
-| | SLOWLOG | CLUSTER NODES |
-| | SMOVE | CLUSTER REPLICAS |
-| | SPOP | CLUSTER REPLICATE |
-| | SRANDMEMBER | CLUSTER RESET |
-| | SSCAN | CLUSTER SAVECONFIG |
-| | STRLEN | CLUSTER SET-CONFIG-EPOCH |
-| | SUNION | CLUSTER SETSLOT |
-| | SUNIONSTORE | CLUSTER SLAVES |
-| | TIME | CLUSTER SLOTS |
-| | UNLINK [1] | COMMAND |
+| HMGET | FLUSHDB | ACL SAVE |
+| HMSET | GETBIT | ACL SETUSER |
+| HSET | GETRANGE | ACL USERS |
+| HVALS | GETSET | ACL WHOAMI |
+| KEYS | HDEL | BGREWRITEAOF |
+| PERSIST | HEXISTS | BGSAVE |
+| PEXPIRE | HGET | BITFIELD |
+| PEXPIREAT | HINCRBY | BLPOP |
+| PING | HINCRBYFLOAT | BRPOP |
+| PSUBSCRIBE | HKEYS | BRPOPLPUSH |
+| PTTL | HLEN | BZPOPMAX |
+| PUBLISH | HSCAN | BZPOPMIN |
+| PUNSUBSCRIBE | HSETNX | CLIENT CACHING |
+| QUIT | HSTRLEN | CLIENT GETNAME |
+| RENAME | INCR | CLIENT GETREDIR |
+| SADD | INCRBY | CLIENT ID |
+| SET | INCRBYFLOAT | CLIENT KILL |
+| SMEMBERS | INFO | CLIENT LIST |
+| SREM | MGET | CLIENT PAUSE |
+| SUBSCRIBE | MSET | CLIENT REPLY |
+| TTL | MSETNX | CLIENT SETNAME |
+| TYPE | PSETEX | CLIENT TRACKING |
+| UNSUBSCRIBE | SCAN | CLIENT UNBLOCK |
+| | SCARD | CLUSTER ADDSLOTS |
+| | SDIFF | CLUSTER BUMPEPOCH |
+| | SDIFFSTORE | CLUSTER COUNT-FAILURE-REPORTS |
+| | SELECT | CLUSTER COUNTKEYSINSLOT |
+| | SETBIT | CLUSTER DELSLOTS |
+| | SETEX | CLUSTER FAILOVER |
+| | SETNX | CLUSTER FLUSHSLOTS |
+| | SETRANGE | CLUSTER FORGET |
+| | SHUTDOWN | CLUSTER GETKEYSINSLOT |
+| | SINTER | CLUSTER INFO |
+| | SINTERSTORE | CLUSTER KEYSLOT |
+| | SISMEMBER | CLUSTER MEET |
+| | SLOWLOG | CLUSTER MYID |
+| | SMOVE | CLUSTER NODES |
+| | SPOP | CLUSTER REPLICAS |
+| | SRANDMEMBER | CLUSTER REPLICATE |
+| | SSCAN | CLUSTER RESET |
+| | STRLEN | CLUSTER SAVECONFIG |
+| | SUNION | CLUSTER SET-CONFIG-EPOCH |
+| | SUNIONSTORE | CLUSTER SETSLOT |
+| | TIME | CLUSTER SLAVES |
+| | UNLINK [1] | CLUSTER SLOTS |
+| | | COMMAND |
| | | COMMAND COUNT |
| | | COMMAND GETKEYS |
| | | COMMAND INFO |
diff --git a/geode-redis/src/distributedTest/java/org/apache/geode/redis/internal/executor/hash/HMgetDUnitTest.java b/geode-redis/src/distributedTest/java/org/apache/geode/redis/internal/executor/hash/HMgetDUnitTest.java
new file mode 100644
index 0000000..c2d7422
--- /dev/null
+++ b/geode-redis/src/distributedTest/java/org/apache/geode/redis/internal/executor/hash/HMgetDUnitTest.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You 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.apache.geode.redis.internal.executor.hash;
+
+import static org.apache.geode.distributed.ConfigurationProperties.MAX_WAIT_TIME_RECONNECT;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import redis.clients.jedis.Jedis;
+
+import org.apache.geode.redis.ConcurrentLoopingThreads;
+import org.apache.geode.test.awaitility.GeodeAwaitility;
+import org.apache.geode.test.dunit.rules.MemberVM;
+import org.apache.geode.test.dunit.rules.RedisClusterStartupRule;
+
+public class HMgetDUnitTest {
+
+ @ClassRule
+ public static RedisClusterStartupRule clusterStartUp = new RedisClusterStartupRule(4);
+
+ private static final String LOCAL_HOST = "127.0.0.1";
+ private static final int HASH_SIZE = 1000;
+ private static final int JEDIS_TIMEOUT =
+ Math.toIntExact(GeodeAwaitility.getTimeout().toMillis());
+ private static Jedis jedis1;
+ private static Jedis jedis2;
+ private static Jedis jedis3;
+
+ private static Properties locatorProperties;
+
+ private static MemberVM locator;
+ private static MemberVM server1;
+ private static MemberVM server2;
+
+ private static int redisServerPort1;
+ private static int redisServerPort2;
+
+ @BeforeClass
+ public static void classSetup() {
+ locatorProperties = new Properties();
+ locatorProperties.setProperty(MAX_WAIT_TIME_RECONNECT, "15000");
+
+ locator = clusterStartUp.startLocatorVM(0, locatorProperties);
+ server1 = clusterStartUp.startRedisVM(1, locator.getPort());
+ server2 = clusterStartUp.startRedisVM(2, locator.getPort());
+
+ redisServerPort1 = clusterStartUp.getRedisPort(1);
+ redisServerPort2 = clusterStartUp.getRedisPort(2);
+
+ jedis1 = new Jedis(LOCAL_HOST, redisServerPort1, JEDIS_TIMEOUT);
+ jedis2 = new Jedis(LOCAL_HOST, redisServerPort2, JEDIS_TIMEOUT);
+ jedis3 = new Jedis(LOCAL_HOST, redisServerPort1, JEDIS_TIMEOUT);
+ }
+
+ @Before
+ public void testSetup() {
+ jedis1.flushAll();
+ }
+
+ @AfterClass
+ public static void tearDown() {
+ jedis1.disconnect();
+ jedis2.disconnect();
+
+ server1.stop();
+ server2.stop();
+ }
+
+ @Test
+ public void testConcurrentHMget_whileUpdatingValues() {
+ String key = "key";
+
+ Map<String, String> testMap = makeHashMap(HASH_SIZE, "field-", "value-");
+
+ jedis1.hset(key, testMap);
+
+ new ConcurrentLoopingThreads(HASH_SIZE,
+ (i) -> jedis1.hset(key, "field-" + i, "value-" + i),
+ (i) -> assertThat(jedis2.hmget(key, "field-" + i)).isNotNull(),
+ (i) -> assertThat(jedis3.hmget(key, "field-" + i)).isNotNull());
+ }
+
+ private Map<String, String> makeHashMap(int hashSize, String baseFieldName,
+ String baseValueName) {
+ Map<String, String> map = new HashMap<>();
+ for (int i = 0; i < hashSize; i++) {
+ map.put(baseFieldName + i, baseValueName + i);
+ }
+ return map;
+ }
+}
diff --git a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/hash/AbstractHashesIntegrationTest.java b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/hash/AbstractHashesIntegrationTest.java
index 9454e1b..f0c3f2c 100755
--- a/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/hash/AbstractHashesIntegrationTest.java
+++ b/geode-redis/src/integrationTest/java/org/apache/geode/redis/internal/executor/hash/AbstractHashesIntegrationTest.java
@@ -115,6 +115,22 @@
}
@Test
+ public void testHMSetErrorMessage_givenIncorrectDataType() {
+ Map<String, String> animalMap = new HashMap<>();
+ animalMap.put("chicken", "eggs");
+
+ jedis.set("farm", "chicken");
+ assertThatThrownBy(() -> jedis.hmset("farm", animalMap))
+ .isInstanceOf(JedisDataException.class)
+ .hasMessageContaining("WRONGTYPE Operation against a key holding the wrong kind of value");
+
+ jedis.sadd("zoo", "elephant");
+ assertThatThrownBy(() -> jedis.hmset("zoo", animalMap))
+ .isInstanceOf(JedisDataException.class)
+ .hasMessageContaining("WRONGTYPE Operation against a key holding the wrong kind of value");
+ }
+
+ @Test
public void testHSet() {
String key = "key";
Map<String, String> hash = new HashMap<String, String>();
@@ -136,27 +152,26 @@
}
@Test
- public void testHMGetHDelHGetAllHVals() {
+ public void testHMGet_HDel_HGetAll_HVals() {
String key = "key";
- Map<String, String> hash = new HashMap<String, String>();
+ Map<String, String> hash = new HashMap<>();
for (int i = 0; i < 10; i++) {
hash.put("field_" + i, "member_" + i);
}
jedis.hmset(key, hash);
+
Set<String> keys = hash.keySet();
String[] keyArray = keys.toArray(new String[keys.size()]);
List<String> retList = jedis.hmget(key, keyArray);
- for (int i = 0; i < keys.size(); i++) {
- assertThat(hash.get(keyArray[i])).isEqualTo(retList.get(i));
- }
+ assertThat(retList).containsExactlyInAnyOrderElementsOf(hash.values());
Map<String, String> retMap = jedis.hgetAll(key);
assertThat(retMap).containsExactlyInAnyOrderEntriesOf(hash);
List<String> retVals = jedis.hvals(key);
- Set<String> retSet = new HashSet<String>(retVals);
+ Set<String> retSet = new HashSet<>(retVals);
assertThat(retSet.containsAll(hash.values())).isTrue();
@@ -165,6 +180,38 @@
}
@Test
+ public void testHMGet_returnNull_forUnknownFields() {
+ String key = "key";
+ jedis.hset(key, "rooster", "crows");
+ jedis.hset(key, "duck", "quacks");
+
+ List<String> result =
+ jedis.hmget(key, "unknown-1", "rooster", "unknown-2", "duck", "unknown-3");
+ assertThat(result).containsExactly(null, "crows", null, "quacks", null);
+ }
+
+ @Test
+ public void testHMGet_givenWrongNumberOfArguments() {
+ assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HMGET))
+ .hasMessage("ERR wrong number of arguments for 'hmget' command");
+ assertThatThrownBy(() -> jedis.sendCommand(Protocol.Command.HMGET, "1"))
+ .hasMessage("ERR wrong number of arguments for 'hmget' command");
+ }
+
+ @Test
+ public void testHMGetErrorMessage_givenIncorrectDataType() {
+ jedis.set("farm", "chicken");
+ assertThatThrownBy(() -> jedis.hmget("farm", "chicken"))
+ .isInstanceOf(JedisDataException.class)
+ .hasMessage("WRONGTYPE " + RedisConstants.ERROR_WRONG_TYPE);
+
+ jedis.sadd("zoo", "elephant");
+ assertThatThrownBy(() -> jedis.hmget("zoo", "chicken"))
+ .isInstanceOf(JedisDataException.class)
+ .hasMessage("WRONGTYPE " + RedisConstants.ERROR_WRONG_TYPE);
+ }
+
+ @Test
public void testHDelErrorMessage_givenIncorrectDataType() {
jedis.set("farm", "chicken");
assertThatThrownBy(() -> {
diff --git a/geode-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java b/geode-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
index 4efdb0c..97d90a1 100755
--- a/geode-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
+++ b/geode-redis/src/main/java/org/apache/geode/redis/internal/RedisCommandType.java
@@ -156,6 +156,7 @@
/************* Hashes *****************/
HGETALL(new HGetAllExecutor(), SUPPORTED, new ExactParameterRequirements(2)),
+ HMGET(new HMGetExecutor(), SUPPORTED, new MinimumParameterRequirements(3)),
HMSET(new HMSetExecutor(), SUPPORTED,
new MinimumParameterRequirements(4).and(new EvenParameterRequirements())),
HSET(new HSetExecutor(), SUPPORTED,
@@ -238,7 +239,6 @@
HINCRBYFLOAT(new HIncrByFloatExecutor(), UNSUPPORTED, new ExactParameterRequirements(4)),
HKEYS(new HKeysExecutor(), UNSUPPORTED, new ExactParameterRequirements(2)),
HLEN(new HLenExecutor(), UNSUPPORTED, new ExactParameterRequirements(2)),
- HMGET(new HMGetExecutor(), UNSUPPORTED, new MinimumParameterRequirements(3)),
HSCAN(new HScanExecutor(), UNSUPPORTED, new MinimumParameterRequirements(3),
new OddParameterRequirements(ERROR_SYNTAX)),
HSETNX(new HSetNXExecutor(), UNSUPPORTED, new ExactParameterRequirements(4)),
diff --git a/geode-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java b/geode-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java
index e0b05be..52deaff 100644
--- a/geode-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java
+++ b/geode-redis/src/test/java/org/apache/geode/redis/internal/SupportedCommandsJUnitTest.java
@@ -38,6 +38,7 @@
"EXPIREAT",
"GET",
"HGETALL",
+ "HMGET",
"HMSET",
"HSET",
"HVALS",
@@ -82,7 +83,6 @@
"HINCRBYFLOAT",
"HKEYS",
"HLEN",
- "HMGET",
"HSCAN",
"HSETNX",
"HSTRLEN",