This is an automated email from the ASF dual-hosted git repository.

ringles pushed a commit to branch 
GEODE-9892-Create-Infrastructure-for-Redis-Lists
in repository https://gitbox.apache.org/repos/asf/geode.git


The following commit(s) were added to 
refs/heads/GEODE-9892-Create-Infrastructure-for-Redis-Lists by this push:
     new 85f8cf5  LPUSH, LPOP, LLEN implemented but not storing in region 
properly
85f8cf5 is described below

commit 85f8cf5d7f08fccc0b4e83e61ee9307203a5a3fd
Author: Ray Ingles <[email protected]>
AuthorDate: Mon Jan 10 10:16:24 2022 -0500

    LPUSH, LPOP, LLEN implemented but not storing in region properly
---
 .../list/AbstractListsIntegrationTest.java         |  44 +-
 .../redis/internal/commands/RedisCommandType.java  |   5 +-
 .../list/{LPopExecutor.java => LLenExecutor.java}  |  42 +-
 .../commands/executor/list/LPopExecutor.java       |  30 +-
 .../internal/commands/parameters/Parameter.java    |   6 +-
 .../geode/redis/internal/data/NullRedisList.java   |   7 +-
 .../geode/redis/internal/data/RedisList.java       |  26 +-
 .../geode/redis/internal/data/RedisListTest.java   | 499 +++++++++++++++++++++
 8 files changed, 553 insertions(+), 106 deletions(-)

diff --git 
a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
 
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
index 04e1244..5282340 100755
--- 
a/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
+++ 
b/geode-for-redis/src/integrationTest/java/org/apache/geode/redis/internal/commands/executor/list/AbstractListsIntegrationTest.java
@@ -15,11 +15,10 @@
 package org.apache.geode.redis.internal.commands.executor.list;
 
 import static 
org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertAtLeastNArgs;
+import static 
org.apache.geode.redis.RedisCommandArgumentsTestHelper.assertExactNumberOfArgs;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
-import java.util.List;
-
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -54,7 +53,7 @@ public abstract class AbstractListsIntegrationTest implements 
RedisIntegrationTe
   }
 
   @Test
