feat: add fieldttl command that returns the ttl of the member (#2026)

Currently implemented only for saddex.

Signed-off-by: Roman Gershman <roman@dragonflydb.io>
This commit is contained in:
Roman Gershman 2023-10-16 12:12:00 +03:00 committed by GitHub
parent 6c1e9fcefc
commit c6946d561c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 110 additions and 50 deletions

View file

@ -57,6 +57,7 @@ void DenseSet::IteratorBase::Advance() {
++curr_list_;
if (curr_list_ == owner_->entries_.end()) {
curr_entry_ = nullptr;
owner_ = nullptr;
return;
}
owner_->ExpireIfNeeded(nullptr, &(*curr_list_));

View file

@ -58,7 +58,7 @@ class StringSet : public DenseSet {
iterator() : IteratorBase() {
}
iterator(DenseSet* set, bool is_end) : IteratorBase(set, is_end) {
iterator(DenseSet* set) : IteratorBase(set, false) {
}
iterator& operator++() {
@ -67,7 +67,10 @@ class StringSet : public DenseSet {
}
bool operator==(const iterator& b) const {
return curr_list_ == b.curr_list_;
if (owner_ == nullptr && b.owner_ == nullptr) { // to allow comparison with end()
return true;
}
return owner_ == b.owner_ && curr_entry_ == b.curr_entry_;
}
bool operator!=(const iterator& b) const {
@ -86,57 +89,14 @@ class StringSet : public DenseSet {
using IteratorBase::HasExpiry;
};
class const_iterator : private IteratorBase {
public:
using iterator_category = std::input_iterator_tag;
using value_type = const char*;
using pointer = value_type*;
using reference = value_type&;
const_iterator() : IteratorBase() {
}
const_iterator(const DenseSet* set, bool is_end) : IteratorBase(set, is_end) {
}
const_iterator& operator++() {
Advance();
return *this;
}
bool operator==(const const_iterator& b) const {
return curr_list_ == b.curr_list_;
}
bool operator!=(const const_iterator& b) const {
return !(*this == b);
}
value_type operator*() const {
return (value_type)curr_entry_->GetObject();
}
value_type operator->() const {
return (value_type)curr_entry_->GetObject();
}
};
iterator begin() {
return iterator{this, false};
return iterator{this};
}
iterator end() {
return iterator{this, true};
return iterator{};
}
/*
const_iterator cbegin() const {
return const_iterator{this, false};
}
const_iterator cend() const {
return const_iterator{this, true};
}
*/
uint32_t Scan(uint32_t, const std::function<void(sds)>&) const;
iterator Find(std::string_view member) {
return iterator{FindIt(&member, 1)};

View file

@ -24,6 +24,7 @@ extern "C" {
#include "server/rdb_extensions.h"
#include "server/rdb_load.h"
#include "server/rdb_save.h"
#include "server/set_family.h"
#include "server/transaction.h"
#include "util/varz.h"
@ -618,6 +619,27 @@ OpStatus OpExpire(const OpArgs& op_args, string_view key, const DbSlice::ExpireP
return res.status();
}
// returns -2 if the key was not found, -3 if the field was not found,
// -1 if ttl on the field was not found.
OpResult<long> OpFieldTtl(Transaction* t, EngineShard* shard, string_view key, string_view field) {
auto& db_slice = shard->db_slice();
const DbContext& db_cntx = t->GetDbContext();
auto [it, expire_it] = db_slice.FindExt(db_cntx, key);
if (!IsValid(it))
return -2;
if (it->second.ObjType() != OBJ_SET) // TODO: to finish for hashes.
return OpStatus::WRONG_TYPE;
if (it->second.ObjType() == OBJ_SET) {
int32_t res = SetFamily::FieldExpireTime(db_cntx, it->second, field);
return res <= 0 ? res : int32_t(res - MemberTimeSeconds(db_cntx.time_now_ms));
}
// TODO: to finish with hash family.
return OpStatus::INVALID_VALUE;
}
} // namespace
void GenericFamily::Init(util::ProactorPool* pp) {
@ -1060,6 +1082,24 @@ void GenericFamily::Restore(CmdArgList args, ConnectionContext* cntx) {
}
}
// Returns -2 if key not found, WRONG_TYPE if key is not a set or hash
// -1 if the field does not have associated TTL on it, and -3 if field is not found.
void GenericFamily::FieldTtl(CmdArgList args, ConnectionContext* cntx) {
string_view key = ArgS(args, 0);
string_view field = ArgS(args, 1);
auto cb = [&](Transaction* t, EngineShard* shard) { return OpFieldTtl(t, shard, key, field); };
OpResult<long> result = cntx->transaction->ScheduleSingleHopT(std::move(cb));
if (result) {
(*cntx)->SendLong(*result);
return;
}
(*cntx)->SendError(result.status());
}
void GenericFamily::Move(CmdArgList args, ConnectionContext* cntx) {
string_view key = ArgS(args, 0);
string_view target_db_sv = ArgS(args, 1);
@ -1455,6 +1495,7 @@ constexpr uint32_t kSelect = FAST | CONNECTION;
constexpr uint32_t kScan = KEYSPACE | READ | SLOW;
constexpr uint32_t kTTL = KEYSPACE | READ | FAST;
constexpr uint32_t kPTTL = KEYSPACE | READ | FAST;
constexpr uint32_t kFieldTtl = KEYSPACE | READ | FAST;
constexpr uint32_t kTime = FAST;
constexpr uint32_t kType = KEYSPACE | READ | FAST;
constexpr uint32_t kDump = KEYSPACE | READ | SLOW;
@ -1495,6 +1536,7 @@ void GenericFamily::Register(CommandRegistry* registry) {
<< CI{"SCAN", CO::READONLY | CO::FAST | CO::LOADING, -2, 0, 0, 0, acl::kScan}.HFUNC(Scan)
<< CI{"TTL", CO::READONLY | CO::FAST, 2, 1, 1, 1, acl::kTTL}.HFUNC(Ttl)
<< CI{"PTTL", CO::READONLY | CO::FAST, 2, 1, 1, 1, acl::kPTTL}.HFUNC(Pttl)
<< CI{"FIELDTTL", CO::READONLY | CO::FAST, 3, 1, 1, 1, acl::kFieldTtl}.HFUNC(FieldTtl)
<< CI{"TIME", CO::LOADING | CO::FAST, 1, 0, 0, 0, acl::kTime}.HFUNC(Time)
<< CI{"TYPE", CO::READONLY | CO::FAST | CO::LOADING, 2, 1, 1, 1, acl::kType}.HFUNC(Type)
<< CI{"DUMP", CO::READONLY, 2, 1, 1, 1, acl::kDump}.HFUNC(Dump)

View file

@ -60,6 +60,7 @@ class GenericFamily {
static void Type(CmdArgList args, ConnectionContext* cntx);
static void Dump(CmdArgList args, ConnectionContext* cntx);
static void Restore(CmdArgList args, ConnectionContext* cntx);
static void FieldTtl(CmdArgList args, ConnectionContext* cntx);
static OpResult<void> RenameGeneric(CmdArgList args, bool skip_exist_dest,
ConnectionContext* cntx);

View file

@ -602,4 +602,21 @@ TEST_F(GenericFamilyTest, Info) {
EXPECT_EQ(1, get_rdb_changes_since_last_save(resp.GetString()));
}
TEST_F(GenericFamilyTest, FieldTtl) {
TEST_current_time_ms = kMemberExpiryBase * 1000; // to reset to test time.
EXPECT_THAT(Run({"saddex", "key", "1", "val1"}), IntArg(1));
EXPECT_THAT(Run({"saddex", "key", "2", "val2"}), IntArg(1));
EXPECT_EQ(-2, CheckedInt({"fieldttl", "nokey", "val1"})); // key not found
EXPECT_EQ(-3, CheckedInt({"fieldttl", "key", "bar"})); // field not found
EXPECT_EQ(1, CheckedInt({"fieldttl", "key", "val1"}));
EXPECT_EQ(2, CheckedInt({"fieldttl", "key", "val2"}));
AdvanceTime(1100);
EXPECT_EQ(-3, CheckedInt({"fieldttl", "key", "val1"}));
EXPECT_EQ(1, CheckedInt({"fieldttl", "key", "val2"}));
Run({"set", "str", "val"});
EXPECT_THAT(Run({"fieldttl", "str", "bar"}), ErrArg("wrong"));
}
} // namespace dfly

View file

@ -305,6 +305,31 @@ bool IsInSet(const DbContext& db_context, const SetType& st, string_view member)
}
}
// returns -3 if member is not found, -1 if no ttl is associated with this member.
int32_t GetExpiry(const DbContext& db_context, const SetType& st, string_view member) {
if (st.second == kEncodingIntSet) {
long long llval;
if (!string2ll(member.data(), member.size(), &llval))
return -3;
return -1;
}
if (IsDenseEncoding(st)) {
StringSet* ss = (StringSet*)st.first;
ss->set_time(MemberTimeSeconds(db_context.time_now_ms));
auto it = ss->Find(member);
if (it == ss->end())
return -3;
return it.ExpiryTime();
} else {
// Old encoding, does not support expiry.
return -1;
}
}
void FindInSet(StringVec& memberships, const DbContext& db_context, const SetType& st,
const vector<string_view>& members) {
for (const auto& member : members) {
@ -1494,9 +1519,9 @@ void SAddEx(CmdArgList args, ConnectionContext* cntx) {
return (*cntx)->SendError(kInvalidIntErr);
}
vector<string_view> vals(args.size() - 3);
for (size_t i = 3; i < args.size(); ++i) {
vals[i - 3] = ArgS(args, i);
vector<string_view> vals(args.size() - 2);
for (size_t i = 2; i < args.size(); ++i) {
vals[i - 2] = ArgS(args, i);
}
ArgSlice arg_slice{vals.data(), vals.size()};
@ -1628,4 +1653,12 @@ void SetFamily::ConvertTo(const intset* src, dict* dest) {
}
}
int32_t SetFamily::FieldExpireTime(const DbContext& db_context, const PrimeValue& pv,
std::string_view field) {
DCHECK_EQ(OBJ_SET, pv.ObjType());
SetType st{pv.RObjPtr(), pv.Encoding()};
return GetExpiry(db_context, st, field);
}
} // namespace dfly

View file

@ -6,6 +6,7 @@
#include "facade/op_status.h"
#include "server/common.h"
#include "server/table.h"
typedef struct intset intset;
typedef struct redisObject robj;
@ -30,6 +31,11 @@ class SetFamily {
// Returns true if succeeded, false on OOM.
static bool ConvertToStrSet(const intset* is, size_t expected_len, robj* dest);
// returns expiry time in seconds since kMemberExpiryBase date.
// returns -3 if field was not found, -1 if no ttl is associated with the item.
static int32_t FieldExpireTime(const DbContext& db_context, const PrimeValue& pv,
std::string_view field);
private:
};