diff --git a/.github/workflows/install-redis-modules/action.yml b/.github/workflows/install-redis-modules/action.yml deleted file mode 100644 index e4e9c9453a..0000000000 --- a/.github/workflows/install-redis-modules/action.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Install Redis Modules - -inputs: - redis-version: - description: "redis version of clusters" - required: true - type: string - - modules: - description: "required redis modules to install" - required: false - type: string - default: 'all' - options: - - "all" - - "search" - - "json" - - - -runs: - using: "composite" - steps: - - name: Cache RedisJSON Dependencies - if: inputs.modules == 'all' || inputs.modules == 'json' - id: cache-dependencies-redisjson - uses: actions/cache@v3 - with: - path: | - ./cmake - ./redisjson/bin - key: ${{ runner.os }}-${{ inputs.redis-version }}-redisjson - - - - name: Install CMake - if: steps.cache-dependencies-redisearch.outputs.cache-hit != 'true' || steps.cache-dependencies-redisjson.outputs.cache-hit != 'true' - shell: bash - run: | - set -x - sudo apt-get update - sudo apt-get install -y cmake - cp /usr/bin/cmake ./cmake - - - - name: Checkout RedisJSON Repository - if: steps.cache-dependencies-redisjson.outputs.cache-hit != 'true' && (inputs.modules == 'all' || inputs.modules == 'json') - uses: actions/checkout@v4 - with: - repository: "RedisJSON/RedisJSON" - path: "./redisjson" - ref: ${{ startsWith(inputs.redis-version, '6') && 'v2.6.0' || '' }} - submodules: recursive - - - name: Build RedisJSON - if: steps.cache-dependencies-redisjson.outputs.cache-hit != 'true' && (inputs.modules == 'all' || inputs.modules == 'json') - shell: bash - working-directory: ./redisjson - run: | - set -x - echo "Building RedisJSON..." - make - - - name: Copy redisjson.so - if: inputs.modules == 'all' || inputs.modules == 'json' - shell: bash - run: | - set -x - echo "Copying RedisJSON..." - cp $GITHUB_WORKSPACE/redisjson/bin/linux-x64-release/rejson.so $GITHUB_WORKSPACE/redisjson.so diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 4394951c67..62bab729ea 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -61,11 +61,6 @@ jobs: target: "x86_64-unknown-linux-gnu" github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install Redis Modules - uses: ./.github/workflows/install-redis-modules - with: - redis-version: ${{ matrix.redis }} - - name: test run: npm test working-directory: ./node @@ -83,10 +78,6 @@ jobs: npm ci npm run build-and-test working-directory: ./node/hybrid-node-tests/ecmascript-test - - - name: test redis modules - run: npm run test-modules -- --load-module=$GITHUB_WORKSPACE/redisjson.so - working-directory: ./node - uses: ./.github/workflows/test-benchmark with: diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index a5a6cd31d5..915937a5f9 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -99,17 +99,12 @@ jobs: pip install -r ../benchmarks/python/requirements.txt python -m mypy .. - - name: Install Redis Modules - uses: ./.github/workflows/install-redis-modules - with: - redis-version: ${{ matrix.redis }} - - name: Test with pytest working-directory: ./python run: | source .env/bin/activate cd python/tests/ - pytest --asyncio-mode=auto --override-ini=addopts= --load-module=$GITHUB_WORKSPACE/redisjson.so + pytest --asyncio-mode=auto - uses: ./.github/workflows/test-benchmark with: diff --git a/java/client/src/main/java/glide/api/BaseClient.java b/java/client/src/main/java/glide/api/BaseClient.java index 6392b8265d..ba6b594dcb 100644 --- a/java/client/src/main/java/glide/api/BaseClient.java +++ b/java/client/src/main/java/glide/api/BaseClient.java @@ -99,6 +99,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; import static redis_request.RedisRequestOuterClass.RequestType.SMembers; import static redis_request.RedisRequestOuterClass.RequestType.SMove; +import static redis_request.RedisRequestOuterClass.RequestType.SPop; import static redis_request.RedisRequestOuterClass.RequestType.SRandMember; import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SUnionStore; @@ -186,6 +187,8 @@ import glide.managers.BaseCommandResponseResolver; import glide.managers.CommandManager; import glide.managers.ConnectionManager; +import java.util.Arrays; +import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -244,7 +247,8 @@ protected static CompletableFuture CreateClient( .connectToRedis(config) .thenApply(ignore -> constructor.apply(connectionManager, commandManager)); } catch (InterruptedException e) { - // Something bad happened while we were establishing netty connection to UDS + // Something bad happened while we were establishing netty connection to + // UDS var future = new CompletableFuture(); future.completeExceptionally(e); return future; @@ -263,7 +267,8 @@ public void close() throws ExecutionException { try { connectionManager.closeConnection().get(); } catch (InterruptedException e) { - // suppressing the interrupted exception - it is already suppressed in the future + // suppressing the interrupted exception - it is already suppressed in the + // future throw new RuntimeException(e); } } @@ -295,10 +300,15 @@ protected static CommandManager buildCommandManager(ChannelHandler channelHandle * @throws RedisException On a type mismatch. */ @SuppressWarnings("unchecked") - protected T handleRedisResponse(Class classType, boolean isNullable, Response response) - throws RedisException { + protected T handleRedisResponse( + Class classType, EnumSet flags, Response response) throws RedisException { + boolean encodingUtf8 = flags.contains(ResponseFlags.ENCODING_UTF8); + boolean isNullable = flags.contains(ResponseFlags.IS_NULLABLE); Object value = - new BaseCommandResponseResolver(RedisValueResolver::valueFromPointer).apply(response); + encodingUtf8 + ? new BaseCommandResponseResolver(RedisValueResolver::valueFromPointer).apply(response) + : new BaseCommandResponseResolver(RedisValueResolver::valueFromPointerBinary) + .apply(response); if (isNullable && (value == null)) { return null; } @@ -314,43 +324,52 @@ protected T handleRedisResponse(Class classType, boolean isNullable, Resp } protected Object handleObjectOrNullResponse(Response response) throws RedisException { - return handleRedisResponse(Object.class, true, response); + return handleRedisResponse( + Object.class, EnumSet.of(ResponseFlags.IS_NULLABLE, ResponseFlags.ENCODING_UTF8), response); } protected String handleStringResponse(Response response) throws RedisException { - return handleRedisResponse(String.class, false, response); + return handleRedisResponse(String.class, EnumSet.of(ResponseFlags.ENCODING_UTF8), response); } protected String handleStringOrNullResponse(Response response) throws RedisException { - return handleRedisResponse(String.class, true, response); + return handleRedisResponse( + String.class, EnumSet.of(ResponseFlags.IS_NULLABLE, ResponseFlags.ENCODING_UTF8), response); + } + + protected byte[] handleBytesOrNullResponse(Response response) throws RedisException { + return handleRedisResponse(byte[].class, EnumSet.of(ResponseFlags.IS_NULLABLE), response); } protected Boolean handleBooleanResponse(Response response) throws RedisException { - return handleRedisResponse(Boolean.class, false, response); + return handleRedisResponse(Boolean.class, EnumSet.noneOf(ResponseFlags.class), response); } protected Long handleLongResponse(Response response) throws RedisException { - return handleRedisResponse(Long.class, false, response); + return handleRedisResponse(Long.class, EnumSet.noneOf(ResponseFlags.class), response); } protected Long handleLongOrNullResponse(Response response) throws RedisException { - return handleRedisResponse(Long.class, true, response); + return handleRedisResponse(Long.class, EnumSet.of(ResponseFlags.IS_NULLABLE), response); } protected Double handleDoubleResponse(Response response) throws RedisException { - return handleRedisResponse(Double.class, false, response); + return handleRedisResponse(Double.class, EnumSet.noneOf(ResponseFlags.class), response); } protected Double handleDoubleOrNullResponse(Response response) throws RedisException { - return handleRedisResponse(Double.class, true, response); + return handleRedisResponse(Double.class, EnumSet.of(ResponseFlags.IS_NULLABLE), response); } protected Object[] handleArrayResponse(Response response) throws RedisException { - return handleRedisResponse(Object[].class, false, response); + return handleRedisResponse(Object[].class, EnumSet.of(ResponseFlags.ENCODING_UTF8), response); } protected Object[] handleArrayOrNullResponse(Response response) throws RedisException { - return handleRedisResponse(Object[].class, true, response); + return handleRedisResponse( + Object[].class, + EnumSet.of(ResponseFlags.IS_NULLABLE, ResponseFlags.ENCODING_UTF8), + response); } /** @@ -360,7 +379,7 @@ protected Object[] handleArrayOrNullResponse(Response response) throws RedisExce */ @SuppressWarnings("unchecked") // raw Map cast to Map protected Map handleMapResponse(Response response) throws RedisException { - return handleRedisResponse(Map.class, false, response); + return handleRedisResponse(Map.class, EnumSet.of(ResponseFlags.ENCODING_UTF8), response); } /** @@ -370,12 +389,13 @@ protected Map handleMapResponse(Response response) throws RedisEx */ @SuppressWarnings("unchecked") // raw Map cast to Map protected Map handleMapOrNullResponse(Response response) throws RedisException { - return handleRedisResponse(Map.class, true, response); + return handleRedisResponse( + Map.class, EnumSet.of(ResponseFlags.IS_NULLABLE, ResponseFlags.ENCODING_UTF8), response); } @SuppressWarnings("unchecked") // raw Set cast to Set protected Set handleSetResponse(Response response) throws RedisException { - return handleRedisResponse(Set.class, false, response); + return handleRedisResponse(Set.class, EnumSet.of(ResponseFlags.ENCODING_UTF8), response); } /** Process a FUNCTION LIST standalone response. */ @@ -401,12 +421,24 @@ public CompletableFuture get(@NonNull String key) { Get, new String[] {key}, this::handleStringOrNullResponse); } + @Override + public CompletableFuture get(@NonNull byte[] key) { + return commandManager.submitNewCommand( + Get, Arrays.asList(key), this::handleBytesOrNullResponse); + } + @Override public CompletableFuture getdel(@NonNull String key) { return commandManager.submitNewCommand( GetDel, new String[] {key}, this::handleStringOrNullResponse); } + @Override + public CompletableFuture set(@NonNull byte[] key, @NonNull byte[] value) { + return commandManager.submitNewCommand( + Set, Arrays.asList(key, value), this::handleStringResponse); + } + @Override public CompletableFuture set(@NonNull String key, @NonNull String value) { return commandManager.submitNewCommand( @@ -1656,6 +1688,18 @@ public CompletableFuture srandmember(@NonNull String key, long count) SRandMember, arguments, response -> castArray(handleArrayResponse(response), String.class)); } + @Override + public CompletableFuture spop(@NonNull String key) { + String[] arguments = new String[] {key}; + return commandManager.submitNewCommand(SPop, arguments, this::handleStringOrNullResponse); + } + + @Override + public CompletableFuture> spopCount(@NonNull String key, long count) { + String[] arguments = new String[] {key, Long.toString(count)}; + return commandManager.submitNewCommand(SPop, arguments, this::handleSetResponse); + } + @Override public CompletableFuture bitfield( @NonNull String key, @NonNull BitFieldSubCommands[] subCommands) { diff --git a/java/client/src/main/java/glide/api/ResponseFlags.java b/java/client/src/main/java/glide/api/ResponseFlags.java new file mode 100644 index 0000000000..690a9ca00a --- /dev/null +++ b/java/client/src/main/java/glide/api/ResponseFlags.java @@ -0,0 +1,9 @@ +/** Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 */ +package glide.api; + +public enum ResponseFlags { + /** Strings in the response are UTF-8 encoded */ + ENCODING_UTF8, + /** Null is a valid response */ + IS_NULLABLE, +} diff --git a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java index f4abbf623d..cb17baf6ad 100644 --- a/java/client/src/main/java/glide/api/commands/SetBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/SetBaseCommands.java @@ -304,4 +304,42 @@ public interface SetBaseCommands { * } */ CompletableFuture srandmember(String key, long count); + + /** + * Removes and returns one random member from the set stored at key. + * + * @see redis.io for details. + * @param key The key of the set. + * @return The value of the popped member.
+ * If key does not exist, null will be returned. + * @example + *
{@code
+     * String value1 = client.spop("mySet").get();
+     * assert value1.equals("value1");
+     *
+     * String value2 = client.spop("nonExistingSet").get();
+     * assert value2.equals(null);
+     * }
+ */ + CompletableFuture spop(String key); + + /** + * Removes and returns up to count random members from the set stored at key + * , depending on the set's length. + * + * @see redis.io for details. + * @param key The key of the set. + * @param count The count of the elements to pop from the set. + * @return A set of popped elements will be returned depending on the set's length.
+ * If key does not exist, an empty Set will be returned. + * @example + *
{@code
+     * Set values1 = client.spopCount("mySet", 2).get();
+     * assert values1.equals(new String[] {"value1", "value2"});
+     *
+     * Set values2 = client.spopCount("nonExistingSet", 2).get();
+     * assert values2.size() == 0;
+     * }
+ */ + CompletableFuture> spopCount(String key, long count); } diff --git a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java index 6eaa4067c9..45c46a1e90 100644 --- a/java/client/src/main/java/glide/api/commands/StringBaseCommands.java +++ b/java/client/src/main/java/glide/api/commands/StringBaseCommands.java @@ -34,6 +34,25 @@ public interface StringBaseCommands { */ CompletableFuture get(String key); + /** + * Gets the value associated with the given key, or null if no such + * value exists. + * + * @see redis.io for details. + * @param key The key to retrieve from the database. + * @return Response from Redis. If key exists, returns the value of + * key as a String. Otherwise, return null. + * @example + *
{@code
+     * byte[] value = client.get("key").get();
+     * assert Arrays.equals(value, "value".getBytes());
+     *
+     * String value = client.get("non_existing_key").get();
+     * assert value.equals(null);
+     * }
+ */ + CompletableFuture get(byte[] key); + /** * Gets a string value associated with the given key and deletes the key. * @@ -67,6 +86,21 @@ public interface StringBaseCommands { */ CompletableFuture set(String key, String value); + /** + * Sets the given key with the given value. + * + * @see redis.io for details. + * @param key The key to store. + * @param value The value to store with the given key. + * @return Response from Redis containing "OK". + * @example + *
{@code
+     * String value = client.set("key".getBytes(), "value".getBytes()).get();
+     * assert value.equals("OK");
+     * }
+ */ + CompletableFuture set(byte[] key, byte[] value); + /** * Sets the given key with the given value. Return value is dependent on the passed options. * diff --git a/java/client/src/main/java/glide/api/models/BaseTransaction.java b/java/client/src/main/java/glide/api/models/BaseTransaction.java index f0e9653785..ea19d44700 100644 --- a/java/client/src/main/java/glide/api/models/BaseTransaction.java +++ b/java/client/src/main/java/glide/api/models/BaseTransaction.java @@ -120,6 +120,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; import static redis_request.RedisRequestOuterClass.RequestType.SMembers; import static redis_request.RedisRequestOuterClass.RequestType.SMove; +import static redis_request.RedisRequestOuterClass.RequestType.SPop; import static redis_request.RedisRequestOuterClass.RequestType.SRandMember; import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SUnionStore; @@ -4057,6 +4058,37 @@ public T srandmember(@NonNull String key, long count) { return getThis(); } + /** + * Removes and returns one random member from the set stored at key. + * + * @see redis.io for details. + * @param key The key of the set. + * @return Command Response - The value of the popped member.
+ * If key does not exist, null will be returned. + */ + public T spop(@NonNull String key) { + ArgsArray commandArgs = buildArgs(key); + protobufTransaction.addCommands(buildCommand(SPop, commandArgs)); + return getThis(); + } + + /** + * Removes and returns up to count random members from the set stored at key + * , depending on the set's length. + * + * @see redis.io for details. + * @param key The key of the set. + * @param count The count of the elements to pop from the set. + * @return Command Response - A set of popped elements will be returned depending on the set's + * length.
+ * If key does not exist, an empty Set will be returned. + */ + public T spopCount(@NonNull String key, long count) { + ArgsArray commandArgs = buildArgs(key, Long.toString(count)); + protobufTransaction.addCommands(buildCommand(SPop, commandArgs)); + return getThis(); + } + /** * Reads or modifies the array of bits representing the string that is held at key * based on the specified subCommands. diff --git a/java/client/src/main/java/glide/ffi/resolvers/RedisValueResolver.java b/java/client/src/main/java/glide/ffi/resolvers/RedisValueResolver.java index e1693078c8..4aaa4a3123 100644 --- a/java/client/src/main/java/glide/ffi/resolvers/RedisValueResolver.java +++ b/java/client/src/main/java/glide/ffi/resolvers/RedisValueResolver.java @@ -17,4 +17,13 @@ public class RedisValueResolver { * @return A RESP3 value */ public static native Object valueFromPointer(long pointer); + + /** + * Resolve a value received from Redis using given C-style pointer. This method does not assume + * that strings are valid UTF-8 encoded strings + * + * @param pointer A memory pointer from {@link Response} + * @return A RESP3 value + */ + public static native Object valueFromPointerBinary(long pointer); } diff --git a/java/client/src/main/java/glide/managers/CommandManager.java b/java/client/src/main/java/glide/managers/CommandManager.java index 2e2abfae15..214a819016 100644 --- a/java/client/src/main/java/glide/managers/CommandManager.java +++ b/java/client/src/main/java/glide/managers/CommandManager.java @@ -57,6 +57,23 @@ public CompletableFuture submitNewCommand( return submitCommandToChannel(command, responseHandler); } + /** + * Build a command and send. + * + * @param requestType Redis command type + * @param arguments Redis command arguments + * @param responseHandler The handler for the response object + * @return A result promise of type T + */ + public CompletableFuture submitNewCommand( + RequestType requestType, + List arguments, + RedisExceptionCheckedFunction responseHandler) { + + RedisRequest.Builder command = prepareRedisRequest(requestType, arguments); + return submitCommandToChannel(command, responseHandler); + } + /** * Build a command and send. * @@ -76,6 +93,25 @@ public CompletableFuture submitNewCommand( return submitCommandToChannel(command, responseHandler); } + /** + * Build a command and send. + * + * @param requestType Redis command type + * @param arguments Redis command arguments + * @param route Command routing parameters + * @param responseHandler The handler for the response object + * @return A result promise of type T + */ + public CompletableFuture submitNewCommand( + RequestType requestType, + List arguments, + Route route, + RedisExceptionCheckedFunction responseHandler) { + + RedisRequest.Builder command = prepareRedisRequest(requestType, arguments, route); + return submitCommandToChannel(command, responseHandler); + } + /** * Build a Transaction and send. * @@ -177,6 +213,33 @@ protected RedisRequest.Builder prepareRedisRequest( return prepareRedisRequestRoute(builder, route); } + /** + * Build a protobuf command request object with routing options. + * + * @param requestType Redis command type + * @param arguments Redis command arguments + * @param route Command routing parameters + * @return An incomplete request. {@link CallbackDispatcher} is responsible to complete it by + * adding a callback id. + */ + protected RedisRequest.Builder prepareRedisRequest( + RequestType requestType, List arguments, Route route) { + ArgsArray.Builder commandArgs = ArgsArray.newBuilder(); + for (var arg : arguments) { + commandArgs.addArgs(ByteString.copyFrom(arg)); + } + + var builder = + RedisRequest.newBuilder() + .setSingleCommand( + Command.newBuilder() + .setRequestType(requestType) + .setArgsArray(commandArgs.build()) + .build()); + + return prepareRedisRequestRoute(builder, route); + } + /** * Build a protobuf transaction request object with routing options. * @@ -247,6 +310,29 @@ protected RedisRequest.Builder prepareRedisRequest(RequestType requestType, Stri .build()); } + /** + * Build a protobuf command request object. + * + * @param requestType Redis command type + * @param arguments Redis command arguments + * @return An uncompleted request. {@link CallbackDispatcher} is responsible to complete it by + * adding a callback id. + */ + protected RedisRequest.Builder prepareRedisRequest( + RequestType requestType, List arguments) { + ArgsArray.Builder commandArgs = ArgsArray.newBuilder(); + for (var arg : arguments) { + commandArgs.addArgs(ByteString.copyFrom(arg)); + } + + return RedisRequest.newBuilder() + .setSingleCommand( + Command.newBuilder() + .setRequestType(requestType) + .setArgsArray(commandArgs.build()) + .build()); + } + private RedisRequest.Builder prepareRedisRequestRoute(RedisRequest.Builder builder, Route route) { if (route instanceof SimpleMultiNodeRoute) { diff --git a/java/client/src/test/java/glide/api/RedisClientTest.java b/java/client/src/test/java/glide/api/RedisClientTest.java index 008e7bd1dd..f0848e6a6e 100644 --- a/java/client/src/test/java/glide/api/RedisClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClientTest.java @@ -148,6 +148,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; import static redis_request.RedisRequestOuterClass.RequestType.SMembers; import static redis_request.RedisRequestOuterClass.RequestType.SMove; +import static redis_request.RedisRequestOuterClass.RequestType.SPop; import static redis_request.RedisRequestOuterClass.RequestType.SRandMember; import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SUnionStore; @@ -5520,6 +5521,55 @@ public void srandmember_with_count_returns_success() { assertArrayEquals(value, payload); } + @SneakyThrows + @Test + public void spop_returns_success() { + // setup + String key = "testKey"; + String[] arguments = new String[] {key}; + String value = "value"; + + CompletableFuture testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.submitNewCommand(eq(SPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture response = service.spop(key); + String payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + + @SneakyThrows + @Test + public void spopCount_returns_success() { + // setup + String key = "testKey"; + long count = 2; + String[] arguments = new String[] {key, Long.toString(count)}; + Set value = Set.of("one", "two"); + + CompletableFuture> testResponse = new CompletableFuture<>(); + testResponse.complete(value); + + // match on protobuf request + when(commandManager.>submitNewCommand(eq(SPop), eq(arguments), any())) + .thenReturn(testResponse); + + // exercise + CompletableFuture> response = service.spopCount(key, count); + Set payload = response.get(); + + // verify + assertEquals(testResponse, response); + assertEquals(value, payload); + } + @SneakyThrows @Test public void bitfieldReadOnly_returns_success() { diff --git a/java/client/src/test/java/glide/api/RedisClusterClientTest.java b/java/client/src/test/java/glide/api/RedisClusterClientTest.java index 1a32dd7669..94bdb36c48 100644 --- a/java/client/src/test/java/glide/api/RedisClusterClientTest.java +++ b/java/client/src/test/java/glide/api/RedisClusterClientTest.java @@ -42,6 +42,7 @@ import glide.managers.CommandManager; import glide.managers.ConnectionManager; import glide.managers.RedisExceptionCheckedFunction; +import java.util.EnumSet; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -141,7 +142,8 @@ public TestClient(CommandManager commandManager, Object objectToReturn) { } @Override - protected T handleRedisResponse(Class classType, boolean isNullable, Response response) { + protected T handleRedisResponse( + Class classType, EnumSet flags, Response response) { @SuppressWarnings("unchecked") T returnValue = (T) object; return returnValue; diff --git a/java/client/src/test/java/glide/api/models/TransactionTests.java b/java/client/src/test/java/glide/api/models/TransactionTests.java index e887b2e06a..11a23ee584 100644 --- a/java/client/src/test/java/glide/api/models/TransactionTests.java +++ b/java/client/src/test/java/glide/api/models/TransactionTests.java @@ -130,6 +130,7 @@ import static redis_request.RedisRequestOuterClass.RequestType.SMIsMember; import static redis_request.RedisRequestOuterClass.RequestType.SMembers; import static redis_request.RedisRequestOuterClass.RequestType.SMove; +import static redis_request.RedisRequestOuterClass.RequestType.SPop; import static redis_request.RedisRequestOuterClass.RequestType.SRandMember; import static redis_request.RedisRequestOuterClass.RequestType.SRem; import static redis_request.RedisRequestOuterClass.RequestType.SUnionStore; @@ -903,6 +904,12 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)), transaction.srandmember("key", 1); results.add(Pair.of(SRandMember, buildArgs("key", "1"))); + transaction.spop("key"); + results.add(Pair.of(SPop, buildArgs("key"))); + + transaction.spopCount("key", 1); + results.add(Pair.of(SPop, buildArgs("key", "1"))); + transaction.bitfieldReadOnly( "key", new BitFieldReadOnlySubCommands[] {new BitFieldGet(new SignedEncoding(5), new Offset(3))}); diff --git a/java/integTest/src/test/java/glide/SharedCommandTests.java b/java/integTest/src/test/java/glide/SharedCommandTests.java index 2064cc8087..f69fe8a425 100644 --- a/java/integTest/src/test/java/glide/SharedCommandTests.java +++ b/java/integTest/src/test/java/glide/SharedCommandTests.java @@ -260,7 +260,7 @@ public void set_requires_a_key(BaseClient client) { @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") public void get_requires_a_key(BaseClient client) { - assertThrows(NullPointerException.class, () -> client.get(null)); + assertThrows(NullPointerException.class, () -> client.get((String) null)); } @SneakyThrows @@ -319,6 +319,17 @@ public void set_only_if_does_not_exists_missing_key(BaseClient client) { assertEquals(ANOTHER_VALUE, data); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void set_get_binary_data(BaseClient client) { + byte[] key = "set_get_binary_data_key".getBytes(); + byte[] value = {(byte) 0x01, (byte) 0x00, (byte) 0x01, (byte) 0x00, (byte) 0x02}; + assert client.set(key, value).get().equals("OK"); + byte[] data = client.get(key).get(); + assert Arrays.equals(data, value); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") @@ -4586,6 +4597,45 @@ public void srandmember(BaseClient client) { assertInstanceOf(RequestException.class, executionExceptionWithCount.getCause()); } + @SneakyThrows + @ParameterizedTest(autoCloseArguments = false) + @MethodSource("getClients") + public void spop_spopCount(BaseClient client) { + String key = UUID.randomUUID().toString(); + String stringKey = UUID.randomUUID().toString(); + String nonExistingKey = UUID.randomUUID().toString(); + String member1 = UUID.randomUUID().toString(); + String member2 = UUID.randomUUID().toString(); + String member3 = UUID.randomUUID().toString(); + + assertEquals(1, client.sadd(key, new String[] {member1}).get()); + assertEquals(member1, client.spop(key).get()); + + assertEquals(3, client.sadd(key, new String[] {member1, member2, member3}).get()); + // Pop with count value greater than the size of the set + assertEquals(Set.of(member1, member2, member3), client.spopCount(key, 4).get()); + assertEquals(0, client.scard(key).get()); + + assertEquals(3, client.sadd(key, new String[] {member1, member2, member3}).get()); + assertEquals(Set.of(), client.spopCount(key, 0).get()); + + assertNull(client.spop(nonExistingKey).get()); + assertEquals(Set.of(), client.spopCount(nonExistingKey, 3).get()); + + // invalid argument - count must be positive + ExecutionException executionException = + assertThrows(ExecutionException.class, () -> client.spopCount(key, -1).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + + // key exists but is not a set + assertEquals(OK, client.set(stringKey, "foo").get()); + executionException = assertThrows(ExecutionException.class, () -> client.spop(stringKey).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + executionException = + assertThrows(ExecutionException.class, () -> client.spopCount(stringKey, 3).get()); + assertInstanceOf(RequestException.class, executionException.getCause()); + } + @SneakyThrows @ParameterizedTest(autoCloseArguments = false) @MethodSource("getClients") diff --git a/java/integTest/src/test/java/glide/TransactionTestUtilities.java b/java/integTest/src/test/java/glide/TransactionTestUtilities.java index b847d39635..08abd14ae6 100644 --- a/java/integTest/src/test/java/glide/TransactionTestUtilities.java +++ b/java/integTest/src/test/java/glide/TransactionTestUtilities.java @@ -396,7 +396,9 @@ private static Object[] setCommands(BaseTransaction transaction) { .sadd(setKey4, new String[] {"foo"}) .srandmember(setKey4) .srandmember(setKey4, 2) - .srandmember(setKey4, -2); + .srandmember(setKey4, -2) + .spop(setKey4) + .spopCount(setKey4, 3); // setKey4 is now empty if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { transaction @@ -425,6 +427,8 @@ private static Object[] setCommands(BaseTransaction transaction) { "foo", // srandmember(setKey4) new String[] {"foo"}, // srandmember(setKey4, 2) new String[] {"foo", "foo"}, // srandmember(setKey4, -2)}; + "foo", // spop(setKey4) + Set.of(), // spopCount(setKey4, 3) }; if (REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0")) { expectedResults = diff --git a/java/src/lib.rs b/java/src/lib.rs index eb81b165f1..9d42b8e298 100644 --- a/java/src/lib.rs +++ b/java/src/lib.rs @@ -16,7 +16,11 @@ mod ffi_test; pub use ffi_test::*; // TODO: Consider caching method IDs here in a static variable (might need RwLock to mutate) -fn redis_value_to_java<'local>(env: &mut JNIEnv<'local>, val: Value) -> JObject<'local> { +fn redis_value_to_java<'local>( + env: &mut JNIEnv<'local>, + val: Value, + encoding_utf8: bool, +) -> JObject<'local> { match val { Value::Nil => JObject::null(), Value::SimpleString(str) => JObject::from(env.new_string(str).unwrap()), @@ -24,20 +28,37 @@ fn redis_value_to_java<'local>(env: &mut JNIEnv<'local>, val: Value) -> JObject< Value::Int(num) => env .new_object("java/lang/Long", "(J)V", &[num.into()]) .unwrap(), - Value::BulkString(data) => match std::str::from_utf8(data.as_ref()) { - Ok(val) => JObject::from(env.new_string(val).unwrap()), - Err(_err) => { - let _ = env.throw("Error decoding Unicode data"); - JObject::null() + Value::BulkString(data) => { + if encoding_utf8 { + let Ok(utf8_str) = String::from_utf8(data) else { + let _ = env.throw("Failed to construct UTF-8 string"); + return JObject::null(); + }; + match env.new_string(utf8_str) { + Ok(string) => JObject::from(string), + Err(e) => { + let _ = env.throw(format!( + "Failed to construct Java UTF-8 string from Rust UTF-8 string. {:?}", + e + )); + JObject::null() + } + } + } else { + let Ok(bytearr) = env.byte_array_from_slice(data.as_ref()) else { + let _ = env.throw("Failed to allocate byte array"); + return JObject::null(); + }; + bytearr.into() } - }, + } Value::Array(array) => { let items: JObjectArray = env .new_object_array(array.len() as i32, "java/lang/Object", JObject::null()) .unwrap(); for (i, item) in array.into_iter().enumerate() { - let java_value = redis_value_to_java(env, item); + let java_value = redis_value_to_java(env, item, encoding_utf8); env.set_object_array_element(&items, i as i32, java_value) .unwrap(); } @@ -50,8 +71,8 @@ fn redis_value_to_java<'local>(env: &mut JNIEnv<'local>, val: Value) -> JObject< .unwrap(); for (key, value) in map { - let java_key = redis_value_to_java(env, key); - let java_value = redis_value_to_java(env, value); + let java_key = redis_value_to_java(env, key, encoding_utf8); + let java_value = redis_value_to_java(env, value, encoding_utf8); env.call_method( &linked_hash_map, "put", @@ -75,7 +96,7 @@ fn redis_value_to_java<'local>(env: &mut JNIEnv<'local>, val: Value) -> JObject< let set = env.new_object("java/util/HashSet", "()V", &[]).unwrap(); for elem in array { - let java_value = redis_value_to_java(env, elem); + let java_value = redis_value_to_java(env, elem, encoding_utf8); env.call_method( &set, "add", @@ -102,7 +123,19 @@ pub extern "system" fn Java_glide_ffi_resolvers_RedisValueResolver_valueFromPoin pointer: jlong, ) -> JObject<'local> { let value = unsafe { Box::from_raw(pointer as *mut Value) }; - redis_value_to_java(&mut env, *value) + redis_value_to_java(&mut env, *value, true) +} + +#[no_mangle] +pub extern "system" fn Java_glide_ffi_resolvers_RedisValueResolver_valueFromPointerBinary< + 'local, +>( + mut env: JNIEnv<'local>, + _class: JClass<'local>, + pointer: jlong, +) -> JObject<'local> { + let value = unsafe { Box::from_raw(pointer as *mut Value) }; + redis_value_to_java(&mut env, *value, false) } #[no_mangle] diff --git a/node/.prettierignore b/node/.prettierignore index 086fea31e1..6ced842400 100644 --- a/node/.prettierignore +++ b/node/.prettierignore @@ -1,5 +1,6 @@ # ignore that dir, because there are a lot of files which we don't manage, e.g. json files in cargo crates rust-client/* +*.md # unignore specific files !rust-client/package.json !rust-client/tsconfig.json diff --git a/node/THIRD_PARTY_LICENSES_NODE b/node/THIRD_PARTY_LICENSES_NODE index 002dfc0ce7..f3279da7de 100644 --- a/node/THIRD_PARTY_LICENSES_NODE +++ b/node/THIRD_PARTY_LICENSES_NODE @@ -36860,7 +36860,7 @@ THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- -Package: @types:node:20.14.0 +Package: @types:node:20.14.1 The following copyrights and licenses were found in the source code of this package: diff --git a/node/package.json b/node/package.json index 581a5ce986..7cecbb624b 100644 --- a/node/package.json +++ b/node/package.json @@ -36,7 +36,6 @@ "build-test-utils": "cd ../utils && npm i && npm run build", "lint": "eslint -f unix \"src/**/*.{ts,tsx}\"", "prepack": "npmignore --auto", - "test-modules": "jest --verbose --runInBand 'tests/RedisModules.test.ts'", "prettier:check:ci": "./node_modules/.bin/prettier --check . --ignore-unknown '!**/*.{js,d.ts}'", "prettier:format": "./node_modules/.bin/prettier --write . --ignore-unknown '!**/*.{js,d.ts}'" }, diff --git a/node/tests/RedisModules.test.ts b/node/tests/RedisModules.test.ts deleted file mode 100644 index 67643885b0..0000000000 --- a/node/tests/RedisModules.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Copyright GLIDE-for-Redis Project Contributors - SPDX Identifier: Apache-2.0 - */ - -import { - afterAll, - afterEach, - beforeAll, - describe, - expect, - it, -} from "@jest/globals"; -import { - BaseClientConfiguration, - InfoOptions, - RedisClusterClient, - parseInfoResponse, -} from "../"; -import { RedisCluster } from "../../utils/TestUtils.js"; -import { runBaseTests } from "./SharedTests"; -import { flushallOnPort, getFirstResult } from "./TestUtilities"; - -type Context = { - client: RedisClusterClient; -}; - -const TIMEOUT = 10000; - -describe("RedisModules", () => { - let testsFailed = 0; - let cluster: RedisCluster; - beforeAll(async () => { - const args = process.argv.slice(2); - const loadModuleArgs = args.filter((arg) => - arg.startsWith("--load-module="), - ); - const loadModuleValues = loadModuleArgs.map((arg) => arg.split("=")[1]); - cluster = await RedisCluster.createCluster( - true, - 3, - 0, - loadModuleValues, - ); - }, 20000); - - afterEach(async () => { - await Promise.all(cluster.ports().map((port) => flushallOnPort(port))); - }); - - afterAll(async () => { - if (testsFailed === 0) { - await cluster.close(); - } - }); - - const getOptions = (ports: number[]): BaseClientConfiguration => { - return { - addresses: ports.map((port) => ({ - host: "localhost", - port, - })), - }; - }; - - runBaseTests({ - init: async (protocol, clientName) => { - const options = getOptions(cluster.ports()); - options.protocol = protocol; - options.clientName = clientName; - testsFailed += 1; - const client = await RedisClusterClient.createClient(options); - return { - context: { - client, - }, - client, - }; - }, - close: (context: Context, testSucceeded: boolean) => { - if (testSucceeded) { - testsFailed -= 1; - } - - context.client.close(); - }, - timeout: TIMEOUT, - }); - - it("simple json test", async () => { - const client = await RedisClusterClient.createClient( - getOptions(cluster.ports()), - ); - const info = parseInfoResponse( - getFirstResult(await client.info([InfoOptions.Modules])).toString(), - )["module"]; - expect(info).toEqual(expect.stringContaining("ReJSON")); - client.close(); - }); -}); diff --git a/python/THIRD_PARTY_LICENSES_PYTHON b/python/THIRD_PARTY_LICENSES_PYTHON index d570bbe26b..a3fd27ffd3 100644 --- a/python/THIRD_PARTY_LICENSES_PYTHON +++ b/python/THIRD_PARTY_LICENSES_PYTHON @@ -37724,7 +37724,7 @@ The following copyrights and licenses were found in the source code of this pack ---- -Package: googleapis-common-protos:1.63.0 +Package: googleapis-common-protos:1.63.1 The following copyrights and licenses were found in the source code of this package: