Skip to content

Commit 48f8d11

Browse files
authored
fix: crash for lazy expired sets (#6979)
1 parent a90b685 commit 48f8d11

2 files changed

Lines changed: 78 additions & 3 deletions

File tree

src/server/set_family.cc

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
5775
intset* 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

src/server/set_family_test.cc

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,4 +538,52 @@ TEST_F(SetFamilyTest, StoreOverwritesNonSetKeyAccounting) {
538538
EXPECT_EQ(after3.db_stats[0].memory_usage_by_type[OBJ_LIST], 0u);
539539
}
540540

541+
// Regression test for #6973: SDIFF/SDIFFSTORE crash when all set members
542+
// have expired via per-member TTL, leaving the key present but the set empty.
543+
TEST_F(SetFamilyTest, SDiffAllMembersExpired) {
544+
TEST_current_time_ms = kMemberExpiryBase * 1000;
545+
546+
// Add members with a short TTL.
547+
Run({"saddex", "src", "1", "a", "b", "c"});
548+
Run({"sadd", "other", "x"});
549+
550+
// Advance time so all members in "src" expire.
551+
AdvanceTime(2000);
552+
553+
// SDIFF should return empty (like KEY_NOTFOUND), not crash.
554+
auto resp = Run({"sdiff", "src", "other"});
555+
EXPECT_THAT(resp, ArrLen(0));
556+
557+
// The key must be deleted after lazy expiry emptied the set.
558+
EXPECT_THAT(Run({"exists", "src"}), IntArg(0));
559+
560+
// SDIFFSTORE should store nothing and return 0.
561+
Run({"saddex", "src", "1", "a", "b", "c"});
562+
AdvanceTime(2000);
563+
resp = Run({"sdiffstore", "dest", "src", "other"});
564+
EXPECT_THAT(resp, IntArg(0));
565+
EXPECT_THAT(Run({"exists", "src"}), IntArg(0));
566+
}
567+
568+
// Verify key deletion after lazy member expiry for SUNION and SINTER.
569+
TEST_F(SetFamilyTest, SetOpsDeleteEmptyAfterExpiry) {
570+
TEST_current_time_ms = kMemberExpiryBase * 1000;
571+
572+
Run({"saddex", "s1", "1", "a", "b"});
573+
AdvanceTime(2000);
574+
575+
// SUNION triggers iteration which expires all members — key should be deleted.
576+
auto resp = Run({"sunion", "s1"});
577+
EXPECT_THAT(resp, ArrLen(0));
578+
EXPECT_THAT(Run({"exists", "s1"}), IntArg(0));
579+
580+
Run({"saddex", "s2", "1", "a", "b"});
581+
AdvanceTime(2000);
582+
583+
// SINTER single-key path — same behavior.
584+
resp = Run({"sinter", "s2"});
585+
EXPECT_THAT(resp, ArrLen(0));
586+
EXPECT_THAT(Run({"exists", "s2"}), IntArg(0));
587+
}
588+
541589
} // namespace dfly

0 commit comments

Comments
 (0)