@@ -54,6 +54,24 @@ bool IsDenseEncoding(const CompactObj& co) {
5454 return co.Encoding () == kEncodingStrMap2 ;
5555}
5656
57+ // After iterating a StringSet with set_time(), lazy member expiry may have emptied it.
58+ // Per Redis semantics empty collections must not exist as keys, so delete the stale key.
59+ // Returns true if the key was deleted.
60+ bool DeleteSetIfEmpty (DbSlice& db_slice, const DbContext& db_cntx, string_view key,
61+ const PrimeValue& pv) {
62+ if (!IsDenseEncoding (pv))
63+ return false ;
64+
65+ if (StringSet* ss = (StringSet*)pv.RObjPtr (); !ss->Empty ())
66+ return false ;
67+
68+ if (auto res = db_slice.FindMutable (db_cntx, key, OBJ_SET); res) {
69+ db_slice.DelMutable (db_cntx, std::move (*res));
70+ return true ;
71+ }
72+ return false ;
73+ }
74+
5775intset* IntsetAddSafe (string_view val, intset* is, bool * success, bool * added) {
5876 long long llval;
5977 *added = false ;
@@ -725,8 +743,9 @@ OpResult<StringVec> OpUnion(const OpArgs& op_args, ShardArgs::Iterator start,
725743 DCHECK (start != end);
726744 absl::flat_hash_set<string> uniques;
727745
746+ auto & db_slice = op_args.GetDbSlice ();
728747 for (; start != end; ++start) {
729- auto find_res = op_args. GetDbSlice () .FindReadOnly (op_args.db_cntx , *start, OBJ_SET);
748+ auto find_res = db_slice .FindReadOnly (op_args.db_cntx , *start, OBJ_SET);
730749 if (find_res) {
731750 const PrimeValue& pv = find_res.value ()->second ;
732751 if (IsDenseEncoding (pv)) {
@@ -737,6 +756,7 @@ OpResult<StringVec> OpUnion(const OpArgs& op_args, ShardArgs::Iterator start,
737756 uniques.emplace (ce.ToString ());
738757 return true ;
739758 });
759+ DeleteSetIfEmpty (db_slice, op_args.db_cntx , *start, pv);
740760 continue ;
741761 }
742762
@@ -772,7 +792,11 @@ OpResult<StringVec> OpDiff(const OpArgs& op_args, ShardArgs::Iterator start,
772792 return true ;
773793 });
774794
775- DCHECK (!uniques.empty ()); // otherwise the key would not exist.
795+ // Lazy per-member TTL expiry during iteration may have emptied the set.
796+ // Delete the stale key and return KEY_NOTFOUND per Redis empty-key semantics.
797+ if (DeleteSetIfEmpty (db_slice, op_args.db_cntx , *start, pv)) {
798+ return OpStatus::KEY_NOTFOUND;
799+ }
776800
777801 for (++start; start != end; ++start) {
778802 auto diff_res = db_slice.FindReadOnly (op_args.db_cntx , *start, OBJ_SET);
@@ -783,7 +807,8 @@ OpResult<StringVec> OpDiff(const OpArgs& op_args, ShardArgs::Iterator start,
783807 continue ; // KEY_NOTFOUND
784808 }
785809
786- SetType st2{diff_res.value ()->second .RObjPtr (), diff_res.value ()->second .Encoding ()};
810+ const PrimeValue& diff_pv = diff_res.value ()->second ;
811+ SetType st2{diff_pv.RObjPtr (), diff_pv.Encoding ()};
787812 if (st2.second == kEncodingIntSet ) {
788813 int ii = 0 ;
789814 intset* is = (intset*)st2.first ;
@@ -796,6 +821,7 @@ OpResult<StringVec> OpDiff(const OpArgs& op_args, ShardArgs::Iterator start,
796821 }
797822 } else {
798823 DiffStrSet (op_args.db_cntx , st2, &uniques);
824+ DeleteSetIfEmpty (db_slice, op_args.db_cntx , *start, diff_pv);
799825 }
800826 }
801827
@@ -830,6 +856,7 @@ OpResult<StringVec> OpInter(const Transaction* t, EngineShard* es, bool remove_f
830856 result.push_back (ce.ToString ());
831857 return true ;
832858 });
859+ DeleteSetIfEmpty (db_slice, t->GetDbContext (), *it, pv);
833860 return result;
834861 }
835862
0 commit comments