Skip to content

Commit

Permalink
support LMPOP, LMOVE and BLMOVE commands
Browse files Browse the repository at this point in the history
  • Loading branch information
sewenew committed Oct 6, 2024
1 parent 90e3267 commit 87d7690
Show file tree
Hide file tree
Showing 19 changed files with 489 additions and 4 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,9 +318,9 @@ Visual Studio 2019 (Win 10)

If you build *redis-plus-plus* with `-DREDIS_PLUS_PLUS_BUILD_TEST=ON` (the default behavior, and you can disable building test with `-DREDIS_PLUS_PLUS_BUILD_TEST=OFF`), you'll get a test program in *build/test* directory: *build/test/test_redis++*.

In order to run the tests, you need to set up a Redis instance, and a Redis Cluster. Since the test program will send most of Redis commands to the server and cluster, you need to set up Redis of the latest version (by now, it's 5.0). Otherwise, the tests might fail. For example, if you set up Redis 4.0 for testing, the test program will fail when it tries to send the `ZPOPMAX` command (a Redis 5.0 command) to the server. If you want to run the tests with other Redis versions, you have to comment out commands that haven't been supported by your Redis, from test source files in *redis-plus-plus/test/src/sw/redis++/* directory. Sorry for the inconvenience, and I'll fix this problem to make the test program work with any version of Redis in the future.
In order to run the tests, you need to set up a Redis instance, and a Redis Cluster. Since the test program will send most of Redis commands to the server and cluster, you need to set up Redis of the latest version. Otherwise, the tests might fail. For example, if you set up Redis 4.0 for testing, the test program will fail when it tries to send the `ZPOPMAX` command (a Redis 5.0 command) to the server. If you want to run the tests with other Redis versions, you have to comment out commands that haven't been supported by your Redis, from test source files in *redis-plus-plus/test/src/sw/redis++/* directory. Sorry for the inconvenience, and I'll fix this problem to make the test program work with any version of Redis in the future.

**NOTE**: The latest version of Redis is only a requirement for running the tests. In fact, you can use *redis-plus-plus* with Redis of any version, e.g. Redis 2.0, Redis 3.0, Redis 4.0, Redis 5.0.
**NOTE**: The latest version of Redis is only a requirement for running the tests. In fact, you can use *redis-plus-plus* with Redis of any version, i.e. Redis 2.0 and above.

**NEVER** run the test program in production envronment, since the keys, which the test program reads or writes, might conflict with your application.

Expand Down
54 changes: 54 additions & 0 deletions src/sw/redis++/async_redis.h
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,60 @@ class AsyncRedis {
return rpush(key, il.begin(), il.end(), std::forward<Callback>(cb));
}

template <typename Output, typename Input>
Future<Optional<std::pair<std::string, Output>>> lmpop(Input first, Input last, ListWhence whence, long long count = 1) {
range_check("LMPOP", first, last);

return _command<Optional<std::pair<std::string, Output>>>(fmt::lmpop<Input>, first, last, whence, count);
}

template <typename Output, typename Input, typename Callback>
auto lmpop(Input first, Input last, ListWhence whence, long long count, Callback &&cb)
-> typename std::enable_if<IsInvocable<typename std::decay<Callback>::type, Future<Optional<std::pair<std::string, Output>>> &&>::value, void>::type {
range_check("LMPOP", first, last);

_callback_fmt_command<Optional<std::pair<std::string, Output>>>(std::forward<Callback>(cb),
fmt::lmpop<Input>, first, last, whence, count);
}

template <typename Output, typename Input, typename Callback>
auto lmpop(Input first, Input last, ListWhence whence, Callback &&cb)
-> typename std::enable_if<IsInvocable<typename std::decay<Callback>::type, Future<Optional<std::pair<std::string, Output>>> &&>::value, void>::type {
return lmpop<Output>(first, last, whence, 1, std::forward<Callback>(cb));
}

Future<OptionalString> lmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence) {
return _command<OptionalString>(fmt::lmove, src, dest, src_whence, dest_whence);
}

template <typename Callback>
void lmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence, Callback &&cb) {
_callback_fmt_command<OptionalString>(std::forward<Callback>(cb), fmt::lmove, src, dest, src_whence, dest_whence);
}

Future<OptionalString> blmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence,
const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
return _command<OptionalString>(fmt::blmove, src, dest, src_whence, dest_whence, timeout.count());
}

template <typename Callback>
void blmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence,
const std::chrono::seconds &timeout, Callback &&cb) {
_callback_fmt_command<OptionalString>(std::forward<Callback>(cb), fmt::blmove, src, dest,
src_whence, dest_whence, timeout.count());
}

template <typename Callback>
auto blmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence, Callback &&cb)
-> typename std::enable_if<IsInvocable<typename std::decay<Callback>::type, Future<OptionalString> &&>::value, void>::type {
blmove(src, dest, src_whence, dest_whence, std::chrono::seconds{0}, std::forward<Callback>(cb));
}

// HASH commands.

Future<long long> hdel(const StringView &key, const StringView &field) {
Expand Down
54 changes: 54 additions & 0 deletions src/sw/redis++/async_redis_cluster.h
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,60 @@ class AsyncRedisCluster {
return rpush(key, il.begin(), il.end());
}

template <typename Output, typename Input>
Future<Optional<std::pair<std::string, Output>>> lmpop(Input first, Input last, ListWhence whence, long long count = 1) {
range_check("LMPOP", first, last);

return _command<Optional<std::pair<std::string, Output>>>(fmt::lmpop<Input>, first, last, whence, count);
}

template <typename Output, typename Input, typename Callback>
auto lmpop(Input first, Input last, ListWhence whence, long long count, Callback &&cb)
-> typename std::enable_if<IsInvocable<typename std::decay<Callback>::type, Future<Optional<std::pair<std::string, Output>>> &&>::value, void>::type {
range_check("LMPOP", first, last);

_callback_fmt_command<Optional<std::pair<std::string, Output>>>(std::forward<Callback>(cb),
fmt::lmpop<Input>, first, last, whence, count);
}

template <typename Output, typename Input, typename Callback>
auto lmpop(Input first, Input last, ListWhence whence, Callback &&cb)
-> typename std::enable_if<IsInvocable<typename std::decay<Callback>::type, Future<Optional<std::pair<std::string, Output>>> &&>::value, void>::type {
return lmpop<Output>(first, last, whence, 1, std::forward<Callback>(cb));
}

Future<OptionalString> lmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence) {
return _command<OptionalString>(fmt::lmove, src, dest, src_whence, dest_whence);
}

template <typename Callback>
void lmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence, Callback &&cb) {
_callback_fmt_command<OptionalString>(std::forward<Callback>(cb), fmt::lmove, src, dest, src_whence, dest_whence);
}

Future<OptionalString> blmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence,
const std::chrono::seconds &timeout = std::chrono::seconds{0}) {
return _command<OptionalString>(fmt::blmove, src, dest, src_whence, dest_whence, timeout.count());
}

template <typename Callback>
void blmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence,
const std::chrono::seconds &timeout, Callback &&cb) {
_callback_fmt_command<OptionalString>(std::forward<Callback>(cb), fmt::blmove, src, dest,
src_whence, dest_whence, timeout.count());
}

template <typename Callback>
auto blmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence, Callback &&cb)
-> typename std::enable_if<IsInvocable<typename std::decay<Callback>::type, Future<OptionalString> &&>::value, void>::type {
blmove(src, dest, src_whence, dest_whence, std::chrono::seconds{0}, std::forward<Callback>(cb));
}

// HASH commands.

Future<long long> hdel(const StringView &key, const StringView &field) {
Expand Down
33 changes: 33 additions & 0 deletions src/sw/redis++/cmd_formatter.h
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,39 @@ FormattedCommand rpush_range(const StringView &key, Input first, Input last) {
return format_cmd(args);
}

template <typename Input>
FormattedCommand lmpop(Input first, Input last, ListWhence whence, long long count) {
assert(first != last);

CmdArgs args;

auto keys_num = std::distance(first, last);

args << "LMPOP" << keys_num << std::make_pair(first, last) << to_string(whence) << "COUNT" << count;

return format_cmd(args);
}

inline FormattedCommand lmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence) {
auto src_whence_str = to_string(src_whence);
auto dest_whence_str = to_string(dest_whence);
return format_cmd("LMOVE %b %b %s %s",
src.data(), src.size(),
dest.data(), dest.size(),
src_whence_str.data(), dest_whence_str.data());
}

inline FormattedCommand blmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence, long long timeout) {
auto src_whence_str = to_string(src_whence);
auto dest_whence_str = to_string(dest_whence);
return format_cmd("BLMOVE %b %b %s %s %lld",
src.data(), src.size(),
dest.data(), dest.size(),
src_whence_str.data(), dest_whence_str.data(), timeout);
}

// HASH commands.

inline FormattedCommand hdel(const StringView &key, const StringView &field) {
Expand Down
2 changes: 1 addition & 1 deletion src/sw/redis++/command.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ void linsert(Connection &connection,
break;

default:
assert(false);
throw Error("unknown insert position");
}

connection.send("LINSERT %b %s %b %b",
Expand Down
33 changes: 33 additions & 0 deletions src/sw/redis++/command.h
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,39 @@ inline void rpushx(Connection &connection, const StringView &key, const StringVi
val.data(), val.size());
}

template <typename Input>
inline void lmpop(Connection &connection, Input first, Input last, ListWhence whence, long long count) {
assert(first != last);

CmdArgs args;

auto keys_num = std::distance(first, last);

args << "LMPOP" << keys_num << std::make_pair(first, last) << to_string(whence) << "COUNT" << count;

connection.send(args);
}

inline void lmove(Connection &connection, const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence) {
auto src_whence_str = to_string(src_whence);
auto dest_whence_str = to_string(dest_whence);
connection.send("LMOVE %b %b %s %s",
src.data(), src.size(),
dest.data(), dest.size(),
src_whence_str.data(), dest_whence_str.data());
}

inline void blmove(Connection &connection, const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence, long long timeout) {
auto src_whence_str = to_string(src_whence);
auto dest_whence_str = to_string(dest_whence);
connection.send("BLMOVE %b %b %s %s %lld",
src.data(), src.size(),
dest.data(), dest.size(),
src_whence_str.data(), dest_whence_str.data(), timeout);
}

// HASH commands.

inline void hdel(Connection &connection, const StringView &key, const StringView &field) {
Expand Down
16 changes: 16 additions & 0 deletions src/sw/redis++/command_options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,22 @@ const std::string& RightBoundedInterval<std::string>::lower() const {
return NEGATIVE_INFINITY_STRING;
}

std::string to_string(ListWhence whence) {
std::string str;
switch (whence) {
case ListWhence::LEFT:
str = "LEFT";
break;
case ListWhence::RIGHT:
str = "RIGHT";
break;
default:
throw Error("unknown list whence");
}

return str;
}

}

}
Expand Down
7 changes: 7 additions & 0 deletions src/sw/redis++/command_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ enum class InsertPosition {
AFTER
};

enum class ListWhence {
LEFT,
RIGHT
};

enum class BoundType {
CLOSED,
OPEN,
Expand Down Expand Up @@ -209,6 +214,8 @@ struct WithDist : TupleWithType<double, T> {};
template <typename T>
struct WithHash : TupleWithType<long long, T> {};

std::string to_string(ListWhence whence);

}

}
Expand Down
14 changes: 14 additions & 0 deletions src/sw/redis++/redis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,20 @@ long long Redis::rpushx(const StringView &key, const StringView &val) {
return reply::parse<long long>(*reply);
}

OptionalString Redis::lmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence) {
auto reply = command(cmd::lmove, src, dest, src_whence, dest_whence);

return reply::parse<OptionalString>(*reply);
}

OptionalString Redis::blmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence, const std::chrono::seconds &timeout) {
auto reply = command(cmd::blmove, src, dest, src_whence, dest_whence, timeout.count());

return reply::parse<OptionalString>(*reply);
}

long long Redis::hdel(const StringView &key, const StringView &field) {
auto reply = command(cmd::hdel, key, field);

Expand Down
75 changes: 75 additions & 0 deletions src/sw/redis++/redis.h
Original file line number Diff line number Diff line change
Expand Up @@ -1314,6 +1314,81 @@ class Redis {
/// @see https://redis.io/commands/rpushx
long long rpushx(const StringView &key, const StringView &val);

/// @brief Pop one or more elements from the first non-empty list.
///
/// Example:
/// @code{.cpp}
/// auto lists = {"l1", "l2"};
/// auto val = redis.lmpop<std::vector<std::string>>(lists.begin(), lists.end(), ListWhence::LEFT, 2);
/// if (val)
/// std::cout << "list: " << val->first << ", size: " << val->second.size() << std::endl;
/// else
/// std::cout << "all lists are empty" << std::endl;
/// @endcode
/// @param first Iterator to the first list.
/// @param last Off-the-end iterator to the given list range.
/// @param whence ListWhence::LEFT or ListWhence::RIGHT.
/// @param count Number of elements to be popped.
/// @return Elements popped from list.
/// @note If key does not exist, `lmpop` returns `Optional<std::pair<std::string, Output>>{}` (`std::nullopt`).
/// @see https://redis.io/commands/lmpop
template <typename Output, typename Input>
Optional<std::pair<std::string, Output>> lmpop(Input first, Input last, ListWhence whence, long long count = 1);

/// @brief Pop one or more elements from the first non-empty list.
/// @param il Initializer list of Redis lists.
/// @param pos ListWhence::LEFT or ListWhence::RIGHT.
/// @param count Number of elements to be popped.
/// @return Elements popped from list.
/// @note If key does not exist, `lmpop` returns `Optional<std::pair<std::string, Output>>{}` (`std::nullopt`).
/// @see https://redis.io/commands/lmpop
template <typename Output, typename T>
Optional<std::pair<std::string, Output>> lmpop(std::initializer_list<T> il, ListWhence pos, long long count = 1) {
return lmpop<Output>(il.begin(), il.end(), pos, count);
}

/// @brief Move element from src list to dest list.
///
/// Example:
/// @code{.cpp}
/// auto val = redis.lmove("src", "dest", ListWhence::LEFT, ListWhence::RIGHT);
/// if (val)
/// std::cout << "moved " << *val << " from src to dest" << std::endl;
/// else
/// std::cout << "src list does not exist" << std::endl;
/// @endcode
/// @param src Source list.
/// @param dest Destination list.
/// @param src_whence From where of the source list.
/// @param dest_whence To where of the dest list.
/// @return The moved element.
/// @note If source list does not exist, `lmove` returns `OptionalString{}` (`std::nullopt`).
/// @see https://redis.io/commands/lmove
OptionalString lmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence);

/// @brief The block version of lmove.
///
/// Example:
/// @code{.cpp}
/// auto val = redis.blmove("src", "dest", ListWhence::LEFT, ListWhence::RIGHT, std::chrono::seonds(2));
/// if (val)
/// std::cout << "moved " << *val << " from src to dest" << std::endl;
/// else
/// std::cout << "src list does not exist" << std::endl;
/// @endcode
/// @param src Source list.
/// @param dest Destination list.
/// @param src_whence From where of the source list.
/// @param dest_whence To where of the dest list.
/// @param timeout Timeout in seconds. 0 means block forever.
/// @return The moved element.
/// @note If source list does not exist, `blmove` returns `OptionalString{}` (`std::nullopt`).
/// @see https://redis.io/commands/blmove
OptionalString blmove(const StringView &src, const StringView &dest,
ListWhence src_whence, ListWhence dest_whence,
const std::chrono::seconds &timeout = std::chrono::seconds{0});

// HASH commands.

/// @brief Remove the given field from hash.
Expand Down
10 changes: 10 additions & 0 deletions src/sw/redis++/redis.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,16 @@ inline long long Redis::rpush(const StringView &key, Input first, Input last) {
return reply::parse<long long>(*reply);
}

template <typename Output, typename Input>
Optional<std::pair<std::string, Output>> Redis::lmpop(Input first, Input last, ListWhence whence, long long count) {
range_check("LMPOP", first, last);

auto reply = command(cmd::lmpop<Input>, first, last, whence, count);

return reply::parse<Optional<std::pair<std::string, Output>>>(*reply);
}


// HASH commands.

template <typename Input>
Expand Down
Loading

0 comments on commit 87d7690

Please sign in to comment.