This is an automated email from the ASF dual-hosted git repository.
twice pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git
The following commit(s) were added to refs/heads/unstable by this push:
new dca713489 feat(string): Add to Support MSETEX Command (#3318)
dca713489 is described below
commit dca7134890a558f6fcd2a17dea176965c0607ad8
Author: Byeonggyu Park <[email protected]>
AuthorDate: Thu Jan 1 17:54:45 2026 +0900
feat(string): Add to Support MSETEX Command (#3318)
## Issue
closes #3307
## Description
In redis, it supports MSETEX which supports MSET with TTL options from
v8.4.0.
Syntax of `MSETEX` is below.
```
MSETEX numkeys key value [key value ...] [NX | XX] [EX seconds |
PX milliseconds | EXAT unix-time-seconds |
PXAT unix-time-milliseconds | KEEPTTL]
```
This change adds support for the `MSETEX` command in kvrocks, making it
compatible with Redis.
- Reference: https://redis.io/docs/latest/commands/msetex/
---------
Co-authored-by: Twice <[email protected]>
---
src/commands/cmd_string.cc | 69 ++++++++++++++++-
src/types/redis_string.cc | 73 ++++++++++++------
src/types/redis_string.h | 13 +++-
tests/cppunit/iterator_test.cc | 2 +-
tests/cppunit/types/string_test.cc | 71 ++++++++++++++++-
tests/gocase/go.mod | 2 +-
tests/gocase/go.sum | 6 +-
.../integration/slotmigrate/slotmigrate_test.go | 2 +-
tests/gocase/unit/type/strings/strings_test.go | 88 ++++++++++++++++++++++
9 files changed, 288 insertions(+), 38 deletions(-)
diff --git a/src/commands/cmd_string.cc b/src/commands/cmd_string.cc
index 25ac408ce..9e3bf8aef 100644
--- a/src/commands/cmd_string.cc
+++ b/src/commands/cmd_string.cc
@@ -421,7 +421,7 @@ class CommandMSet : public Commander {
kvs.emplace_back(StringPair{args_[i], args_[i + 1]});
}
- auto s = string_db.MSet(ctx, kvs, 0);
+ auto s = string_db.MSet(ctx, kvs);
if (!s.ok()) {
return {Status::RedisExecErr, s.ToString()};
}
@@ -447,6 +447,70 @@ class CommandSetNX : public Commander {
}
};
+class CommandMSetEX : public Commander {
+ public:
+ Status Parse(const std::vector<std::string> &args) override {
+ auto parsed_num_keys = ParseInt<int>(args[1], 10);
+ if (!parsed_num_keys) {
+ return {Status::RedisParseErr, errValueNotInteger};
+ }
+
+ if (*parsed_num_keys <= 0) return {Status::RedisParseErr,
errValueMustBePositive};
+ num_keys_ = *parsed_num_keys;
+ min_args_ = 2 + 2 * num_keys_;
+ if (args.size() < min_args_) {
+ return {Status::RedisParseErr, errWrongNumOfArguments};
+ }
+
+ CommandParser parser(args, min_args_);
+ std::string_view ttl_flag, set_flag;
+ while (parser.Good()) {
+ if (auto v = GET_OR_RET(ParseExpireFlags(parser, ttl_flag))) {
+ expire_ = *v;
+ } else if (parser.EatEqICaseFlag("KEEPTTL", ttl_flag)) {
+ keep_ttl_ = true;
+ } else if (parser.EatEqICaseFlag("NX", set_flag)) {
+ set_flag_ = StringSetType::NX;
+ } else if (parser.EatEqICaseFlag("XX", set_flag)) {
+ set_flag_ = StringSetType::XX;
+ } else {
+ return parser.InvalidSyntax();
+ }
+ }
+ return Status::OK();
+ }
+
+ Status Execute(engine::Context &ctx, Server *srv, Connection *conn,
std::string *output) override {
+ bool ret = false;
+
+ std::vector<StringPair> kvs;
+ for (size_t i = 2; i < min_args_; i += 2) {
+ kvs.emplace_back(StringPair{args_[i], args_[i + 1]});
+ }
+
+ redis::String string_db(srv->storage, conn->GetNamespace());
+ auto s = string_db.MSetEX(ctx, kvs, {expire_, set_flag_, keep_ttl_}, &ret);
+ if (!s.ok()) {
+ return {Status::RedisExecErr, s.ToString()};
+ }
+
+ *output = redis::Integer(ret ? 1 : 0);
+ return Status::OK();
+ }
+
+ static CommandKeyRange Range(const std::vector<std::string> &args) {
+ int num_keys = *ParseInt<int>(args[1], 10);
+ return {2, 2 + 2 * num_keys, 2};
+ }
+
+ private:
+ size_t num_keys_ = 0;
+ size_t min_args_ = 4;
+ StringSetType set_flag_ = StringSetType::NONE;
+ uint64_t expire_ = 0;
+ bool keep_ttl_ = false;
+};
+
class CommandMSetNX : public Commander {
public:
Status Parse(const std::vector<std::string> &args) override {
@@ -465,7 +529,7 @@ class CommandMSetNX : public Commander {
kvs.emplace_back(StringPair{args_[i], args_[i + 1]});
}
- auto s = string_db.MSetNX(ctx, kvs, 0, &ret);
+ auto s = string_db.MSetNX(ctx, kvs, &ret);
if (!s.ok()) {
return {Status::RedisExecErr, s.ToString()};
}
@@ -753,6 +817,7 @@ REDIS_REGISTER_COMMANDS(
MakeCmdAttr<CommandAppend>("append", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandSet>("set", -3, "write", 1, 1, 1),
MakeCmdAttr<CommandSetEX>("setex", 4, "write", 1, 1, 1),
MakeCmdAttr<CommandPSetEX>("psetex", 4, "write", 1, 1, 1),
MakeCmdAttr<CommandSetNX>("setnx", 3, "write", 1, 1, 1),
+ MakeCmdAttr<CommandMSetEX>("msetex", -4, "write", CommandMSetEX::Range),
MakeCmdAttr<CommandMSetNX>("msetnx", -3, "write", 1, -1, 2),
MakeCmdAttr<CommandMSet>("mset", -3, "write", 1, -1, 2),
MakeCmdAttr<CommandIncrBy>("incrby", 3, "write", 1, 1, 1),
MakeCmdAttr<CommandIncrByFloat>("incrbyfloat", 3, "write", 1, 1, 1),
diff --git a/src/types/redis_string.cc b/src/types/redis_string.cc
index 00cb099ab..273e51ec4 100644
--- a/src/types/redis_string.cc
+++ b/src/types/redis_string.cc
@@ -199,7 +199,7 @@ rocksdb::Status String::GetDel(engine::Context &ctx, const
std::string &user_key
rocksdb::Status String::Set(engine::Context &ctx, const std::string &user_key,
const std::string &value) {
std::vector<StringPair> pairs{StringPair{user_key, value}};
- return MSet(ctx, pairs, /*expire=*/0);
+ return MSet(ctx, pairs);
}
rocksdb::Status String::Set(engine::Context &ctx, const std::string &user_key,
const std::string &value,
@@ -394,46 +394,71 @@ rocksdb::Status String::IncrByFloat(engine::Context &ctx,
const std::string &use
return updateRawValue(ctx, ns_key, raw_value);
}
-rocksdb::Status String::MSet(engine::Context &ctx, const
std::vector<StringPair> &pairs, uint64_t expire_ms) {
+rocksdb::Status String::MSet(engine::Context &ctx, const
std::vector<StringPair> &pairs, StringMSetArgs args,
+ bool *flag) {
+ if (flag != nullptr) {
+ *flag = false;
+ }
+
+ if (args.type != StringSetType::NONE) {
+ int exists = 0;
+ int key_count = static_cast<int>(pairs.size());
+ std::vector<Slice> keys;
+ keys.reserve(pairs.size());
+ for (const auto &pair : pairs) {
+ keys.emplace_back(pair.key);
+ }
+ auto s = Exists(ctx, keys, &exists);
+ if (!s.ok()) return s;
+ if ((args.type == StringSetType::NX && exists > 0) || (args.type ==
StringSetType::XX && exists < key_count)) {
+ return rocksdb::Status::OK();
+ }
+ }
+
auto batch = storage_->GetWriteBatchBase();
WriteBatchLogData log_data(kRedisString);
- auto s = batch->PutLogData(log_data.Encode());
+ rocksdb::Status s = batch->PutLogData(log_data.Encode());
if (!s.ok()) return s;
+
for (const auto &pair : pairs) {
std::string bytes;
Metadata metadata(kRedisString, false);
- metadata.expire = expire_ms;
+ if (args.keep_ttl) {
+ uint64_t old_expire = 0;
+ rocksdb::Status s = GetExpireTime(ctx, pair.key, &old_expire);
+ if (s.ok() && old_expire != 0) {
+ metadata.expire = old_expire;
+ }
+ } else {
+ metadata.expire = args.expire;
+ }
metadata.Encode(&bytes);
bytes.append(pair.value.data(), pair.value.size());
std::string ns_key = AppendNamespacePrefix(pair.key);
s = batch->Put(metadata_cf_handle_, ns_key, bytes);
if (!s.ok()) return s;
}
- return storage_->Write(ctx, storage_->DefaultWriteOptions(),
batch->GetWriteBatch());
-}
-
-rocksdb::Status String::MSetNX(engine::Context &ctx, const
std::vector<StringPair> &pairs, uint64_t expire_ms,
- bool *flag) {
- *flag = false;
-
- int exists = 0;
- std::vector<Slice> keys;
- keys.reserve(pairs.size());
+ s = storage_->Write(ctx, storage_->DefaultWriteOptions(),
batch->GetWriteBatch());
+ if (!s.ok()) return s;
- for (StringPair pair : pairs) {
- std::string ns_key = AppendNamespacePrefix(pair.key);
- keys.emplace_back(pair.key);
+ if (flag != nullptr) {
+ *flag = true;
}
- if (Exists(ctx, keys, &exists).ok() && exists > 0) {
- return rocksdb::Status::OK();
- }
+ return rocksdb::Status::OK();
+}
- rocksdb::Status s = MSet(ctx, pairs, /*expire_ms=*/expire_ms);
- if (!s.ok()) return s;
+rocksdb::Status String::MSet(engine::Context &ctx, const
std::vector<StringPair> &pairs) {
+ return MSet(ctx, pairs, {/*expire=*/0, StringSetType::NONE,
/*keep_ttl=*/false}, nullptr);
+}
- *flag = true;
- return rocksdb::Status::OK();
+rocksdb::Status String::MSetEX(engine::Context &ctx, const
std::vector<StringPair> &pairs, StringMSetArgs args,
+ bool *flag) {
+ return MSet(ctx, pairs, args, flag);
+}
+
+rocksdb::Status String::MSetNX(engine::Context &ctx, const
std::vector<StringPair> &pairs, bool *flag) {
+ return MSet(ctx, pairs, {/*expire=*/0, StringSetType::NX,
/*keep_ttl=*/false}, flag);
}
// Change the value of user_key to a new_value if the current value of the key
matches old_value.
diff --git a/src/types/redis_string.h b/src/types/redis_string.h
index a37f63544..4e7bdfeac 100644
--- a/src/types/redis_string.h
+++ b/src/types/redis_string.h
@@ -44,6 +44,13 @@ struct StringSetArgs {
bool keep_ttl;
};
+struct StringMSetArgs {
+ // Expire time in mill seconds.
+ uint64_t expire;
+ StringSetType type;
+ bool keep_ttl;
+};
+
enum class StringLCSType { NONE, LEN, IDX };
struct StringLCSArgs {
@@ -100,8 +107,10 @@ class String : public Database {
rocksdb::Status IncrByFloat(engine::Context &ctx, const std::string
&user_key, double increment, double *new_value);
std::vector<rocksdb::Status> MGet(engine::Context &ctx, const
std::vector<Slice> &keys,
std::vector<std::string> *values);
- rocksdb::Status MSet(engine::Context &ctx, const std::vector<StringPair>
&pairs, uint64_t expire_ms);
- rocksdb::Status MSetNX(engine::Context &ctx, const std::vector<StringPair>
&pairs, uint64_t expire_ms, bool *flag);
+ rocksdb::Status MSet(engine::Context &ctx, const std::vector<StringPair>
&pairs, StringMSetArgs args, bool *flag);
+ rocksdb::Status MSet(engine::Context &ctx, const std::vector<StringPair>
&pairs);
+ rocksdb::Status MSetEX(engine::Context &ctx, const std::vector<StringPair>
&pairs, StringMSetArgs args, bool *flag);
+ rocksdb::Status MSetNX(engine::Context &ctx, const std::vector<StringPair>
&pairs, bool *flag);
rocksdb::Status CAS(engine::Context &ctx, const std::string &user_key, const
std::string &old_value,
const std::string &new_value, uint64_t expire_ms, int
*flag);
rocksdb::Status CAD(engine::Context &ctx, const std::string &user_key, const
std::string &value, int *flag);
diff --git a/tests/cppunit/iterator_test.cc b/tests/cppunit/iterator_test.cc
index 183edcdde..1a3045617 100644
--- a/tests/cppunit/iterator_test.cc
+++ b/tests/cppunit/iterator_test.cc
@@ -390,7 +390,7 @@ TEST_F(WALIteratorTest, BasicString) {
auto start_seq = storage_->GetDB()->GetLatestSequenceNumber();
redis::String string(storage_.get(), "test_ns0");
string.Set(*ctx_, "a", "1");
- string.MSet(*ctx_, {{"b", "2"}, {"c", "3"}}, 0);
+ string.MSet(*ctx_, {{"b", "2"}, {"c", "3"}});
ASSERT_TRUE(string.Del(*ctx_, "b").ok());
std::vector<std::string> put_keys, delete_keys;
diff --git a/tests/cppunit/types/string_test.cc
b/tests/cppunit/types/string_test.cc
index 1b5c1a27c..1b8741767 100644
--- a/tests/cppunit/types/string_test.cc
+++ b/tests/cppunit/types/string_test.cc
@@ -69,7 +69,7 @@ TEST_F(RedisStringTest, GetAndSet) {
}
TEST_F(RedisStringTest, MGetAndMSet) {
- string_->MSet(*ctx_, pairs_, 0);
+ string_->MSet(*ctx_, pairs_);
std::vector<Slice> keys;
std::vector<std::string> values;
keys.reserve(pairs_.size());
@@ -186,7 +186,7 @@ TEST_F(RedisStringTest, MSetXX) {
TEST_F(RedisStringTest, MSetNX) {
bool flag = false;
- string_->MSetNX(*ctx_, pairs_, 0, &flag);
+ string_->MSetNX(*ctx_, pairs_, &flag);
EXPECT_TRUE(flag);
std::vector<Slice> keys;
std::vector<std::string> values;
@@ -202,7 +202,7 @@ TEST_F(RedisStringTest, MSetNX) {
std::vector<StringPair> new_pairs{
{"a", "1"}, {"b", "2"}, {"c", "3"}, {pairs_[0].key, pairs_[0].value},
{"d", "4"},
};
- string_->MSetNX(*ctx_, pairs_, 0, &flag);
+ string_->MSetNX(*ctx_, pairs_, &flag);
EXPECT_FALSE(flag);
for (auto &pair : pairs_) {
@@ -219,6 +219,71 @@ TEST_F(RedisStringTest, MSetNXWithTTL) {
s = string_->Del(*ctx_, key_);
}
+TEST_F(RedisStringTest, MSetEX) {
+ {
+ bool flag = false;
+ string_->MSetEX(*ctx_, pairs_, {0, StringSetType::NX, false}, &flag);
+ EXPECT_TRUE(flag);
+ std::vector<Slice> keys;
+ std::vector<std::string> values;
+ keys.reserve(pairs_.size());
+ for (const auto &pair : pairs_) {
+ keys.emplace_back(pair.key);
+ }
+ string_->MGet(*ctx_, keys, &values);
+ for (const auto &pair : pairs_) {
+ int64_t ttl = 0;
+ auto s = string_->TTL(*ctx_, pair.key.ToString(), &ttl);
+ EXPECT_EQ(ttl, -1);
+ }
+ }
+ {
+ bool flag = false;
+ pairs_.emplace_back(StringPair{"a", "1"});
+ string_->MSetEX(*ctx_, pairs_, {0, StringSetType::XX, true}, &flag);
+ EXPECT_FALSE(flag);
+ }
+ for (auto &pair : pairs_) {
+ auto s = string_->Del(*ctx_, pair.key);
+ }
+}
+
+TEST_F(RedisStringTest, MSetEXWithTTL) {
+ {
+ bool flag = false;
+ string_->MSetEX(*ctx_, pairs_, {util::GetTimeStampMS() + 3000,
StringSetType::NONE, false}, &flag);
+ EXPECT_TRUE(flag);
+ for (const auto &pair : pairs_) {
+ int64_t ttl = 0;
+ auto s = string_->TTL(*ctx_, pair.key.ToString(), &ttl);
+ EXPECT_TRUE(ttl >= 2000 && ttl <= 4000);
+ }
+ }
+ {
+ bool flag = false;
+ pairs_[0].value = "new-test-strings-value1";
+ pairs_[1].value = "new-test-strings-value2";
+ string_->MSetEX(*ctx_, pairs_, {0, StringSetType::XX, true}, &flag);
+ EXPECT_TRUE(flag);
+ std::vector<Slice> keys;
+ std::vector<std::string> values;
+ keys.reserve(pairs_.size());
+ for (const auto &pair : pairs_) {
+ keys.emplace_back(pair.key);
+ }
+ string_->MGet(*ctx_, keys, &values);
+ for (size_t i = 0; i < pairs_.size(); i++) {
+ EXPECT_EQ(pairs_[i].value, values[i]);
+ int64_t ttl = 0;
+ auto s = string_->TTL(*ctx_, pairs_[i].key.ToString(), &ttl);
+ EXPECT_TRUE(ttl >= 2000 && ttl <= 4000);
+ }
+ }
+ for (auto &pair : pairs_) {
+ auto s = string_->Del(*ctx_, pair.key);
+ }
+}
+
TEST_F(RedisStringTest, SetEX) {
string_->SetEX(*ctx_, key_, "test-value", util::GetTimeStampMS() + 3000);
int64_t ttl = 0;
diff --git a/tests/gocase/go.mod b/tests/gocase/go.mod
index 7ec1c0e2b..bde40811b 100644
--- a/tests/gocase/go.mod
+++ b/tests/gocase/go.mod
@@ -4,7 +4,7 @@ go 1.24.0
require (
github.com/linxGnu/grocksdb v1.10.2
- github.com/redis/go-redis/v9 v9.14.0
+ github.com/redis/go-redis/v9 v9.17.0
github.com/shirou/gopsutil/v4 v4.25.8
github.com/stretchr/testify v1.11.1
golang.org/x/exp v0.0.0-20250911091902-df9299821621
diff --git a/tests/gocase/go.sum b/tests/gocase/go.sum
index fc666ae92..2520f45f4 100644
--- a/tests/gocase/go.sum
+++ b/tests/gocase/go.sum
@@ -8,8 +8,6 @@ github.com/davecgh/go-spew v1.1.1
h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f
h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod
h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
-github.com/ebitengine/purego v0.8.4
h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
-github.com/ebitengine/purego v0.8.4/go.mod
h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.9.0
h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
github.com/ebitengine/purego v0.9.0/go.mod
h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/go-ole/go-ole v1.2.6/go.mod
h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -25,8 +23,8 @@ github.com/pmezard/go-difflib v1.0.0
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55
h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod
h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
-github.com/redis/go-redis/v9 v9.14.0
h1:u4tNCjXOyzfgeLN+vAZaW1xUooqWDqVEsZN0U01jfAE=
-github.com/redis/go-redis/v9 v9.14.0/go.mod
h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
+github.com/redis/go-redis/v9 v9.17.0
h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM=
+github.com/redis/go-redis/v9 v9.17.0/go.mod
h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
github.com/shirou/gopsutil/v4 v4.25.8
h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
github.com/shirou/gopsutil/v4 v4.25.8/go.mod
h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
github.com/stretchr/testify v1.11.1
h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
diff --git a/tests/gocase/integration/slotmigrate/slotmigrate_test.go
b/tests/gocase/integration/slotmigrate/slotmigrate_test.go
index 00aeed9bf..7f82885e6 100644
--- a/tests/gocase/integration/slotmigrate/slotmigrate_test.go
+++ b/tests/gocase/integration/slotmigrate/slotmigrate_test.go
@@ -1336,7 +1336,7 @@ func TestSlotRangeMigrate(t *testing.T) {
require.Equal(t, "OK", rdb0.Do(ctx, "clusterx", "migrate",
"112-113", id1).Val())
waitForMigrateSlotRangeState(t, rdb0, "112-113",
SlotMigrationStateSuccess)
for slot := 112; slot <= 118; slot++ {
- require.Contains(t, rdb0.LPush(ctx,
util.SlotTable[slot], 10).Err(), "MOVED")
+ require.ErrorContains(t, rdb0.LPush(ctx,
util.SlotTable[slot], 10).Err(), "MOVED")
}
// overlap
diff --git a/tests/gocase/unit/type/strings/strings_test.go
b/tests/gocase/unit/type/strings/strings_test.go
index 56b67cb00..584030e65 100644
--- a/tests/gocase/unit/type/strings/strings_test.go
+++ b/tests/gocase/unit/type/strings/strings_test.go
@@ -319,6 +319,94 @@ func testString(t *testing.T, configs
util.KvrocksServerConfigs) {
require.ErrorContains(t, r.Err(), "wrong number")
})
+ t.Run("MSETEX wrong args", func(t *testing.T) {
+ r := rdb.Do(ctx, "msetex", "0").Err()
+ require.ErrorContains(t, r, "wrong number")
+ r = rdb.Do(ctx, "msetex", "0", "a", "1").Err()
+ require.ErrorContains(t, r, "value is out of range, must be
positive")
+ r = rdb.Do(ctx, "msetex", "3", "a", "1", "b", "2").Err()
+ require.ErrorContains(t, r, "wrong number")
+ r = rdb.Do(ctx, "msetex", "1", "a", "1", "b", "2", "xx").Err()
+ require.ErrorContains(t, r, "syntax error")
+ r = rdb.Do(ctx, "msetex", "1", "a", "1", "ex", "-1").Err()
+ require.ErrorContains(t, r, "out of numeric range")
+ r = rdb.Do(ctx, "msetex", "1", "a", "1", "ex", "10",
"keepttl").Err()
+ require.ErrorContains(t, r, "syntax error")
+ })
+
+ t.Run("MSETEX with NX|XX", func(t *testing.T) {
+ require.NoError(t, rdb.Del(ctx, "xx1", "xx2").Err())
+ res := rdb.MSetEX(ctx, redis.MSetEXArgs{Condition: redis.NX},
"xx1", "1", "xx2", "2")
+ require.EqualValues(t, 1, res.Val())
+ require.Equal(t, "1", rdb.Get(ctx, "xx1").Val())
+
+ require.NoError(t, rdb.Set(ctx, "xx3", "pre", 0).Err())
+ res = rdb.MSetEX(ctx, redis.MSetEXArgs{Condition: redis.NX},
"xx3", "a", "xx4", "b")
+ require.EqualValues(t, 0, res.Val())
+ require.Equal(t, "pre", rdb.Get(ctx, "xx3").Val())
+ require.EqualValues(t, 0, rdb.Exists(ctx, "xx4").Val())
+
+ res = rdb.MSetEX(ctx, redis.MSetEXArgs{
+ Condition: redis.XX,
+ Expiration: &redis.ExpirationOption{
+ Mode: redis.EX, Value: 10,
+ }}, "xx1", "new1", "xx2", "new2")
+ require.EqualValues(t, 1, res.Val())
+ require.Equal(t, "new1", rdb.Get(ctx, "xx1").Val())
+ require.Equal(t, "new2", rdb.Get(ctx, "xx2").Val())
+ util.BetweenValues(t, rdb.TTL(ctx, "xx1").Val(), 9*time.Second,
10*time.Second)
+ util.BetweenValues(t, rdb.TTL(ctx, "xx2").Val(), 9*time.Second,
10*time.Second)
+ })
+
+ t.Run("MSETEX with TTL", func(t *testing.T) {
+ require.NoError(t, rdb.Del(ctx, "a", "b").Err())
+ res := rdb.MSetEX(ctx, redis.MSetEXArgs{
+ Condition: redis.NX,
+ Expiration: &redis.ExpirationOption{
+ Mode: redis.EX,
+ Value: 2,
+ }}, "a", "1", "b", "2")
+ require.EqualValues(t, 1, res.Val())
+ require.Equal(t, "1", rdb.Get(ctx, "a").Val())
+ util.BetweenValues(t, rdb.TTL(ctx, "a").Val(), 1*time.Second,
2*time.Second)
+ util.BetweenValues(t, rdb.TTL(ctx, "b").Val(), 1*time.Second,
2*time.Second)
+
+ res = rdb.MSetEX(ctx, redis.MSetEXArgs{
+ Condition: redis.XX,
+ Expiration: &redis.ExpirationOption{
+ Mode: redis.PX,
+ Value: 3000,
+ }}, "a", "10", "d", "20")
+ require.EqualValues(t, 0, res.Val())
+ require.Equal(t, "1", rdb.Get(ctx, "a").Val())
+
+ res = rdb.MSetEX(ctx, redis.MSetEXArgs{
+ Condition: redis.XX,
+ Expiration: &redis.ExpirationOption{
+ Mode: redis.PX,
+ Value: 3000,
+ }}, "a", "10", "b", "20")
+ require.EqualValues(t, 1, res.Val())
+ require.Equal(t, "10", rdb.Get(ctx, "a").Val())
+ require.Equal(t, "20", rdb.Get(ctx, "b").Val())
+ util.BetweenValues(t, rdb.TTL(ctx, "a").Val(), 2*time.Second,
3*time.Second)
+ util.BetweenValues(t, rdb.TTL(ctx, "b").Val(), 2*time.Second,
3*time.Second)
+ })
+
+ t.Run("MSETEX with KEEPTTL", func(t *testing.T) {
+ require.NoError(t, rdb.Set(ctx, "k1", "v", 5*time.Second).Err())
+ require.NoError(t, rdb.Del(ctx, "k2").Err())
+ res := rdb.MSetEX(ctx, redis.MSetEXArgs{
+ Expiration: &redis.ExpirationOption{
+ Mode: redis.KEEPTTL,
+ }}, "k1", "v2", "k2", "v3")
+ require.EqualValues(t, 1, res.Val())
+ require.Equal(t, "v2", rdb.Get(ctx, "k1").Val())
+ require.Equal(t, "v3", rdb.Get(ctx, "k2").Val())
+ util.BetweenValues(t, rdb.TTL(ctx, "k1").Val(), 4*time.Second,
5*time.Second)
+ require.EqualValues(t, -1, rdb.TTL(ctx, "k2").Val())
+ })
+
t.Run("MSETNX with already existent key", func(t *testing.T) {
r := rdb.MSetNX(ctx, map[string]interface{}{
"x1": "xxx",