@@ -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
798851uint64_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