From fc340bd01269fef7dba52871ed09ff10b9a81634 Mon Sep 17 00:00:00 2001 From: f1v3-dev Date: Wed, 15 Jul 2026 13:52:14 +0900 Subject: [PATCH] FEATURE: Add attribute get/set APIs --- .../spy/memcached/v2/AsyncArcusCommands.java | 87 ++++++++ .../memcached/v2/AsyncArcusCommandsIF.java | 19 ++ .../v2/attribute/UpdateAttributes.java | 25 ++- .../v2/AttributesAsyncArcusCommandsTest.java | 201 ++++++++++++++++++ .../v2/attribute/UpdateAttributesTest.java | 2 +- 5 files changed, 330 insertions(+), 4 deletions(-) create mode 100644 src/test/java/net/spy/memcached/v2/AttributesAsyncArcusCommandsTest.java diff --git a/src/main/java/net/spy/memcached/v2/AsyncArcusCommands.java b/src/main/java/net/spy/memcached/v2/AsyncArcusCommands.java index 14ed91459..14ebfe0ac 100644 --- a/src/main/java/net/spy/memcached/v2/AsyncArcusCommands.java +++ b/src/main/java/net/spy/memcached/v2/AsyncArcusCommands.java @@ -93,6 +93,7 @@ import net.spy.memcached.ops.CollectionGetOperation; import net.spy.memcached.ops.CollectionInsertOperation; import net.spy.memcached.ops.ConcatenationType; +import net.spy.memcached.ops.GetAttrOperation; import net.spy.memcached.ops.GetOperation; import net.spy.memcached.ops.GetsOperation; import net.spy.memcached.ops.Mutator; @@ -103,6 +104,8 @@ import net.spy.memcached.ops.StoreType; import net.spy.memcached.transcoders.Transcoder; import net.spy.memcached.transcoders.TranscoderUtils; +import net.spy.memcached.v2.attribute.ItemAttributes; +import net.spy.memcached.v2.attribute.UpdateAttributes; import net.spy.memcached.v2.vo.BKey; import net.spy.memcached.v2.vo.BTreeElement; import net.spy.memcached.v2.vo.BTreeGetResult; @@ -2382,4 +2385,88 @@ public void complete() { return resultMap; }); } + + @Override + public ArcusFuture setAttributes(String key, UpdateAttributes attributes) { + AbstractArcusResult result = new AbstractArcusResult<>(new AtomicReference<>()); + ArcusFutureImpl future = new ArcusFutureImpl<>(result); + ArcusClient client = arcusClientSupplier.get(); + + OperationCallback cb = new OperationCallback() { + @Override + public void receivedStatus(OperationStatus status) { + switch (status.getStatusCode()) { + case SUCCESS: + result.set(true); + break; + case ERR_NOT_FOUND: + result.set(false); + break; + case CANCELLED: + future.internalCancel(); + break; + default: + /* + * ATTR_ERROR or unknown statement. + */ + result.addError(key, status); + } + } + + @Override + public void complete() { + future.complete(); + } + }; + Operation op = client.getOpFact().setAttr(key, attributes, cb); + future.setOp(op); + client.addOp(key, op); + + return future; + } + + @Override + public ArcusFuture getAttributes(String key) { + AbstractArcusResult result = new AbstractArcusResult<>(new AtomicReference<>()); + ArcusFutureImpl future = new ArcusFutureImpl<>(result); + ArcusClient client = arcusClientSupplier.get(); + + ItemAttributes attributes = new ItemAttributes(); + GetAttrOperation.Callback cb = new GetAttrOperation.Callback() { + @Override + public void gotAttribute(String key, String attr) { + attributes.setAttribute(attr); + } + + @Override + public void receivedStatus(OperationStatus status) { + switch (status.getStatusCode()) { + case SUCCESS: + result.set(attributes); + break; + case ERR_NOT_FOUND: + result.set(null); + break; + case CANCELLED: + future.internalCancel(); + break; + default: + /* + * ATTR_ERROR or unknown statement. + */ + result.addError(key, status); + } + } + + @Override + public void complete() { + future.complete(); + } + }; + Operation op = client.getOpFact().getAttr(key, cb); + future.setOp(op); + client.addOp(key, op); + + return future; + } } diff --git a/src/main/java/net/spy/memcached/v2/AsyncArcusCommandsIF.java b/src/main/java/net/spy/memcached/v2/AsyncArcusCommandsIF.java index c28a9fe8d..37c48dbc3 100644 --- a/src/main/java/net/spy/memcached/v2/AsyncArcusCommandsIF.java +++ b/src/main/java/net/spy/memcached/v2/AsyncArcusCommandsIF.java @@ -27,6 +27,8 @@ import net.spy.memcached.collection.CreateAttributes; import net.spy.memcached.collection.ElementFlagFilter; import net.spy.memcached.collection.ElementValueType; +import net.spy.memcached.v2.attribute.ItemAttributes; +import net.spy.memcached.v2.attribute.UpdateAttributes; import net.spy.memcached.v2.vo.BKey; import net.spy.memcached.v2.vo.BTreeElement; import net.spy.memcached.v2.vo.BTreeGetResult; @@ -885,4 +887,21 @@ ArcusFuture bopDelete(String key, BKey from, BKey to, * @return a map of each server's {@link java.net.SocketAddress} to its version string */ ArcusFuture> versions(); + + /** + * Set the attributes of the item stored at the given key. + * + * @param key the key + * @param attributes the attributes to set + * @return {@code true} if the attributes were set, otherwise {@code false} + */ + ArcusFuture setAttributes(String key, UpdateAttributes attributes); + + /** + * Get the attributes of the item stored at the given key. + * + * @param key the key + * @return the {@link ItemAttributes} of the item, or {@code null} if not found + */ + ArcusFuture getAttributes(String key); } diff --git a/src/main/java/net/spy/memcached/v2/attribute/UpdateAttributes.java b/src/main/java/net/spy/memcached/v2/attribute/UpdateAttributes.java index d3c4fdeb5..1eec69526 100644 --- a/src/main/java/net/spy/memcached/v2/attribute/UpdateAttributes.java +++ b/src/main/java/net/spy/memcached/v2/attribute/UpdateAttributes.java @@ -29,7 +29,7 @@ private UpdateAttributes(Builder builder) { b.append(" overflowaction=").append(builder.overflowAction); } if (builder.readable != null) { - b.append(" readable=").append(builder.readable ? "on" : "off"); + b.append(" readable=").append("on"); } if (builder.maxBKeyRange != null) { b.append(" maxbkeyrange=").append(builder.maxBKeyRange); @@ -66,6 +66,25 @@ public Builder expireTime(long expireTime) { return this; } + /** + * Sets the maximum number of elements the collection may hold. + * + *

The value is interpreted by the server, and two cases are handled + * specially: + *

    + *
  • + * If the value is {@code 0}, the server applies its default count (4,000). + *
  • + *
  • + * If the value is greater than the server's configured maximum, + * the server silently clamps it to that maximum + * and still reports the operation as successful. + *
  • + *
+ * + * @param maxCount the maximum number of elements + * @return this Builder + */ public Builder maxCount(long maxCount) { this.maxCount = maxCount; return this; @@ -76,8 +95,8 @@ public Builder overflowAction(CollectionOverflowAction overflowAction) { return this; } - public Builder readable(boolean readable) { - this.readable = readable; + public Builder readable() { + this.readable = true; return this; } diff --git a/src/test/java/net/spy/memcached/v2/AttributesAsyncArcusCommandsTest.java b/src/test/java/net/spy/memcached/v2/AttributesAsyncArcusCommandsTest.java new file mode 100644 index 000000000..db6cbfb61 --- /dev/null +++ b/src/test/java/net/spy/memcached/v2/AttributesAsyncArcusCommandsTest.java @@ -0,0 +1,201 @@ +package net.spy.memcached.v2; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import net.spy.memcached.collection.CollectionOverflowAction; +import net.spy.memcached.collection.CreateAttributes; +import net.spy.memcached.collection.ElementValueType; +import net.spy.memcached.ops.OperationException; +import net.spy.memcached.v2.attribute.UpdateAttributes; +import net.spy.memcached.v2.vo.BKey; + +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; + +public class AttributesAsyncArcusCommandsTest extends AsyncArcusCommandsTest { + + @Test + void setKVAttributesSuccess() throws ExecutionException, InterruptedException, TimeoutException { + // given + String key = keys.get(0); + + async.set(key, 0, VALUE) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + UpdateAttributes attributes = UpdateAttributes.builder() + .expireTime(100L) + .build(); + + async.setAttributes(key, attributes) + // then + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void setBTreeAttributesSuccess() + throws ExecutionException, InterruptedException, TimeoutException { + // given + String key = keys.get(0); + + CreateAttributes createAttributes = CreateAttributes.builder() + .readable(false) + .build(); + + async.bopCreate(key, ElementValueType.STRING, createAttributes) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + UpdateAttributes attributes = UpdateAttributes.builder() + .expireTime(100L) + .maxCount(5_000L) + .overflowAction(CollectionOverflowAction.largest_trim) + .maxBKeyRange(BKey.of(10L)) + .readable() + .build(); + + async.setAttributes(key, attributes) + .thenCompose(result -> { + assertTrue(result); + return async.getAttributes(key); + }) + .thenAccept(result -> { + assertNotNull(result); + assertEquals(100L, result.getExpireTime()); + assertEquals(5_000L, result.getMaxCount()); + assertEquals(CollectionOverflowAction.largest_trim, result.getOverflowAction()); + assertEquals(true, result.getReadable()); + }) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void setAttributesFailureKeyNotFound() + throws ExecutionException, InterruptedException, TimeoutException { + // given + String key = keys.get(0); + + UpdateAttributes attributes = UpdateAttributes.builder() + .expireTime(100L) + .build(); + + // when + async.setAttributes(key, attributes) + // then + .thenAccept(Assertions::assertFalse) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void setAttributesFailureMapOverflowAction() + throws ExecutionException, InterruptedException, TimeoutException { + // given + String key = keys.get(0); + + async.mopCreate(key, ElementValueType.STRING, CreateAttributes.DEFAULT) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + UpdateAttributes attributes = UpdateAttributes.builder() + .overflowAction(CollectionOverflowAction.smallest_trim) + .build(); + + // when + async.setAttributes(key, attributes) + // then + .handle((result, ex) -> { + assertInstanceOf(OperationException.class, ex); + assertTrue(ex.getMessage().contains("ATTR_ERROR")); + return result; + }) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void setAttributesFailureSetOverflowAction() + throws ExecutionException, InterruptedException, TimeoutException { + // given + String key = keys.get(0); + + async.sopCreate(key, ElementValueType.STRING, CreateAttributes.DEFAULT) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + UpdateAttributes attributes = UpdateAttributes.builder() + .overflowAction(CollectionOverflowAction.largest_trim) + .build(); + + // when + async.setAttributes(key, attributes) + // then + .handle((result, ex) -> { + assertInstanceOf(OperationException.class, ex); + assertTrue(ex.getMessage().contains("ATTR_ERROR")); + return result; + }) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void getAttributesSuccess() throws ExecutionException, InterruptedException, TimeoutException { + // given + String key = keys.get(0); + + CreateAttributes attributes = CreateAttributes.builder() + .expireTime(100L) + .maxCount(5000L) + .overflowAction(CollectionOverflowAction.smallest_trim) + .build(); + + async.bopCreate(key, ElementValueType.STRING, attributes) + .thenAccept(Assertions::assertTrue) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + + // when + async.getAttributes(key) + // then + .thenAccept(result -> { + assertNotNull(result); + assertTrue(result.getExpireTime() <= 100L); + assertEquals(5_000L, result.getMaxCount()); + assertEquals(CollectionOverflowAction.smallest_trim, result.getOverflowAction()); + }) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + + @Test + void getAttributesFailureKeyNotFound() + throws ExecutionException, InterruptedException, TimeoutException { + // given + String key = keys.get(0); + + // when + async.getAttributes(key) + // then + .thenAccept(Assertions::assertNull) + .toCompletableFuture() + .get(300L, TimeUnit.MILLISECONDS); + } + +} diff --git a/src/test/java/net/spy/memcached/v2/attribute/UpdateAttributesTest.java b/src/test/java/net/spy/memcached/v2/attribute/UpdateAttributesTest.java index 1a72da394..edb50e701 100644 --- a/src/test/java/net/spy/memcached/v2/attribute/UpdateAttributesTest.java +++ b/src/test/java/net/spy/memcached/v2/attribute/UpdateAttributesTest.java @@ -38,7 +38,7 @@ void multipleFieldsJoinedBySpace() { UpdateAttributes attributes = UpdateAttributes.builder() .maxCount(2_000L) .overflowAction(CollectionOverflowAction.error) - .readable(true) + .readable() .build(); assertEquals("maxcount=2000 overflowaction=error readable=on", attributes.stringify());