Skip to content

Commit

Permalink
Added transaction supports for DUMP, RESTORE, FUNCTION DUMP, FUNCTION…
Browse files Browse the repository at this point in the history
… RESTORE (valkey-io#1814)

* Added transaction supports for DUMP, RESTORE, FUNCITON DUMP, and FUNCTION RESTORE commands

* Added Python parts of supports

* Added test cleanup after Python function dump/restore transaction test

* Fixed Too many arguments for exec check

* Fixed CI issues

* Added comments to Python's test_transaction
Added RESTORE and FUNCTION RESTORE to Java's TransactionTests

* Updated CHANGELOG

* Added Java integTest

* Addressed review comments and renamed transaction[1|2] to transaction

* Removed unnecessary calls of function_list in the tests
  • Loading branch information
yipin-chen authored Jul 5, 2024
1 parent 11593a2 commit 262d7fd
Show file tree
Hide file tree
Showing 7 changed files with 383 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
* Python: Added FUNCTION DUMP and FUNCTION RESTORE commands ([#1769](https://github.com/aws/glide-for-redis/pull/1769))
* Python: Added FUNCTION STATS command ([#1794](https://github.com/aws/glide-for-redis/pull/1794))
* Python: Added XINFO STREAM command ([#1816](https://github.com/aws/glide-for-redis/pull/1816))
* Python: Added transaction supports for DUMP, RESTORE, FUNCTION DUMP and FUNCTION RESTORE ([#1814](https://github.com/aws/glide-for-redis/pull/1814))

### Breaking Changes
* Node: Update XREAD to return a Map of Map ([#1494](https://github.com/aws/glide-for-redis/pull/1494))
Expand Down
113 changes: 113 additions & 0 deletions java/client/src/main/java/glide/api/models/BaseTransaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.Decr;
import static redis_request.RedisRequestOuterClass.RequestType.DecrBy;
import static redis_request.RedisRequestOuterClass.RequestType.Del;
import static redis_request.RedisRequestOuterClass.RequestType.Dump;
import static redis_request.RedisRequestOuterClass.RequestType.Echo;
import static redis_request.RedisRequestOuterClass.RequestType.Exists;
import static redis_request.RedisRequestOuterClass.RequestType.Expire;
Expand All @@ -59,9 +60,11 @@
import static redis_request.RedisRequestOuterClass.RequestType.FlushAll;
import static redis_request.RedisRequestOuterClass.RequestType.FlushDB;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionDump;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionList;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionRestore;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionStats;
import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd;
import static redis_request.RedisRequestOuterClass.RequestType.GeoDist;
Expand Down Expand Up @@ -132,6 +135,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.RandomKey;
import static redis_request.RedisRequestOuterClass.RequestType.Rename;
import static redis_request.RedisRequestOuterClass.RequestType.RenameNX;
import static redis_request.RedisRequestOuterClass.RequestType.Restore;
import static redis_request.RedisRequestOuterClass.RequestType.SAdd;
import static redis_request.RedisRequestOuterClass.RequestType.SCard;
import static redis_request.RedisRequestOuterClass.RequestType.SDiff;
Expand Down Expand Up @@ -227,6 +231,7 @@
import glide.api.models.commands.RangeOptions.ScoreBoundary;
import glide.api.models.commands.RangeOptions.ScoreRange;
import glide.api.models.commands.RangeOptions.ScoredRangeQuery;
import glide.api.models.commands.RestoreOptions;
import glide.api.models.commands.ScoreFilter;
import glide.api.models.commands.SetOptions;
import glide.api.models.commands.SetOptions.ConditionalSet;
Expand All @@ -250,6 +255,7 @@
import glide.api.models.commands.bitmap.BitFieldOptions.OffsetMultiplier;
import glide.api.models.commands.bitmap.BitmapIndexType;
import glide.api.models.commands.bitmap.BitwiseOperation;
import glide.api.models.commands.function.FunctionRestorePolicy;
import glide.api.models.commands.geospatial.GeoAddOptions;
import glide.api.models.commands.geospatial.GeoSearchOptions;
import glide.api.models.commands.geospatial.GeoSearchOrigin;
Expand Down Expand Up @@ -4865,6 +4871,70 @@ public <ArgType> T copy(@NonNull ArgType source, @NonNull ArgType destination) {
return copy(source, destination, false);
}

/**
* Serialize the value stored at <code>key</code> in a Valkey-specific format and return it to the
* user.
*
* @implNote ArgType is limited to String or GlideString, any other type will throw
* IllegalArgumentException
* @see <a href="https://valkey.io/commands/dump/">valkey.io</a> for details.
* @param key The key of the set.
* @return Command Response - The serialized value of a set. If <code>key</code> does not exist,
* <code>null</code> will be returned.
*/
public <ArgType> T dump(@NonNull ArgType key) {
checkTypeOrThrow(key);
protobufTransaction.addCommands(buildCommand(Dump, newArgsBuilder().add(key)));
return getThis();
}

/**
* Create a <code>key</code> associated with a <code>value</code> that is obtained by
* deserializing the provided serialized <code>value</code> (obtained via {@link #dump}).
*
* @implNote ArgType is limited to String or GlideString, any other type will throw
* IllegalArgumentException
* @see <a href="https://valkey.io/commands/restore/">valkey.io</a> for details.
* @param key The key of the set.
* @param ttl The expiry time (in milliseconds). If <code>0</code>, the <code>key</code> will
* persist.
* @param value The serialized value.
* @return Command Response - Return <code>OK</code> if successfully create a <code>key</code>
* with a <code>value</code>.
*/
public <ArgType> T restore(@NonNull ArgType key, long ttl, @NonNull byte[] value) {
checkTypeOrThrow(key);
protobufTransaction.addCommands(
buildCommand(Restore, newArgsBuilder().add(key).add(ttl).add(value)));
return getThis();
}

/**
* Create a <code>key</code> associated with a <code>value</code> that is obtained by
* deserializing the provided serialized <code>value</code> (obtained via {@link #dump}).
*
* @implNote ArgType is limited to String or GlideString, any other type will throw
* IllegalArgumentException
* @see <a href="https://valkey.io/commands/restore/">valkey.io</a> for details.
* @param key The key of the set.
* @param ttl The expiry time (in milliseconds). If <code>0</code>, the <code>key</code> will
* persist.
* @param value The serialized value.
* @param restoreOptions The restore options. See {@link RestoreOptions}.
* @return Command Response - Return <code>OK</code> if successfully create a <code>key</code>
* with a <code>value</code>.
*/
public <ArgType> T restore(
@NonNull ArgType key,
long ttl,
@NonNull byte[] value,
@NonNull RestoreOptions restoreOptions) {
checkTypeOrThrow(key);
protobufTransaction.addCommands(
buildCommand(Restore, newArgsBuilder().add(key).add(ttl).add(value).add(restoreOptions)));
return getThis();
}

/**
* Counts the number of set bits (population counting) in a string stored at <code>key</code>.
*
Expand Down Expand Up @@ -5229,6 +5299,49 @@ public T functionStats() {
return getThis();
}

/**
* Returns the serialized payload of all loaded libraries. The command will be routed to a random
* node.
*
* @since Redis 7.0 and above.
* @see <a href="https://valkey.io/commands/function-dump/">valkey.io</a> for details.
* @return Command Response - The serialized payload of all loaded libraries.
*/
public T functionDump() {
protobufTransaction.addCommands(buildCommand(FunctionDump));
return getThis();
}

/**
* Restores libraries from the serialized payload returned by {@link #functionDump()}. The command
* will be routed to all primary nodes.
*
* @since Redis 7.0 and above.
* @see <a href="https://valkey.io/commands/function-restore/">valkey.io</a> for details.
* @param payload The serialized data from {@link #functionDump()}.
* @return Command Response - <code>OK</code>.
*/
public T functionRestore(@NonNull byte[] payload) {
protobufTransaction.addCommands(buildCommand(FunctionRestore, newArgsBuilder().add(payload)));
return getThis();
}

/**
* Restores libraries from the serialized payload returned by {@link #functionDump()}. The command
* will be routed to all primary nodes.
*
* @since Redis 7.0 and above.
* @see <a href="https://valkey.io/commands/function-restore/">valkey.io</a> for details.
* @param payload The serialized data from {@link #functionDump()}.
* @param policy A policy for handling existing libraries.
* @return Command Response - <code>OK</code>.
*/
public T functionRestore(@NonNull byte[] payload, @NonNull FunctionRestorePolicy policy) {
protobufTransaction.addCommands(
buildCommand(FunctionRestore, newArgsBuilder().add(payload).add(policy)));
return getThis();
}

/**
* Sets or clears the bit at <code>offset</code> in the string value stored at <code>key</code>.
* The <code>offset</code> is a zero-based index, with <code>0</code> being the first element of
Expand Down
16 changes: 16 additions & 0 deletions java/client/src/test/java/glide/api/models/TransactionTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.Decr;
import static redis_request.RedisRequestOuterClass.RequestType.DecrBy;
import static redis_request.RedisRequestOuterClass.RequestType.Del;
import static redis_request.RedisRequestOuterClass.RequestType.Dump;
import static redis_request.RedisRequestOuterClass.RequestType.Echo;
import static redis_request.RedisRequestOuterClass.RequestType.Exists;
import static redis_request.RedisRequestOuterClass.RequestType.Expire;
Expand All @@ -85,9 +86,11 @@
import static redis_request.RedisRequestOuterClass.RequestType.FlushAll;
import static redis_request.RedisRequestOuterClass.RequestType.FlushDB;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionDelete;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionDump;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionFlush;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionList;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionLoad;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionRestore;
import static redis_request.RedisRequestOuterClass.RequestType.FunctionStats;
import static redis_request.RedisRequestOuterClass.RequestType.GeoAdd;
import static redis_request.RedisRequestOuterClass.RequestType.GeoDist;
Expand Down Expand Up @@ -158,6 +161,7 @@
import static redis_request.RedisRequestOuterClass.RequestType.RandomKey;
import static redis_request.RedisRequestOuterClass.RequestType.Rename;
import static redis_request.RedisRequestOuterClass.RequestType.RenameNX;
import static redis_request.RedisRequestOuterClass.RequestType.Restore;
import static redis_request.RedisRequestOuterClass.RequestType.SAdd;
import static redis_request.RedisRequestOuterClass.RequestType.SCard;
import static redis_request.RedisRequestOuterClass.RequestType.SDiff;
Expand Down Expand Up @@ -1100,6 +1104,12 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)),
results.add(Pair.of(FunctionList, buildArgs(WITH_CODE_REDIS_API)));
results.add(Pair.of(FunctionList, buildArgs(LIBRARY_NAME_REDIS_API, "*")));

transaction.functionDump();
results.add(Pair.of(FunctionDump, buildArgs()));

transaction.functionRestore("TEST".getBytes());
results.add(Pair.of(FunctionRestore, buildArgs("TEST")));

transaction.fcall("func", new String[] {"key1", "key2"}, new String[] {"arg1", "arg2"});
results.add(Pair.of(FCall, buildArgs("func", "2", "key1", "key2", "arg1", "arg2")));
transaction.fcall("func", new String[] {"arg1", "arg2"});
Expand Down Expand Up @@ -1218,6 +1228,12 @@ InfScoreBound.NEGATIVE_INFINITY, new ScoreBoundary(3, false), new Limit(1, 2)),
transaction.copy("key1", "key2", true);
results.add(Pair.of(Copy, buildArgs("key1", "key2", REPLACE_REDIS_API)));

transaction.dump("key1");
results.add(Pair.of(Dump, buildArgs("key1")));

transaction.restore("key2", 0, "TEST".getBytes());
results.add(Pair.of(Restore, buildArgs("key2", "0", "TEST")));

transaction.lcs("key1", "key2");
results.add(Pair.of(LCS, buildArgs("key1", "key2")));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import static glide.TestConfiguration.REDIS_VERSION;
import static glide.TestUtilities.assertDeepEquals;
import static glide.TestUtilities.generateLuaLibCode;
import static glide.api.BaseClient.OK;
import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC;
import static glide.api.models.configuration.RequestRoutingConfiguration.SimpleMultiNodeRoute.ALL_PRIMARIES;
Expand All @@ -18,7 +19,9 @@
import glide.TransactionTestUtilities.TransactionBuilder;
import glide.api.RedisClusterClient;
import glide.api.models.ClusterTransaction;
import glide.api.models.GlideString;
import glide.api.models.commands.SortClusterOptions;
import glide.api.models.commands.function.FunctionRestorePolicy;
import glide.api.models.configuration.NodeAddress;
import glide.api.models.configuration.RedisClusterClientConfiguration;
import glide.api.models.configuration.RequestRoutingConfiguration.SingleNodeRoute;
Expand Down Expand Up @@ -337,4 +340,32 @@ public void waitTest() {
assertEquals(expectedResult[0], results[0]);
assertTrue((Long) expectedResult[1] <= (Long) results[1]);
}

@Test
@SneakyThrows
public void test_transaction_function_dump_restore() {
assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"));
String libName = "mylib";
String funcName = "myfun";
String code = generateLuaLibCode(libName, Map.of(funcName, "return args[1]"), true);

// Setup
clusterClient.functionLoad(code, true).get();

// Verify functionDump
ClusterTransaction transaction = new ClusterTransaction();
transaction.withBinarySafeOutput();
transaction.functionDump();
Object[] result = clusterClient.exec(transaction).get();
GlideString payload = (GlideString) (result[0]);

// Verify functionRestore
transaction = new ClusterTransaction();
transaction.functionRestore(payload.getBytes(), FunctionRestorePolicy.REPLACE);
// For the cluster mode, PRIMARY SlotType is required to avoid the error:
// "RequestError: An error was signalled by the server -
// ReadOnly: You can't write against a read only replica."
Object[] response = clusterClient.exec(transaction, new SlotIdRoute(1, SlotType.PRIMARY)).get();
assertEquals(OK, response[0]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static glide.TestConfiguration.REDIS_VERSION;
import static glide.TestUtilities.assertDeepEquals;
import static glide.TestUtilities.commonClientConfig;
import static glide.TestUtilities.generateLuaLibCode;
import static glide.api.BaseClient.OK;
import static glide.api.models.GlideString.gs;
import static glide.api.models.commands.SortBaseOptions.OrderBy.DESC;
Expand All @@ -29,6 +30,7 @@
import glide.api.models.Transaction;
import glide.api.models.commands.InfoOptions;
import glide.api.models.commands.SortOptions;
import glide.api.models.commands.function.FunctionRestorePolicy;
import glide.api.models.commands.scan.ScanOptions;
import glide.api.models.exceptions.RequestException;
import java.time.Instant;
Expand Down Expand Up @@ -603,4 +605,55 @@ public void scan_with_options_test() {
"Unable to find " + typeKeys.get(type) + " in a scan by match pattern");
}
}

@Test
@SneakyThrows
public void test_transaction_dump_restore() {
GlideString key1 = gs("{key}-1" + UUID.randomUUID());
GlideString key2 = gs("{key}-2" + UUID.randomUUID());
String value = UUID.randomUUID().toString();

// Setup
assertEquals(OK, client.set(key1, gs(value)).get());

// Verify dump
Transaction transaction = new Transaction();
transaction.withBinarySafeOutput();
transaction.dump(key1);
Object[] result = client.exec(transaction).get();
GlideString payload = (GlideString) (result[0]);

// Verify restore
transaction = new Transaction();
transaction.restore(key2, 0, payload.getBytes());
transaction.get(key2);
Object[] response = client.exec(transaction).get();
assertEquals(OK, response[0]);
assertEquals(value, response[1]);
}

@Test
@SneakyThrows
public void test_transaction_function_dump_restore() {
assumeTrue(REDIS_VERSION.isGreaterThanOrEqualTo("7.0.0"));
String libName = "mylib";
String funcName = "myfun";
String code = generateLuaLibCode(libName, Map.of(funcName, "return args[1]"), true);

// Setup
client.functionLoad(code, true).get();

// Verify functionDump
Transaction transaction = new Transaction();
transaction.withBinarySafeOutput();
transaction.functionDump();
Object[] result = client.exec(transaction).get();
GlideString payload = (GlideString) (result[0]);

// Verify functionRestore
transaction = new Transaction();
transaction.functionRestore(payload.getBytes(), FunctionRestorePolicy.REPLACE);
Object[] response = client.exec(transaction).get();
assertEquals(OK, response[0]);
}
}
Loading

0 comments on commit 262d7fd

Please sign in to comment.