-  public void testLPush_withExistingKey_ofWrongType_shouldReturnError() {
+  public void lpushErrors_withExistingKey_ofWrongType() {
     String key = "key";
     String stringValue = "preexistingValue";
     String[] setValue = new String[1];
@@ -66,7 +65,7 @@ public abstract class AbstractListsIntegrationTest implements 
RedisIntegrationTe
   }
 
   @Test
-  public void 
testLPush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
+  public void 
lpush_withExistingKey_ofWrongType_shouldNotOverWriteExistingKey() {
     String key = "key";
     String stringValue = "preexistingValue";
     String[] setValue = new String[1];
@@ -82,7 +81,7 @@ public abstract class AbstractListsIntegrationTest implements 
RedisIntegrationTe
   }
 
   @Test
-  public void testLPush_canStoreBinaryData() {
+  public void lpush_canStoreBinaryData() {
     byte[] blob = new byte[256];
     for (int i = 0; i < 256; i++) {
       blob[i] = (byte) i;
@@ -108,11 +107,6 @@ public abstract class AbstractListsIntegrationTest 
implements RedisIntegrationTe
   }
 
   @Test
-  public void lpopCount_withNonExistentKey_returnsNull() {
-    assertThat(jedis.lpop("non existent", 3)).isNull();
-  }
-
-  @Test
   public void lpop_returnsOneMember() {
     jedis.lpush("key", "m1", "m2");
     String result = jedis.lpop("key");
@@ -120,17 +114,31 @@ public abstract class AbstractListsIntegrationTest 
implements RedisIntegrationTe
   }
 
   @Test
-  public void lpopCount_returnsTwoUniqueMembers() {
-    jedis.lpush("key", "m1", "m2", "m3");
-    List<String> results = jedis.lpop("key", 2);
-    assertThat(results).hasSize(2);
-    assertThat(results).containsAnyOf("m1", "m2", "m3");
-    assertThat(results.get(0)).isNotEqualTo(results.get(1));
+  public void llen_givenWrongNumOfArgs_returnsError() {
+    assertExactNumberOfArgs(jedis, Protocol.Command.LLEN, 1);
   }
 
   @Test
-  public void lpopNegativeCount_returnsError() {
+  public void llen_givenNonexistentList_returnsZero() {
+    assertThat(jedis.llen("nonexistent")).isEqualTo(0L);
+  }
+
+  @Test
+  public void llen_returnsListLength() {
     jedis.lpush("key", "m1", "m2", "m3");
-    assertThatThrownBy(() -> jedis.lpop("key", 
-3)).hasMessageContaining("value is out of range, must be positive");
+    assertThat(jedis.llen("key")).isEqualTo(3L);
+
+    String result = jedis.lpop("key");
+    assertThat(result).isEqualTo("m3");
+    assertThat(jedis.llen("key")).isEqualTo(2L);
+
+    result = jedis.lpop("key");
+    assertThat(result).isEqualTo("m2");
+    assertThat(jedis.llen("key")).isEqualTo(1L);
+
+    result = jedis.lpop("key");
+    assertThat(result).isEqualTo("m1");
+    assertThat(jedis.llen("key")).isEqualTo(0L);
   }
+
 }
diff --git 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
index b2cd57e..0c14663 100755
--- 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
+++ 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/RedisCommandType.java
@@ -76,6 +76,7 @@ import 
org.apache.geode.redis.internal.commands.executor.key.RestoreExecutor;
 import org.apache.geode.redis.internal.commands.executor.key.ScanExecutor;
 import org.apache.geode.redis.internal.commands.executor.key.TTLExecutor;
 import org.apache.geode.redis.internal.commands.executor.key.TypeExecutor;
+import org.apache.geode.redis.internal.commands.executor.list.LLenExecutor;
 import org.apache.geode.redis.internal.commands.executor.list.LPopExecutor;
 import org.apache.geode.redis.internal.commands.executor.list.LPushExecutor;
 import 
org.apache.geode.redis.internal.commands.executor.pubsub.PsubscribeExecutor;
@@ -289,9 +290,9 @@ public enum RedisCommandType {
 
   /************** Lists *****************/
 
+  LLEN(new LLenExecutor(), SUPPORTED, new Parameter().exact(2).flags(READONLY, 
FAST)),
+  LPOP(new LPopExecutor(), SUPPORTED, new Parameter().exact(2).flags(WRITE, 
FAST)),
   LPUSH(new LPushExecutor(), SUPPORTED, new Parameter().min(3).flags(WRITE, 
DENYOOM, FAST)),
-  LPOP(new LPopExecutor(), SUPPORTED,
-      new Parameter().min(2).max(3, ERROR_SYNTAX).flags(WRITE, FAST)),
 
   /************* Server *****************/
   COMMAND(new COMMANDCommandExecutor(), SUPPORTED, new 
Parameter().min(1).firstKey(0).flags(RANDOM,
diff --git 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LLenExecutor.java
similarity index 54%
copy from 
geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
copy to 
geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LLenExecutor.java
index 447ae32..810ea3b 100755
--- 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
+++ 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LLenExecutor.java
@@ -15,56 +15,26 @@
 package org.apache.geode.redis.internal.commands.executor.list;
 
 
-import static 
org.apache.geode.redis.internal.RedisConstants.ERROR_VALUE_MUST_BE_POSITIVE;
-import static org.apache.geode.redis.internal.netty.Coder.bytesToLong;
-import static org.apache.geode.redis.internal.netty.Coder.narrowLongToInt;
-
-import java.util.Collection;
-import java.util.List;
-
 import org.apache.geode.cache.Region;
 import org.apache.geode.redis.internal.commands.Command;
 import org.apache.geode.redis.internal.commands.executor.CommandExecutor;
 import org.apache.geode.redis.internal.commands.executor.RedisResponse;
 import org.apache.geode.redis.internal.data.RedisData;
 import org.apache.geode.redis.internal.data.RedisKey;
+import org.apache.geode.redis.internal.data.RedisList;
+import org.apache.geode.redis.internal.data.RedisSet;
 import org.apache.geode.redis.internal.netty.ExecutionHandlerContext;
 
-public class LPopExecutor implements CommandExecutor {
+public class LLenExecutor implements CommandExecutor {
 
   @Override
   public RedisResponse executeCommand(Command command, ExecutionHandlerContext 
context) {
-    List<byte[]> commandElems = command.getProcessedCommand();
-    boolean isCountPassed = false;
-    int popCount;
-
-    if (commandElems.size() == 3) {
-      isCountPassed = true;
-      try {
-        popCount = narrowLongToInt(bytesToLong(commandElems.get(2)));
-      } catch (NumberFormatException nex) {
-        return RedisResponse.error(ERROR_VALUE_MUST_BE_POSITIVE);
-      }
-      if (popCount < 0) {
-        return RedisResponse.error(ERROR_VALUE_MUST_BE_POSITIVE);
-      }
-    } else {
-      popCount = 1;
-    }
-
     Region<RedisKey, RedisData> region = context.getRegion();
     RedisKey key = command.getKey();
-    Collection<byte[]> popped = context.listLockedExecute(key, false,
-        list -> list.lpop(region, key, popCount));
-
-    if (popped.isEmpty() && !isCountPassed) {
-      return RedisResponse.nil();
-    }
 
-    if (!isCountPassed) {
-      return RedisResponse.bulkString(popped.iterator().next());
-    }
+    int result = context.listLockedExecute(key, false,
+        list -> list.llen(region, key));
 
-    return RedisResponse.array(popped, true);
+    return RedisResponse.integer(result);
   }
 }
diff --git 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
index 447ae32..b06c000 100755
--- 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
+++ 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/executor/list/LPopExecutor.java
@@ -34,37 +34,15 @@ public class LPopExecutor implements CommandExecutor {
 
   @Override
   public RedisResponse executeCommand(Command command, ExecutionHandlerContext 
context) {
-    List<byte[]> commandElems = command.getProcessedCommand();
-    boolean isCountPassed = false;
-    int popCount;
-
-    if (commandElems.size() == 3) {
-      isCountPassed = true;
-      try {
-        popCount = narrowLongToInt(bytesToLong(commandElems.get(2)));
-      } catch (NumberFormatException nex) {
-        return RedisResponse.error(ERROR_VALUE_MUST_BE_POSITIVE);
-      }
-      if (popCount < 0) {
-        return RedisResponse.error(ERROR_VALUE_MUST_BE_POSITIVE);
-      }
-    } else {
-      popCount = 1;
-    }
-
     Region<RedisKey, RedisData> region = context.getRegion();
     RedisKey key = command.getKey();
-    Collection<byte[]> popped = context.listLockedExecute(key, false,
-        list -> list.lpop(region, key, popCount));
+    byte[] popped = context.listLockedExecute(key, false,
+        list -> list.lpop(region, key));
 
-    if (popped.isEmpty() && !isCountPassed) {
+    if (popped == null) {
       return RedisResponse.nil();
     }
 
-    if (!isCountPassed) {
-      return RedisResponse.bulkString(popped.iterator().next());
-    }
-
-    return RedisResponse.array(popped, true);
+    return RedisResponse.bulkString(popped);
   }
 }
diff --git 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/parameters/Parameter.java
 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/parameters/Parameter.java
index dc71b8a..84f2832 100644
--- 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/parameters/Parameter.java
+++ 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/commands/parameters/Parameter.java
@@ -73,10 +73,10 @@ public class Parameter {
     return this;
   }
 
-  public Parameter min(int minumum) {
-    arity = -minumum;
+  public Parameter min(int minimum) {
+    arity = -minimum;
     predicates.add(c -> {
-      if (c.getProcessedCommand().size() < minumum) {
+      if (c.getProcessedCommand().size() < minimum) {
         throw new 
RedisParametersMismatchException(c.wrongNumberOfArgumentsErrorMessage());
       }
     });
diff --git 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
index 306b019..699e0e0 100644
--- 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
+++ 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/NullRedisList.java
@@ -16,10 +16,7 @@
 
 package org.apache.geode.redis.internal.data;
 
-import static java.util.Collections.emptyList;
-
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.List;
 
 import org.apache.geode.cache.Region;
@@ -42,12 +39,12 @@ class NullRedisList extends RedisList {
   }
 
   @Override
-  public Collection<byte[]> lpop(Region<RedisKey, RedisData> region, RedisKey 
key, int popCount) {
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
     return null;
   }
 
   @Override
-  public int llen() {
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
     return 0;
   }
 }
diff --git 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
index ae05e1e..5da9685 100644
--- 
a/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
+++ 
b/geode-for-redis/src/main/java/org/apache/geode/redis/internal/data/RedisList.java
@@ -67,32 +67,26 @@ public class RedisList extends AbstractRedisData {
     if (elementsAdded != 0) {
       storeChanges(region, key, new AddByteArrays(elementsToAdd));
     }
-    return elements.size();
+    return elementsAdded;
   }
 
   /**
    * @param region the region this instance is stored in
    * @param key the name of the set to add to
    * @param popCount the number of elements to remove and return
-   * @return the element(s) actually popped
+   * @return the element actually popped
    */
-  public Collection<byte[]> lpop(Region<RedisKey, RedisData> region, RedisKey 
key, int popCount) {
+  public byte[] lpop(Region<RedisKey, RedisData> region, RedisKey key) {
     int originalSize = elements.size();
     if (originalSize == 0) {
-      return emptyList();
-    }
-
-    if (popCount >= originalSize) {
-      region.remove(key, this);
-      return this.elements;
+      return null;
     }
 
-    List<byte[]> popped = new LinkedList<>();
-    while (popped.size() < popCount) {
-      popped.add(elements.remove(0));
-    }
-    if (!popped.isEmpty()) {
-      storeChanges(region, key, new RemoveByteArrays(popped));
+    byte[] popped = elements.remove(0);
+    if (popped != null) {
+      RemoveByteArrays removed = new RemoveByteArrays();
+      removed.add(popped);
+      storeChanges(region, key, removed);
     }
     return popped;
   }
@@ -100,7 +94,7 @@ public class RedisList extends AbstractRedisData {
   /**
    * @return the number of elements in the list
    */
-  public int llen() {
+  public int llen(Region<RedisKey, RedisData> region, RedisKey key) {
     return elements.size();
   }
 
diff --git 
a/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
 
b/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
new file mode 100644
index 0000000..b0c89a8
--- /dev/null
+++ 
b/geode-for-redis/src/test/java/org/apache/geode/redis/internal/data/RedisListTest.java
@@ -0,0 +1,499 @@
+/*
+ * 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.data;
+
+import static 
org.apache.geode.redis.internal.data.AbstractRedisData.NO_EXPIRATION;
+import static 
org.apache.geode.redis.internal.data.NullRedisDataStructures.NULL_REDIS_SET;
+import static org.apache.geode.redis.internal.data.RedisDataType.REDIS_SET;
+import static org.apache.geode.redis.internal.data.RedisSet.setOpStoreResult;
+import static org.apache.geode.redis.internal.netty.Coder.stringToBytes;
+import static org.apache.geode.util.internal.UncheckedUtils.uncheckedCast;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.io.DataOutput;
+import java.io.IOException;
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import it.unimi.dsi.fastutil.bytes.ByteArrays;
+import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+
+import org.apache.geode.DataSerializer;
+import org.apache.geode.cache.Region;
+import org.apache.geode.internal.HeapDataOutputStream;
+import org.apache.geode.internal.serialization.ByteArrayDataInput;
+import org.apache.geode.internal.serialization.SerializationContext;
+import org.apache.geode.internal.size.ReflectionObjectSizer;
+import org.apache.geode.redis.internal.netty.Coder;
+import org.apache.geode.redis.internal.services.RegionProvider;
+import org.apache.geode.test.junit.rules.ExecutorServiceRule;
+
+public class RedisListTest {
+
+  @ClassRule
+  public static final ExecutorServiceRule executor = new ExecutorServiceRule();
+
+  private final ReflectionObjectSizer sizer = 
ReflectionObjectSizer.getInstance();
+
+  @Test
+  public void confirmSerializationIsStable() throws IOException, 
ClassNotFoundException {
+    RedisSet set1 = createRedisSet(1, 2);
+    int expirationTimestamp = 1000;
+    set1.setExpirationTimestampNoDelta(expirationTimestamp);
+    HeapDataOutputStream out = new HeapDataOutputStream(100);
+    DataSerializer.writeObject(set1, out);
+    ByteArrayDataInput in = new ByteArrayDataInput(out.toByteArray());
+    RedisSet set2 = DataSerializer.readObject(in);
+    assertThat(set2).isEqualTo(set1);
+    assertThat(set2.getExpirationTimestamp())
+        .isEqualTo(set1.getExpirationTimestamp())
+        .isEqualTo(expirationTimestamp);
+  }
+
+  @Test
+  public void confirmToDataIsSynchronized() throws NoSuchMethodException {
+    assertThat(Modifier
+        .isSynchronized(RedisSet.class
+            .getMethod("toData", DataOutput.class, 
SerializationContext.class).getModifiers()))
+                .isTrue();
+  }
+
+  private RedisSet createRedisSet(int m1, int m2) {
+    return new RedisSet(Arrays.asList(new byte[] {(byte) m1}, new byte[] 
{(byte) m2}));
+  }
+
+  @Test
+  public void equals_returnsFalse_givenDifferentExpirationTimes() {
+    RedisSet set1 = createRedisSet(1, 2);
+    set1.setExpirationTimestampNoDelta(1000);
+    RedisSet set2 = createRedisSet(1, 2);
+    set2.setExpirationTimestampNoDelta(999);
+    assertThat(set1).isNotEqualTo(set2);
+  }
+
+  @Test
+  public void equals_returnsFalse_givenDifferentValueBytes() {
+    RedisSet set1 = createRedisSet(1, 2);
+    set1.setExpirationTimestampNoDelta(1000);
+    RedisSet set2 = createRedisSet(1, 3);
+    set2.setExpirationTimestampNoDelta(1000);
+    assertThat(set1).isNotEqualTo(set2);
+  }
+
+  @Test
+  public void equals_returnsTrue_givenEqualValueBytesAndExpiration() {
+    RedisSet set1 = createRedisSet(1, 2);
+    int expirationTimestamp = 1000;
+    set1.setExpirationTimestampNoDelta(expirationTimestamp);
+    RedisSet set2 = createRedisSet(1, 2);
+    set2.setExpirationTimestampNoDelta(expirationTimestamp);
+    assertThat(set1).isEqualTo(set2);
+    assertThat(set2.getExpirationTimestamp())
+        .isEqualTo(set1.getExpirationTimestamp())
+        .isEqualTo(expirationTimestamp);
+  }
+
+  @Test
+  public void equals_returnsTrue_givenDifferentEmptySets() {
+    RedisSet set1 = new RedisSet(Collections.emptyList());
+    RedisSet set2 = NULL_REDIS_SET;
+    assertThat(set1).isEqualTo(set2);
+    assertThat(set2).isEqualTo(set1);
+  }
+
+  @Test
+  public void sadd_stores_delta_that_is_stable() {
+    Region<RedisKey, RedisData> region = uncheckedCast(mock(Region.class));
+    when(region.put(any(), 
any())).thenAnswer(this::validateDeltaSerialization);
+
+    RedisSet set1 = createRedisSet(1, 2);
+    byte[] member3 = new byte[] {3};
+    ArrayList<byte[]> adds = new ArrayList<>();
+    adds.add(member3);
+
+    set1.sadd(adds, region, null);
+
+    verify(region).put(any(), any());
+    assertThat(set1.hasDelta()).isFalse();
+  }
+
+  @Test
+  public void srem_stores_delta_that_is_stable() {
+    Region<RedisKey, RedisData> region = uncheckedCast(mock(Region.class));
+    when(region.put(any(), 
any())).thenAnswer(this::validateDeltaSerialization);
+
+    RedisSet set1 = createRedisSet(1, 2);
+    byte[] member1 = new byte[] {1};
+    ArrayList<byte[]> removes = new ArrayList<>();
+    removes.add(member1);
+
+    set1.srem(removes, region, null);
+
+    verify(region).put(any(), any());
+    assertThat(set1.hasDelta()).isFalse();
+  }
+
+  @Test
+  public void sdiffstore_stores_delta_that_is_stable() {
+    RegionProvider regionProvider = uncheckedCast(mock(RegionProvider.class));
+    Region<RedisKey, RedisData> dataRegion = uncheckedCast(mock(Region.class));
+
+    RedisSet setDest = createRedisSet(1, 2);
+
+    when(regionProvider.getTypedRedisDataElseRemove(REDIS_SET, null, 
false)).thenReturn(setDest);
+    when(regionProvider.getDataRegion()).thenReturn(dataRegion);
+    when(dataRegion.put(any(), 
any())).thenAnswer(this::validateDeltaSerialization);
+
+    RedisSet.MemberSet set = new RedisSet.MemberSet();
+    set.add(new byte[] {3});
+    setOpStoreResult(regionProvider, null, set);
+
+    verify(dataRegion).put(any(), any());
+    assertThat(setDest.hasDelta()).isFalse();
+  }
+
+  @Test
+  public void sdiffstore_sets_expiration_time_to_zero() {
+    RegionProvider regionProvider = uncheckedCast(mock(RegionProvider.class));
+    Region<RedisKey, RedisData> dataRegion = uncheckedCast(mock(Region.class));
+
+    RedisSet setDest = createRedisSet(1, 2);
+    setDest.setExpirationTimestamp(dataRegion, null, 100);
+
+    when(regionProvider.getTypedRedisDataElseRemove(REDIS_SET, null, 
false)).thenReturn(setDest);
+    when(regionProvider.getDataRegion()).thenReturn(dataRegion);
+    when(dataRegion.put(any(), 
any())).thenAnswer(this::validateDeltaSerialization);
+
+    RedisSet.MemberSet set = new RedisSet.MemberSet();
+    set.add(new byte[] {3});
+    setOpStoreResult(regionProvider, null, set);
+
+    assertThat(setDest.getExpirationTimestamp()).isEqualTo(NO_EXPIRATION);
+    assertThat(setDest.hasDelta()).isFalse();
+  }
+
+  @Test
+  public void setExpirationTimestamp_stores_delta_that_is_stable() {
+    Region<RedisKey, RedisData> region = uncheckedCast(mock(Region.class));
+    when(region.put(any(), 
any())).thenAnswer(this::validateDeltaSerialization);
+
+    RedisSet set1 = createRedisSet(1, 2);
+    set1.setExpirationTimestamp(region, null, 999);
+
+    verify(region).put(any(), any());
+    assertThat(set1.hasDelta()).isFalse();
+  }
+
+  /************* test size of bytes in use *************/
+
+  /******* constructor *******/
+  @Test
+  public void should_calculateSize_equalToROS_withNoMembers() {
+    Set<byte[]> members = new 
ObjectOpenCustomHashSet<>(ByteArrays.HASH_STRATEGY);
+    RedisSet set = new RedisSet(members);
+
+    int expected = expectedSize(set);
+    int actual = set.getSizeInBytes();
+
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void should_calculateSize_equalToROS_withSingleMember() {
+    Set<byte[]> members = new 
ObjectOpenCustomHashSet<>(ByteArrays.HASH_STRATEGY);
+    members.add(stringToBytes("value"));
+    RedisSet set = new RedisSet(members);
+
+    int actual = set.getSizeInBytes();
+    int expected = expectedSize(set);
+
+    assertThat(actual).isEqualTo(expected);
+  }
+
+  @Test
+  public void should_calculateSize_equalToROS_withVaryingMemberCounts() {
+    for (int i = 0; i < 1024; i += 16) {
+      RedisSet set = createRedisSetOfSpecifiedSize(i);
+
+      int expected = expectedSize(set);
+      int actual = set.getSizeInBytes();
+
+      assertThat(actual).isEqualTo(expected);
+    }
+  }
+
+  private int expectedSize(RedisSet set) {
+    return sizer.sizeof(set) - sizer.sizeof(ByteArrays.HASH_STRATEGY);
+  }
+
+  @Test
+  public void should_calculateSize_equalToROS_withVaryingMemberSize() {
+    for (int i = 0; i < 1024; i += 16) {
+      RedisSet set = createRedisSetWithMemberOfSpecifiedSize(i * 64);
+      int expected = expectedSize(set);
+      int actual = set.getSizeInBytes();
+
+      assertThat(actual).isEqualTo(expected);
+    }
+  }
+
+  /******* sadd *******/
+  @Test
+  public void bytesInUse_sadd_withOneMember() {
+    RedisSet set = new RedisSet(new ArrayList<>());
+    Region<RedisKey, RedisData> region = uncheckedCast(mock(Region.class));
+    final RedisData returnData = mock(RedisData.class);
+    when(region.put(any(RedisKey.class), 
any(RedisData.class))).thenReturn(returnData);
+    final RedisKey key = new RedisKey(stringToBytes("key"));
+    String valueString = "value";
+
+    final byte[] value = stringToBytes(valueString);
+    List<byte[]> members = new ArrayList<>();
+    members.add(value);
+
+    set.sadd(members, region, key);
+
+    assertThat(set.getSizeInBytes()).isEqualTo(expectedSize(set));
+  }
+
+  @Test
+  public void bytesInUse_sadd_withMultipleMembers() {
+    RedisSet set = new RedisSet(new ArrayList<>());
+    Region<RedisKey, RedisData> region = uncheckedCast(mock(Region.class));
+    final RedisData returnData = mock(RedisData.class);
+    when(region.put(any(RedisKey.class), 
any(RedisData.class))).thenReturn(returnData);
+    final RedisKey key = new RedisKey(stringToBytes("key"));
+    String baseString = "value";
+
+    for (int i = 0; i < 1_000; i++) {
+      List<byte[]> members = new ArrayList<>();
+      String valueString = baseString + i;
+      final byte[] value = stringToBytes(valueString);
+      members.add(value);
+      set.sadd(members, region, key);
+
+      long actual = set.getSizeInBytes();
+      long expected = expectedSize(set);
+
+      assertThat(actual).isEqualTo(expected);
+    }
+  }
+
+  /******* remove *******/
+  @Test
+  public void size_shouldDecrease_WhenValueIsRemoved() {
+    Region<RedisKey, RedisData> region = uncheckedCast(mock(Region.class));
+    final RedisData returnData = mock(RedisData.class);
+    when(region.put(any(RedisKey.class), 
any(RedisData.class))).thenReturn(returnData);
+    final RedisKey key = new RedisKey(stringToBytes("key"));
+    final byte[] value1 = stringToBytes("value1");
+    final byte[] value2 = stringToBytes("value2");
+
+    List<byte[]> members = new ArrayList<>();
+    members.add(value1);
+    members.add(value2);
+    RedisSet set = new RedisSet(members);
+
+    List<byte[]> membersToRemove = new ArrayList<>();
+    membersToRemove.add(value1);
+    set.srem(membersToRemove, region, key);
+
+    long finalSize = set.getSizeInBytes();
+    long expectedSize = expectedSize(set);
+
+    assertThat(finalSize).isEqualTo(expectedSize);
+  }
+
+  /******** add and remove *******/
+  @Test
+  public void testSAddsAndSRems_changeSizeToMatchROSSize() {
+    // Start with a non-empty set, add enough members to force a resize of the 
backing set, remove
+    // all but one member, then add members back and assert that the 
calculated size is correct
+    // after every operation
+    List<byte[]> initialMembers = new ArrayList<>();
+    int numOfInitialMembers = 128;
+    for (int i = 0; i < numOfInitialMembers; ++i) {
+      byte[] data = Coder.intToBytes(i);
+      initialMembers.add(data);
+    }
+
+    RedisSet set = new RedisSet(initialMembers);
+
+    assertThat(set.getSizeInBytes()).isEqualTo(expectedSize(set));
+
+    int membersToAdd = numOfInitialMembers * 3;
+    doAddsAndAssertSize(set, membersToAdd);
+
+    doRemovesAndAssertSize(set, set.scard() - 1);
+
+    doAddsAndAssertSize(set, membersToAdd);
+  }
+
+  @Test
+  public void testConcurrencyWhenAddingMembers() throws Exception {
+    int expectedSize = 1000;
+    // Make sure the initial size is smaller than the expected size.
+    RedisSet set = new RedisSet(1);
+
+    AtomicBoolean running = new AtomicBoolean(true);
+    Future<Void> future1 = executor.submit(() -> iterateOverSet(set, running));
+    Future<Void> future2 = executor.submit(() -> addToSet(set, expectedSize));
+
+    future2.get();
+    running.set(false);
+    future1.get();
+
+    assertThat(set.scard()).isEqualTo(expectedSize);
+  }
+
+  @Test
+  public void testConcurrencyWhenRemovingMembers() throws Exception {
+    int numOfInitialMembers = 1000;
+    RedisSet set = new RedisSet(numOfInitialMembers);
+    for (int i = 0; i < numOfInitialMembers; ++i) {
+      set.membersAdd(Coder.intToBytes(i));
+    }
+
+    AtomicBoolean running = new AtomicBoolean(true);
+    Future<Void> future1 = executor.submit(() -> iterateOverSet(set, running));
+    Future<Void> future2 = executor.submit(() -> deleteFromSet(set));
+
+    future2.get();
+    running.set(false);
+    future1.get();
+
+    assertThat(set.scard()).isEqualTo(0);
+  }
+
+
+  /******* helper methods *******/
+
+  private void addToSet(RedisSet set, int count) {
+    for (int i = 0; i < count; i++) {
+      assertThat(set.membersAdd(Coder.intToBytes(i))).isTrue();
+    }
+  }
+
+  private void deleteFromSet(RedisSet set) {
+    int size = set.scard();
+    for (int i = 0; i < size; i++) {
+      Collection<byte[]> removals = set.srandmember(1);
+      byte[] candidate = removals.iterator().next();
+      assertThat(set.membersRemove(candidate))
+          .as("Did not find " + Coder.bytesToLong(candidate)).isTrue();
+    }
+  }
+
+  private void iterateOverSet(RedisSet set, AtomicBoolean running) throws 
Exception {
+    while (running.get()) {
+      HeapDataOutputStream out = new HeapDataOutputStream(100);
+      set.toData(out, null);
+    }
+  }
+
+  private RedisSet createRedisSetOfSpecifiedSize(int setSize) {
+    List<byte[]> arrayList = new ArrayList<>();
+    for (int i = 0; i < setSize; i++) {
+      arrayList.add(stringToBytes(("abcdefgh" + i)));
+    }
+    return new RedisSet(arrayList);
+  }
+
+  private RedisSet createRedisSetWithMemberOfSpecifiedSize(int memberSize) {
+    List<byte[]> arrayList = new ArrayList<>();
+    byte[] member = stringToBytes(createMemberOfSpecifiedSize("a", 
memberSize));
+    if (member.length > 0) {
+      arrayList.add(member);
+    }
+    return new RedisSet(arrayList);
+  }
+
+  private String createMemberOfSpecifiedSize(final String base, final int 
stringSize) {
+    Random random = new Random();
+    if (base.length() > stringSize) {
+      return "";
+    }
+    StringBuilder sb = new StringBuilder(stringSize);
+    sb.append(base);
+    for (int i = base.length(); i < stringSize; i++) {
+      int randy = random.nextInt(10);
+      sb.append(randy);
+    }
+    return sb.toString();
+  }
+
+  void doAddsAndAssertSize(RedisSet set, int membersToAdd) {
+    for (int i = 0; i < membersToAdd; ++i) {
+      int initialSize = sizer.sizeof(set);
+      int initialCalculatedSize = set.getSizeInBytes();
+
+      byte[] data = Coder.intToBytes(set.scard());
+      assertThat(set.membersAdd(data)).isTrue();
+
+      int actualOverhead = sizer.sizeof(set) - initialSize;
+      int calculatedOH = set.getSizeInBytes() - initialCalculatedSize;
+
+      assertThat(calculatedOH).isEqualTo(actualOverhead);
+    }
+    assertThat(set.getSizeInBytes()).isEqualTo(expectedSize(set));
+  }
+
+  void doRemovesAndAssertSize(RedisSet set, int membersToRemove) {
+    int initialCapacity = set.scard();
+    for (int i = 1; i < membersToRemove; ++i) {
+      int initialSize = sizer.sizeof(set);
+      int initialCalculatedSize = set.getSizeInBytes();
+
+      byte[] data = Coder.intToBytes(initialCapacity - i);
+      assertThat(set.membersRemove(data)).isTrue();
+
+      int actualOverhead = sizer.sizeof(set) - initialSize;
+      int calculatedOH = set.getSizeInBytes() - initialCalculatedSize;
+
+      assertThat(calculatedOH).isEqualTo(actualOverhead);
+    }
+    assertThat(set.getSizeInBytes()).isEqualTo(expectedSize(set));
+  }
+
+  private Object validateDeltaSerialization(InvocationOnMock invocation) 
throws IOException {
+    RedisSet value = invocation.getArgument(1, RedisSet.class);
+    assertThat(value.hasDelta()).isTrue();
+    HeapDataOutputStream out = new HeapDataOutputStream(100);
+    value.toDelta(out);
+    ByteArrayDataInput in = new ByteArrayDataInput(out.toByteArray());
+    RedisSet set2 = createRedisSet(1, 2);
+    assertThat(set2).isNotEqualTo(value);
+    set2.fromDelta(in);
+    assertThat(set2).isEqualTo(value);
+    return null;
+  }
+}

Reply via email to