diff --git a/src/core/dense_set.cc b/src/core/dense_set.cc index 6a4b43c0614f..428b2302bfc9 100644 --- a/src/core/dense_set.cc +++ b/src/core/dense_set.cc @@ -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_)); diff --git a/src/core/string_set.h b/src/core/string_set.h index b3dcd0f2cd50..51ee9e881214 100644 --- a/src/core/string_set.h +++ b/src/core/string_set.h @@ -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&) const; iterator Find(std::string_view member) { return iterator{FindIt(&member, 1)}; diff --git a/src/server/generic_family.cc b/src/server/generic_family.cc index e8d9a61feb68..dea64de56bdb 100644 --- a/src/server/generic_family.cc +++ b/src/server/generic_family.cc @@ -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 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 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) diff --git a/src/server/generic_family.h b/src/server/generic_family.h index 4e3e2d9e1fed..82a1c4c8c49c 100644 --- a/src/server/generic_family.h +++ b/src/server/generic_family.h @@ -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 RenameGeneric(CmdArgList args, bool skip_exist_dest, ConnectionContext* cntx); diff --git a/src/server/generic_family_test.cc b/src/server/generic_family_test.cc index 7f7d743d4366..a831e1f924d6 100644 --- a/src/server/generic_family_test.cc +++ b/src/server/generic_family_test.cc @@ -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 diff --git a/src/server/set_family.cc b/src/server/set_family.cc index 8d517c4a2874..5419105eeb8f 100644 --- a/src/server/set_family.cc +++ b/src/server/set_family.cc @@ -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& members) { for (const auto& member : members) { @@ -1494,9 +1519,9 @@ void SAddEx(CmdArgList args, ConnectionContext* cntx) { return (*cntx)->SendError(kInvalidIntErr); } - vector vals(args.size() - 3); - for (size_t i = 3; i < args.size(); ++i) { - vals[i - 3] = ArgS(args, i); + vector 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 diff --git a/src/server/set_family.h b/src/server/set_family.h index ac2e5698bc0f..545d50a8cc6e 100644 --- a/src/server/set_family.h +++ b/src/server/set_family.h @@ -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: };