diff --git a/src/main/java/net/spy/memcached/v2/AsyncArcusCommands.java b/src/main/java/net/spy/memcached/v2/AsyncArcusCommands.java index 61666ce7b..88cb68359 100644 --- a/src/main/java/net/spy/memcached/v2/AsyncArcusCommands.java +++ b/src/main/java/net/spy/memcached/v2/AsyncArcusCommands.java @@ -21,8 +21,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; @@ -62,6 +64,11 @@ import net.spy.memcached.collection.ListDelete; import net.spy.memcached.collection.ListGet; import net.spy.memcached.collection.ListInsert; +import net.spy.memcached.collection.SetCreate; +import net.spy.memcached.collection.SetDelete; +import net.spy.memcached.collection.SetExist; +import net.spy.memcached.collection.SetGet; +import net.spy.memcached.collection.SetInsert; import net.spy.memcached.internal.result.GetsResultImpl; import net.spy.memcached.ops.APIType; import net.spy.memcached.ops.BTreeGetBulkOperation; @@ -1660,4 +1667,117 @@ public ArcusFuture lopDelete(String key, int from, int to, boolean drop ListDelete delete = new ListDelete(from, to, dropIfEmpty, false); return collectionDelete(key, delete); } + + public ArcusFuture sopCreate(String key, ElementValueType type, + CollectionAttributes attributes) { + if (attributes == null) { + throw new IllegalArgumentException("CollectionAttributes cannot be null"); + } + + SetCreate create = new SetCreate( + TranscoderUtils.examineFlags(type), attributes.getExpireTime(), + attributes.getMaxCount(), attributes.getReadable(), false); + return collectionCreate(key, create); + } + + public ArcusFuture sopInsert(String key, T value) { + return sopInsert(key, value, null); + } + + public ArcusFuture sopInsert(String key, T value, CollectionAttributes attributes) { + SetInsert insert = new SetInsert<>(value, null, attributes); + return collectionInsert(key, "", insert); + } + + public ArcusFuture sopExist(String key, T value) { + AbstractArcusResult result = new AbstractArcusResult<>(new AtomicReference<>()); + ArcusFutureImpl future = new ArcusFutureImpl<>(result); + SetExist exist = new SetExist<>(value, tcForCollection); + ArcusClient client = arcusClientSupplier.get(); + + OperationCallback cb = new OperationCallback() { + @Override + public void receivedStatus(OperationStatus status) { + switch (status.getStatusCode()) { + case EXIST: + result.set(true); + break; + case NOT_EXIST: + result.set(false); + break; + case ERR_NOT_FOUND: + result.set(null); + break; + case CANCELLED: + future.internalCancel(); + break; + default: + /* + * TYPE_MISMATCH / UNREADABLE / NOT_SUPPORTED or unknown statement + */ + result.addError(key, status); + break; + } + } + + @Override + public void complete() { + future.complete(); + } + }; + Operation op = client.getOpFact().collectionExist(key, "", exist, cb); + future.setOp(op); + client.addOp(key, op); + + return future; + } + + public ArcusFuture> sopGet(String key, int count, GetArgs args) { + AbstractArcusResult> result + = new AbstractArcusResult<>(new AtomicReference<>(new HashSet<>())); + ArcusFutureImpl> future = new ArcusFutureImpl<>(result); + SetGet get = new SetGet(count, args.isWithDelete(), args.isDropIfEmpty()); + ArcusClient client = arcusClientSupplier.get(); + + CollectionGetOperation.Callback cb = new CollectionGetOperation.Callback() { + @Override + public void gotData(String subkey, int flags, byte[] data, byte[] eflag) { + CachedData cachedData = new CachedData(flags, data, tcForCollection.getMaxSize()); + result.get().add(tcForCollection.decode(cachedData)); + } + + @Override + public void receivedStatus(OperationStatus status) { + switch (status.getStatusCode()) { + case SUCCESS: + case ERR_NOT_FOUND_ELEMENT: + break; + case ERR_NOT_FOUND: + result.set(null); + break; + case CANCELLED: + future.internalCancel(); + break; + default: + /* TYPE_MISMATCH / UNREADABLE / NOT_SUPPORTED or unknown statement */ + result.addError(key, status); + } + } + + @Override + public void complete() { + future.complete(); + } + }; + Operation op = client.getOpFact().collectionGet(key, get, cb); + future.setOp(op); + client.addOp(key, op); + + return future; + } + + public ArcusFuture sopDelete(String key, T value, boolean dropIfEmpty) { + SetDelete delete = new SetDelete<>(value, dropIfEmpty, false, tcForCollection); + return collectionDelete(key, delete); + } } diff --git a/src/main/java/net/spy/memcached/v2/AsyncArcusCommandsIF.java b/src/main/java/net/spy/memcached/v2/AsyncArcusCommandsIF.java index ba5b1268f..602854f14 100644 --- a/src/main/java/net/spy/memcached/v2/AsyncArcusCommandsIF.java +++ b/src/main/java/net/spy/memcached/v2/AsyncArcusCommandsIF.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Map; +import java.util.Set; import net.spy.memcached.CASValue; import net.spy.memcached.collection.CollectionAttributes; @@ -554,4 +555,69 @@ ArcusFuture lopCreate(String key, ElementValueType type, * {@code false} if no elements are found in the range. */ ArcusFuture lopDelete(String key, int from, int to, boolean dropIfEmpty); + + /** + * Create a set with the given attributes. + * + * @param key key of the set to create + * @param type element value type + * @param attributes initial attributes of the set + * @return {@code true} if created, {@code false} if the key already exists. + */ + ArcusFuture sopCreate(String key, ElementValueType type, + CollectionAttributes attributes); + + /** + * Insert an element into a set. + * + * @param key key of the set + * @param value the value to insert + * @return {@code true} if the element was inserted, {@code false} if the element already exists, + * {@code null} if the key is not found. + */ + ArcusFuture sopInsert(String key, T value); + + /** + * Insert an element into a set. + * If the set does not exist, it is created with the given attributes. + * + * @param key key of the set + * @param value the value to insert + * @param attributes attributes to use when creating the set, or {@code null} to not create + * @return {@code true} if the element was inserted, {@code false} if the element already exists, + * {@code null} if the key is not found. + */ + ArcusFuture sopInsert(String key, T value, CollectionAttributes attributes); + + /** + * Check whether an element exists in a set. + * + * @param key key of the set + * @param value the value to check + * @return {@code true} if the element exists, {@code false} if the element is not found, + * {@code null} if the key is not found. + */ + ArcusFuture sopExist(String key, T value); + + /** + * Get elements randomly from a set. + * + * @param key key of the set + * @param count number of elements to retrieve randomly (0 means all elements, max 1000) + * @param args arguments for get operation + * @return set of element values, an empty set if no elements are found, + * {@code null} if the key is not found. + */ + ArcusFuture> sopGet(String key, int count, GetArgs args); + + /** + * Delete an element from a set. + * + * @param key key of the set + * @param value the value to delete + * @param dropIfEmpty whether to delete the set if it becomes empty after deletion + * @return {@code true} if the element was deleted, {@code false} if the element is not found, + * {@code null} if the key is not found. + */ + ArcusFuture sopDelete(String key, T value, boolean dropIfEmpty); } diff --git a/src/test/java/net/spy/memcached/v2/SetAsyncArcusCommandsTest.java b/src/test/java/net/spy/memcached/v2/SetAsyncArcusCommandsTest.java new file mode 100644 index 000000000..307871909 --- /dev/null +++ b/src/test/java/net/spy/memcached/v2/SetAsyncArcusCommandsTest.java @@ -0,0 +1,375 @@ +package net.spy.memcached.v2; + +import java.util.concurrent.TimeUnit; + +import net.spy.memcached.collection.CollectionAttributes; +import net.spy.memcached.collection.ElementValueType; +import net.spy.memcached.ops.OperationException; +import net.spy.memcached.v2.vo.GetArgs; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class SetAsyncArcusCommandsTest extends AsyncArcusCommandsTest { + + @Test + void sopCreate() throws Exception { + // given + String key = keys.get(0); + + // when + async.sopCreate(key, ElementValueType.STRING, new CollectionAttributes()) + // then + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopCreateAlreadyExists() throws Exception { + // given + String key = keys.get(0); + + async.sopCreate(key, ElementValueType.STRING, new CollectionAttributes()) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopCreate(key, ElementValueType.STRING, new CollectionAttributes()) + // then + .thenAccept(Assertions::assertFalse) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopInsert() throws Exception { + // given + String key = keys.get(0); + + async.sopCreate(key, ElementValueType.STRING, new CollectionAttributes()) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopInsert(key, VALUE) + // then + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopInsertNotFound() throws Exception { + // when + async.sopInsert(keys.get(0), VALUE) + // then + .thenAccept(Assertions::assertNull) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopInsertWithAttributes() throws Exception { + // given + String key = keys.get(0); + + // when + async.sopInsert(key, VALUE, new CollectionAttributes()) + // then + .thenCompose(result -> { + assertTrue(result); + return async.sopExist(key, VALUE); + }) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopInsertDuplicate() throws Exception { + // given + String key = keys.get(0); + + async.sopInsert(key, VALUE, new CollectionAttributes()) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopInsert(key, VALUE) + // then + .thenAccept(Assertions::assertFalse) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopInsertTypeMismatch() throws Exception { + // given + String key = keys.get(0); + + async.set(key, 0, VALUE) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopInsert(key, VALUE, new CollectionAttributes()) + // then + .handle((result, ex) -> { + assertInstanceOf(OperationException.class, ex); + assertTrue(ex.getMessage().contains("TYPE_MISMATCH")); + return result; + }) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopExistTrue() throws Exception { + // given + String key = keys.get(0); + + async.sopInsert(key, VALUE, new CollectionAttributes()) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopExist(key, VALUE) + // then + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopExistFalse() throws Exception { + // given + String key = keys.get(0); + + async.sopCreate(key, ElementValueType.STRING, new CollectionAttributes()) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when: 존재하지 않는 값 조회 + async.sopExist(key, VALUE) + // then + .thenAccept(Assertions::assertFalse) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopExistNotFound() throws Exception { + // given + // when + async.sopExist(keys.get(0), VALUE) + // then + .thenAccept(Assertions::assertNull) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopExistTypeMismatch() throws Exception { + // given + String key = keys.get(0); + + async.set(key, 0, VALUE) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopExist(key, VALUE) + // then + .handle((result, ex) -> { + assertInstanceOf(OperationException.class, ex); + assertTrue(ex.getMessage().contains("TYPE_MISMATCH")); + return result; + }) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopGet() throws Exception { + // given + String key = keys.get(0); + + async.sopInsert(key, "v0", new CollectionAttributes()) + .thenCompose(result -> async.sopInsert(key, "v1")) + .thenCompose(result -> async.sopInsert(key, "v2")) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopGet(key, 10, GetArgs.DEFAULT) + // then + .thenAccept(result -> { + assertNotNull(result); + assertEquals(3, result.size()); + }) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopGetNotFound() throws Exception { + // given + // when + async.sopGet(keys.get(0), 10, GetArgs.DEFAULT) + // then + .thenAccept(Assertions::assertNull) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopGetNotFoundElement() throws Exception { + // given + String key = keys.get(0); + + async.sopCreate(key, ElementValueType.STRING, new CollectionAttributes()) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopGet(key, 10, GetArgs.DEFAULT) + // then + .thenAccept(result -> assertTrue(result.isEmpty())) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopGetWithDelete() throws Exception { + // given + String key = keys.get(0); + GetArgs args = new GetArgs.Builder() + .withDelete() + .dropIfEmpty() + .build(); + + async.sopInsert(key, VALUE, new CollectionAttributes()) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopGet(key, 10, args) + .thenCompose(result -> { + assertNotNull(result); + assertEquals(1, result.size()); + return async.sopGet(key, 10, GetArgs.DEFAULT); + }) + .thenAccept(Assertions::assertNull) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopDelete() throws Exception { + // given + String key = keys.get(0); + + async.sopInsert(key, VALUE, new CollectionAttributes()) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopDelete(key, VALUE, false) + // then + .thenCompose(result -> { + assertTrue(result); + return async.sopExist(key, VALUE); + }) + .thenAccept(Assertions::assertFalse) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopDeleteNotFound() throws Exception { + // when + async.sopDelete(keys.get(0), VALUE, false) + // then + .thenAccept(Assertions::assertNull) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopDeleteNotFoundElement() throws Exception { + // given + String key = keys.get(0); + + async.sopCreate(key, ElementValueType.STRING, new CollectionAttributes()) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopDelete(key, VALUE, false) + // then + .thenAccept(Assertions::assertFalse) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopDeleteDropIfEmpty() throws Exception { + // given + String key = keys.get(0); + + async.sopInsert(key, VALUE, new CollectionAttributes()) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when: 마지막 요소 삭제 + dropIfEmpty=true + async.sopDelete(key, VALUE, true) + // then: set 자체가 삭제되어 null + .thenCompose(result -> { + assertTrue(result); + return async.sopGet(key, 10, GetArgs.DEFAULT); + }) + .thenAccept(Assertions::assertNull) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void sopDeleteTypeMismatch() throws Exception { + // given + String key = keys.get(0); + + async.set(key, 0, VALUE) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.sopDelete(key, VALUE, false) + // then + .handle((result, ex) -> { + assertInstanceOf(OperationException.class, ex); + assertTrue(ex.getMessage().contains("TYPE_MISMATCH")); + return result; + }) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } +}