Skip to content

Commit f7c5688

Browse files
feat(hash): add the support of HSETEXPIRE command (HSET + EXPIRE) (#2750)
1 parent 88f1f3e commit f7c5688

File tree

4 files changed

+173
-5
lines changed

4 files changed

+173
-5
lines changed

src/commands/cmd_hash.cc

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include "error_constants.h"
2424
#include "scan_base.h"
2525
#include "server/server.h"
26+
#include "time_util.h"
2627
#include "types/redis_hash.h"
2728

2829
namespace redis {
@@ -258,6 +259,36 @@ class CommandHMSet : public Commander {
258259
std::vector<FieldValue> field_values_;
259260
};
260261

262+
class CommandHSetExpire : public Commander {
263+
public:
264+
Status Parse(const std::vector<std::string> &args) override {
265+
ttl_ = GET_OR_RET(ParseInt<uint64_t>(args[2], 10));
266+
if ((args.size() - 3) % 2 != 0) {
267+
return {Status::RedisParseErr, "Invalid number of arguments: field-value pairs must be complete"};
268+
}
269+
for (size_t i = 3; i < args_.size(); i += 2) {
270+
field_values_.emplace_back(args_[i], args_[i + 1]);
271+
}
272+
return Commander::Parse(args);
273+
}
274+
275+
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
276+
uint64_t ret = 0;
277+
redis::Hash hash_db(srv->storage, conn->GetNamespace());
278+
279+
auto s = hash_db.MSet(ctx, args_[1], field_values_, false, &ret, ttl_ * 1000 + util::GetTimeStampMS());
280+
if (!s.ok()) {
281+
return {Status::RedisExecErr, s.ToString()};
282+
}
283+
*output = redis::RESP_OK;
284+
return Status::OK();
285+
}
286+
287+
private:
288+
std::vector<FieldValue> field_values_;
289+
uint64_t ttl_ = 0;
290+
};
291+
261292
class CommandHKeys : public Commander {
262293
public:
263294
Status Execute(engine::Context &ctx, Server *srv, Connection *conn, std::string *output) override {
@@ -448,6 +479,7 @@ REDIS_REGISTER_COMMANDS(Hash, MakeCmdAttr<CommandHGet>("hget", 3, "read-only", 1
448479
MakeCmdAttr<CommandHIncrBy>("hincrby", 4, "write", 1, 1, 1),
449480
MakeCmdAttr<CommandHIncrByFloat>("hincrbyfloat", 4, "write", 1, 1, 1),
450481
MakeCmdAttr<CommandHMSet>("hset", -4, "write", 1, 1, 1),
482+
MakeCmdAttr<CommandHSetExpire>("hsetexpire", -5, "write", 1, 1, 1),
451483
MakeCmdAttr<CommandHSetNX>("hsetnx", -4, "write", 1, 1, 1),
452484
MakeCmdAttr<CommandHDel>("hdel", -3, "write no-dbsize-check", 1, 1, 1),
453485
MakeCmdAttr<CommandHStrlen>("hstrlen", 3, "read-only", 1, 1, 1),

src/types/redis_hash.cc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -239,14 +239,18 @@ rocksdb::Status Hash::Delete(engine::Context &ctx, const Slice &user_key, const
239239
}
240240

241241
rocksdb::Status Hash::MSet(engine::Context &ctx, const Slice &user_key, const std::vector<FieldValue> &field_values,
242-
bool nx, uint64_t *added_cnt) {
242+
bool nx, uint64_t *added_cnt, uint64_t expire) {
243243
*added_cnt = 0;
244244
std::string ns_key = AppendNamespacePrefix(user_key);
245245

246246
HashMetadata metadata;
247247
rocksdb::Status s = GetMetadata(ctx, ns_key, &metadata);
248248
if (!s.ok() && !s.IsNotFound()) return s;
249-
249+
bool ttl_updated = false;
250+
if (expire > 0 && metadata.expire != expire) {
251+
metadata.expire = expire;
252+
ttl_updated = true;
253+
}
250254
int added = 0;
251255
auto batch = storage_->GetWriteBatchBase();
252256
WriteBatchLogData log_data(kRedisHash);
@@ -279,7 +283,7 @@ rocksdb::Status Hash::MSet(engine::Context &ctx, const Slice &user_key, const st
279283
if (!s.ok()) return s;
280284
}
281285

282-
if (added > 0) {
286+
if (added > 0 || ttl_updated) {
283287
*added_cnt = added;
284288
metadata.size += added;
285289
std::string bytes;

src/types/redis_hash.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class Hash : public SubKeyScanner {
5656
rocksdb::Status IncrByFloat(engine::Context &ctx, const Slice &user_key, const Slice &field, double increment,
5757
double *new_value);
5858
rocksdb::Status MSet(engine::Context &ctx, const Slice &user_key, const std::vector<FieldValue> &field_values,
59-
bool nx, uint64_t *added_cnt);
59+
bool nx, uint64_t *added_cnt, uint64_t expire = 0);
6060
rocksdb::Status RangeByLex(engine::Context &ctx, const Slice &user_key, const RangeLexSpec &spec,
6161
std::vector<FieldValue> *field_values);
6262
rocksdb::Status MGet(engine::Context &ctx, const Slice &user_key, const std::vector<Slice> &fields,

tests/gocase/unit/type/hash/hash_test.go

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,10 @@ import (
3030
"testing"
3131
"time"
3232

33-
"github.com/apache/kvrocks/tests/gocase/util"
33+
"github.com/stretchr/testify/assert"
3434
"github.com/stretchr/testify/require"
35+
36+
"github.com/apache/kvrocks/tests/gocase/util"
3537
)
3638

3739
func getKeys(hash map[string]string) []string {
@@ -118,6 +120,136 @@ var testHash = func(t *testing.T, configs util.KvrocksServerConfigs) {
118120
require.Equal(t, int64(1), rdb.HSet(ctx, "hmsetmulti", "key1", "val1", "key3", "val3").Val())
119121
})
120122

123+
t.Run("HSETEXPIRE wrong number of args", func(t *testing.T) {
124+
pattern := ".*wrong number.*"
125+
ttlStr := "3600"
126+
testKey := "hsetKey"
127+
r := rdb.Do(ctx, "hsetexpire", testKey, ttlStr)
128+
util.ErrorRegexp(t, r.Err(), pattern)
129+
})
130+
131+
t.Run("HSETEXPIRE incomplete pairs", func(t *testing.T) {
132+
pattern := ".*field-value pairs must be complete.*"
133+
ttlStr := "3600"
134+
testKey := "hsetKey"
135+
r := rdb.Do(ctx, "hsetexpire", testKey, ttlStr, "key1", "val1", "key2")
136+
util.ErrorRegexp(t, r.Err(), pattern)
137+
})
138+
139+
t.Run("HSET/HSETEXPIRE/HSETEXPIRE/persist update expire time", func(t *testing.T) {
140+
ttlStr := "3600"
141+
testKey := "hsetKeyUpdateTime"
142+
// create an hash without expiration
143+
r := rdb.Do(ctx, "hset", testKey, "key1", "val1", "key2", "val2")
144+
require.NoError(t, r.Err())
145+
noExp := rdb.ExpireTime(ctx, testKey)
146+
// make sure there is not exp set on the key
147+
assert.Equal(t, -1*time.Nanosecond, noExp.Val())
148+
// validate we inserted the key/vals
149+
values := rdb.HGetAll(ctx, testKey)
150+
assert.Equal(t, 2, len(values.Val()))
151+
152+
// update the hash and add expiration
153+
r = rdb.Do(ctx, "hsetexpire", testKey, ttlStr, "key3", "val3")
154+
require.NoError(t, r.Err())
155+
assert.Equal(t, "OK", r.Val())
156+
firstExp := rdb.ExpireTime(ctx, testKey)
157+
firstExpireTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC).Add(firstExp.Val()).Unix()
158+
// validate there is exp set on the key
159+
assert.NotEqual(t, -1, firstExpireTime)
160+
assert.Greater(t, firstExpireTime, time.Now().Unix())
161+
// validate we updated the key/vals
162+
values = rdb.HGetAll(ctx, testKey)
163+
assert.Equal(t, 3, len(values.Val()))
164+
165+
// update the has and expiration
166+
time.Sleep(1 * time.Second)
167+
r = rdb.Do(ctx, "hsetexpire", testKey, ttlStr, "key4", "val4")
168+
require.NoError(t, r.Err())
169+
assert.Equal(t, "OK", r.Val())
170+
// validate there is exp set on the key and it is new
171+
secondExp := rdb.ExpireTime(ctx, testKey)
172+
secondExpireTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC).Add(secondExp.Val()).Unix()
173+
assert.NotEqual(t, -1, secondExpireTime)
174+
assert.Greater(t, secondExpireTime, time.Now().Unix())
175+
assert.Greater(t, secondExpireTime, firstExpireTime)
176+
// validate we updated the key/vals
177+
values = rdb.HGetAll(ctx, testKey)
178+
assert.Equal(t, 4, len(values.Val()))
179+
180+
//remove expiration on the key and verify
181+
r = rdb.Do(ctx, "persist", testKey)
182+
require.NoError(t, r.Err())
183+
persist := rdb.ExpireTime(ctx, testKey)
184+
assert.Equal(t, -1*time.Nanosecond, persist.Val())
185+
// validate we still have the correct number of key/vals
186+
values = rdb.HGetAll(ctx, testKey)
187+
assert.Equal(t, 4, len(values.Val()))
188+
})
189+
190+
t.Run("HSETEXPIRE/HLEN/EXPIRETIME - Small hash creation", func(t *testing.T) {
191+
ttlStr := "3600"
192+
testKey := "hsetexsmallhash"
193+
hsetExSmallHash := make(map[string]string)
194+
for i := 0; i < 8; i++ {
195+
key := "__avoid_collisions__" + util.RandString(0, 8, util.Alpha)
196+
val := "__avoid_collisions__" + util.RandString(0, 8, util.Alpha)
197+
if _, ok := hsetExSmallHash[key]; ok {
198+
i--
199+
}
200+
rdb.Do(ctx, "hsetexpire", testKey, ttlStr, key, val)
201+
hsetExSmallHash[key] = val
202+
}
203+
require.Equal(t, int64(8), rdb.HLen(ctx, testKey).Val())
204+
val := rdb.ExpireTime(ctx, testKey).Val()
205+
expireTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC).Add(val).Unix()
206+
require.Greater(t, expireTime, time.Now().Unix())
207+
})
208+
209+
t.Run("HSETEXPIRE/HLEN/EXPIRETIME - Big hash creation", func(t *testing.T) {
210+
ttlStr := "3600"
211+
testKey := "hsetexbighash"
212+
hsetExBigHash := make(map[string]string)
213+
for i := 0; i < 1024; i++ {
214+
key := "__avoid_collisions__" + util.RandString(0, 8, util.Alpha)
215+
val := "__avoid_collisions__" + util.RandString(0, 8, util.Alpha)
216+
if _, ok := hsetExBigHash[key]; ok {
217+
i--
218+
}
219+
rdb.Do(ctx, "hsetexpire", testKey, ttlStr, key, val)
220+
hsetExBigHash[key] = val
221+
}
222+
require.Equal(t, int64(1024), rdb.HLen(ctx, testKey).Val())
223+
val := rdb.ExpireTime(ctx, testKey).Val()
224+
expireTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC).Add(val).Unix()
225+
require.Greater(t, expireTime, time.Now().Unix())
226+
})
227+
228+
t.Run("HSETEXPIRE/HLEN/EXPIRETIME - Multi field-value pairs creation", func(t *testing.T) {
229+
ttlStr := "3600"
230+
testKey := "hsetexbighashPair"
231+
hsetExBigHash := make(map[string]string)
232+
cmd := []string{"hsetexpire", testKey, ttlStr}
233+
for i := 0; i < 10; i++ {
234+
key := "__avoid_collisions__" + util.RandString(0, 8, util.Alpha)
235+
val := "__avoid_collisions__" + util.RandString(0, 8, util.Alpha)
236+
if _, ok := hsetExBigHash[key]; ok {
237+
i--
238+
}
239+
cmd = append(cmd, key, val)
240+
hsetExBigHash[key] = val
241+
}
242+
args := make([]interface{}, len(cmd))
243+
for i, v := range cmd {
244+
args[i] = v
245+
}
246+
rdb.Do(ctx, args...)
247+
require.Equal(t, int64(10), rdb.HLen(ctx, testKey).Val())
248+
val := rdb.ExpireTime(ctx, testKey).Val()
249+
expireTime := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC).Add(val).Unix()
250+
require.Greater(t, expireTime, time.Now().Unix())
251+
})
252+
121253
t.Run("HGET against the small hash", func(t *testing.T) {
122254
var err error
123255
for key, val := range smallhash {

0 commit comments

Comments
 (0)