feat(server): SCAN command add ATTR options (#4766)

This commit is contained in:
lichuang 2025-04-01 19:07:37 +08:00 committed by GitHub
parent 3829ca87d7
commit 28fa1783db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 76 additions and 1 deletions

View file

@ -308,6 +308,19 @@ OpResult<ScanOpts> ScanOpts::TryFrom(CmdArgList args) {
if (!absl::SimpleAtoi(ArgS(args, i + 1), &scan_opts.bucket_id)) {
return facade::OpStatus::INVALID_INT;
}
} else if (opt == "ATTR") {
string_view mask = ArgS(args, i + 1);
if (mask == "v") {
scan_opts.mask = ScanOpts::Mask::Volatile;
} else if (mask == "p") {
scan_opts.mask = ScanOpts::Mask::Permanent;
} else if (mask == "a") {
scan_opts.mask = ScanOpts::Mask::Accessed;
} else if (mask == "u") {
scan_opts.mask = ScanOpts::Mask::Untouched;
} else {
return facade::OpStatus::SYNTAX_ERR;
}
} else {
return facade::OpStatus::SYNTAX_ERR;
}

View file

@ -312,6 +312,14 @@ struct ScanOpts {
size_t limit = 10;
std::optional<CompactObjType> type_filter;
unsigned bucket_id = UINT_MAX;
enum class Mask {
Volatile, // volatile, keys that have ttl
Permanent, // permanent, keys that do not have ttl
Accessed, // accessed, the key has been accessed since the last load/flush event, or the last
// time a flag was reset.
Untouched, // untouched, the key has not been accessed/touched.
};
std::optional<Mask> mask;
bool Matches(std::string_view val_name) const;
static OpResult<ScanOpts> TryFrom(CmdArgList args);

View file

@ -599,7 +599,17 @@ bool ScanCb(const OpArgs& op_args, PrimeIterator prime_it, const ScanOpts& opts,
}
bool matches = !opts.type_filter || it->second.ObjType() == opts.type_filter;
if (opts.mask.has_value()) {
if (opts.mask == ScanOpts::Mask::Volatile) {
matches &= it->second.HasExpire();
} else if (opts.mask == ScanOpts::Mask::Permanent) {
matches &= !it->second.HasExpire();
} else if (opts.mask == ScanOpts::Mask::Accessed) {
matches &= it->first.WasTouched();
} else if (opts.mask == ScanOpts::Mask::Untouched) {
matches &= !it->first.WasTouched();
}
}
if (!matches)
return false;
@ -1747,6 +1757,7 @@ void GenericFamily::Echo(CmdArgList args, const CommandContext& cmd_cntx) {
}
// SCAN cursor [MATCH <glob>] [TYPE <type>] [COUNT <count>] [BUCKET <bucket_id>]
// [ATTR <mask>]
void GenericFamily::Scan(CmdArgList args, const CommandContext& cmd_cntx) {
string_view token = ArgS(args, 0);
uint64_t cursor = 0;

View file

@ -412,6 +412,49 @@ TEST_F(GenericFamilyTest, Scan) {
EXPECT_EQ(resp, "");
}
TEST_F(GenericFamilyTest, ScanWithAttr) {
Run({"flushdb"});
Run({"set", "hello", "world"});
Run({"set", "foo", "bar"});
Run({"expire", "hello", "1000"});
auto resp = Run({"scan", "0", "attr", "v"});
auto vec = StrArray(resp.GetVec()[1]);
ASSERT_EQ(1, vec.size());
EXPECT_EQ(vec[0], "hello");
resp = Run({"scan", "0", "attr", "p"});
vec = StrArray(resp.GetVec()[1]);
ASSERT_EQ(1, vec.size());
EXPECT_EQ(vec[0], "foo");
// before run get "foo", scan with a attr should return "hello", because set "hello" expire before
resp = Run({"scan", "0", "attr", "a"});
vec = StrArray(resp.GetVec()[1]);
ASSERT_EQ(1, vec.size());
EXPECT_EQ(vec[0], "hello");
// before run get "foo", scan with a attr should return "foo"
resp = Run({"scan", "0", "attr", "u"});
vec = StrArray(resp.GetVec()[1]);
ASSERT_EQ(1, vec.size());
EXPECT_EQ(vec[0], "foo");
ASSERT_THAT(Run({"get", "foo"}), "bar");
// after run get "foo", scan with a attr should return "foo" and "hello"
resp = Run({"scan", "0", "attr", "a"});
vec = StrArray(resp.GetVec()[1]);
ASSERT_EQ(2, vec.size());
// after run get "foo", scan with a attr should return empty set
resp = Run({"scan", "0", "attr", "u"});
vec = StrArray(resp.GetVec()[1]);
ASSERT_EQ(0, vec.size());
}
TEST_F(GenericFamilyTest, Sort) {
// Test list sort with params
Run({"del", "list-1"});