mirror of
https://github.com/dragonflydb/dragonfly.git
synced 2025-05-11 18:35:46 +02:00
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:
parent
6c1e9fcefc
commit
c6946d561c
7 changed files with 110 additions and 50 deletions
|
@ -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_));
|
||||
|
|
|
@ -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)};
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue