Skip to content

Commit 8a17d8d

Browse files
author
backport test
committed
fix(generic): stop RM from freeing a container under a yielding iteration
1 parent 0c8f126 commit 8a17d8d

7 files changed

Lines changed: 349 additions & 17 deletions

File tree

src/server/container_utils.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ using namespace std;
161161
bool IterateList(const PrimeValue& pv, const IterateFunc& func, size_t start, size_t end,
162162
bool allow_yield) {
163163
DCHECK_LE(start, end);
164+
ScopedValueIteration iteration_guard{pv};
164165
bool success = true;
165166
size_t len = pv.Size();
166167
if (len == 0) {
@@ -211,6 +212,7 @@ bool IterateList(const PrimeValue& pv, const IterateFunc& func, size_t start, si
211212
}
212213

213214
bool IterateSet(const PrimeValue& pv, const IterateFunc& func, bool allow_yield) {
215+
ScopedValueIteration iteration_guard{pv};
214216
bool success = true;
215217
if (pv.Encoding() == kEncodingIntSet) {
216218
intset* is = static_cast<intset*>(pv.RObjPtr());
@@ -242,6 +244,7 @@ bool IterateSet(const PrimeValue& pv, const IterateFunc& func, bool allow_yield)
242244

243245
bool IterateSortedSet(const PrimeValue& pv, const IterateSortedFunc& func, size_t start, size_t end,
244246
bool reverse, bool use_score, bool allow_yield) {
247+
ScopedValueIteration iteration_guard{pv};
245248
size_t llen = pv.Size();
246249
if (llen == 0)
247250
return true;
@@ -305,6 +308,7 @@ bool IterateSortedSet(const PrimeValue& pv, const IterateSortedFunc& func, size_
305308
}
306309

307310
bool IterateMap(const PrimeValue& pv, const IterateKVFunc& func, bool allow_yield) {
311+
ScopedValueIteration iteration_guard{pv};
308312
bool finished = true;
309313

310314
if (pv.Encoding() == kEncodingListPack) {

src/server/db_slice.cc

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,19 +1462,26 @@ DbSlice::Iterator DbSlice::ExpireIfNeeded(const Context& cntx, Iterator it) cons
14621462
return Iterator::FromPrime(ExpireIfNeeded(cntx, it.GetInnerIt()));
14631463
}
14641464

1465+
bool DbSlice::WillExpire(const Context& cntx, PrimeIterator it) const {
1466+
if (!it->first.HasExpire())
1467+
return false;
1468+
1469+
int64_t expire_time = it->first.GetExpireTime();
1470+
1471+
// Never do expiration if expiration is disabled, or on replicas unless replica_delete_expired
1472+
// is enabled (which allows replicas to proactively delete expired keys on the read path).
1473+
return int64_t(cntx.time_now_ms) >= expire_time && expire_allowed_ &&
1474+
(!owner_->IsReplica() || absl::GetFlag(FLAGS_replica_delete_expired));
1475+
}
1476+
14651477
PrimeIterator DbSlice::ExpireIfNeeded(const Context& cntx, PrimeIterator it,
14661478
vector<string>* events) const {
14671479
if (!it->first.HasExpire()) {
14681480
LOG(DFATAL) << "Invalid call to ExpireIfNeeded";
14691481
return it;
14701482
}
14711483

1472-
int64_t expire_time = it->first.GetExpireTime();
1473-
1474-
// Never do expiration if expiration is disabled, or on replicas unless replica_delete_expired
1475-
// is enabled (which allows replicas to proactively delete expired keys on the read path).
1476-
if (int64_t(cntx.time_now_ms) < expire_time || !expire_allowed_ ||
1477-
(owner_->IsReplica() && !absl::GetFlag(FLAGS_replica_delete_expired))) {
1484+
if (!WillExpire(cntx, it)) {
14781485
return it;
14791486
}
14801487

src/server/db_slice.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,9 @@ class DbSlice {
421421
// and returns Iterator{}.
422422
Iterator ExpireIfNeeded(const Context& cntx, Iterator it) const;
423423

424+
// Whether ExpireIfNeeded() would erase 'it' right now.
425+
bool WillExpire(const Context& cntx, PrimeIterator it) const;
426+
424427
// Iterate over all expire table entries and delete expired.
425428
void ExpireAllIfNeeded();
426429

src/server/generic_family.cc

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,11 @@ bool ScanCb(const OpArgs& op_args, PrimeIterator prime_it, const ScanOpts& opts,
649649

650650
DbSlice::Iterator it = DbSlice::Iterator::FromPrime(prime_it);
651651
if (prime_it->first.HasExpire()) {
652+
// Expiring frees the value and this scan holds no key lock, so a parked walk would be left
653+
// pointing into it. Only a key that is expiring anyway may be skipped - it is gone either
654+
// way, and the heartbeat will free it once the walk ends.
655+
if (db_slice.WillExpire(op_args.db_cntx, prime_it) && IsValueIterated(prime_it->second))
656+
return false;
652657
it = db_slice.ExpireIfNeeded(op_args.db_cntx, it);
653658
if (!IsValid(it))
654659
return false;
@@ -775,28 +780,82 @@ uint64_t ScanGeneric(uint64_t cursor, const ScanOpts& scan_opts, StringVec* keys
775780
return cursor;
776781
}
777782

778-
void OpScanAndDelete(const OpArgs& op_args, const ScanOpts& scan_opts, uint64_t* cursor,
779-
uint32_t* deleted) {
783+
// A fiber parked inside container_utils::Iterate* holds raw pointers into the value and RM holds
784+
// no key lock, so freeing it here is a use-after-free. kIterated means the key is still there and
785+
// the caller must not let the cursor move past it.
786+
enum class DeleteScannedResult { kDeleted, kMissing, kIterated };
787+
788+
DeleteScannedResult DeleteScannedKey(const OpArgs& op_args, string_view key, uint64_t deadline_ms) {
789+
auto& db_slice = op_args.GetDbSlice();
790+
791+
while (true) {
792+
// Raw peek: DbSlice::Find* would lazily expire - that is, free - the very value we protect,
793+
// and would re-run the change callbacks on every retry.
794+
PrimeIterator peek = db_slice.GetTables(op_args.db_cntx.db_index)->Find(key);
795+
if (!IsValid(peek))
796+
return DeleteScannedResult::kMissing;
797+
798+
if (!IsValueIterated(peek->second)) {
799+
auto res = db_slice.FindMutable(op_args.db_cntx, key);
800+
if (!IsValid(res.it))
801+
return DeleteScannedResult::kMissing;
802+
803+
if (!IsValueIterated(res.it->second)) { // FindMutable preempts, so re-check before freeing
804+
db_slice.DelMutable(op_args.db_cntx, std::move(res));
805+
if (op_args.shard->journal()) {
806+
RecordDelete(op_args.db_cntx.db_index, key);
807+
}
808+
return DeleteScannedResult::kDeleted;
809+
}
810+
res.post_updater.Cancel(); // nothing changed, do not wake its watchers
811+
}
812+
813+
if (GetCurrentTimeMs() > deadline_ms)
814+
return DeleteScannedResult::kIterated;
815+
// Sleep rather than yield: a yield loop keeps this proactor's ready queue non-empty, so it
816+
// stops polling and nothing timer-driven on this shard fires until the wait is over.
817+
util::ThisFiber::SleepFor(std::chrono::microseconds(100));
818+
}
819+
}
820+
821+
// Returns false if a key could not be deleted before the deadline. *cursor is then rewound to
822+
// where this scan started, so the next pass sees that key again instead of losing it behind a
823+
// cursor OpScan already advanced. Rescanning is idempotent - what we deleted no longer matches.
824+
bool OpScanAndDelete(const OpArgs& op_args, const ScanOpts& scan_opts, uint64_t* cursor,
825+
uint32_t* deleted, uint64_t deadline_ms) {
826+
const uint64_t scan_start = *cursor;
780827
StringVec keys;
781828
OpScan(op_args, scan_opts, cursor, &keys);
782829

783-
auto& db_slice = op_args.GetDbSlice();
784830
uint32_t count = 0;
831+
bool complete = true;
785832
for (const auto& key : keys) {
786-
auto it = db_slice.FindMutable(op_args.db_cntx, key).it;
787-
if (!IsValid(it))
788-
continue;
789-
db_slice.Del(op_args.db_cntx, it);
790-
if (op_args.shard->journal()) {
791-
RecordDelete(op_args.db_cntx.db_index, key);
833+
switch (DeleteScannedKey(op_args, key, deadline_ms)) {
834+
case DeleteScannedResult::kDeleted:
835+
++count;
836+
break;
837+
case DeleteScannedResult::kMissing:
838+
break;
839+
case DeleteScannedResult::kIterated:
840+
complete = false; // the rest of the batch is still worth deleting
841+
break;
792842
}
793-
++count;
794843
}
795844
*deleted += count;
845+
846+
if (!complete)
847+
*cursor = scan_start;
848+
return complete;
796849
}
797850

798851
uint64_t RmGeneric(uint64_t cursor, const ScanOpts& scan_opts, uint32_t* deleted,
799852
ConnectionContext* cntx) {
853+
// A returned cursor of 0 means the pass is over, so it can not also mean "resume at shard 0,
854+
// bucket 0" - which is what we must say when we give up on a key in shard 0's first batch.
855+
// A dash token stays below 2^40 and we shift it by 10, so the high bits are ours.
856+
constexpr uint64_t kResumeBit = 1ULL << 62;
857+
cursor &= ~kResumeBit;
858+
800859
ShardId sid = cursor % 1024;
801860

802861
EngineShardSet* ess = shard_set;
@@ -811,13 +870,15 @@ uint64_t RmGeneric(uint64_t cursor, const ScanOpts& scan_opts, uint32_t* deleted
811870

812871
cursor >>= 10;
813872
DbContext db_cntx{cntx->ns, cntx->conn_state.db_index, GetCurrentTimeMs()};
873+
const uint64_t deadline_ms = db_cntx.time_now_ms + kMaxRmTimeMs;
814874

815875
*deleted = 0;
876+
bool complete = true;
816877

817878
do {
818879
auto cb = [&] {
819880
OpArgs op_args{EngineShard::tlocal(), nullptr, db_cntx};
820-
OpScanAndDelete(op_args, scan_opts, &cursor, deleted);
881+
complete = OpScanAndDelete(op_args, scan_opts, &cursor, deleted, deadline_ms);
821882
};
822883

823884
if (EngineShard::tlocal() && EngineShard::tlocal()->shard_id() == sid) {
@@ -827,6 +888,9 @@ uint64_t RmGeneric(uint64_t cursor, const ScanOpts& scan_opts, uint32_t* deleted
827888
ess->Await(sid, cb);
828889
}
829890

891+
if (!complete) // cursor was rewound; do not step past a key we failed to delete
892+
break;
893+
830894
if (cursor == 0) {
831895
++sid;
832896
if (unsigned(sid) == shard_count)
@@ -841,6 +905,8 @@ uint64_t RmGeneric(uint64_t cursor, const ScanOpts& scan_opts, uint32_t* deleted
841905

842906
if (sid < shard_count) {
843907
cursor = (cursor << 10) | sid;
908+
if (cursor == 0) // gave up in shard 0's first batch; a bare 0 would read as "done"
909+
cursor = kResumeBit;
844910
} else {
845911
DCHECK_EQ(0u, cursor);
846912
}

0 commit comments

Comments
 (0)