2828#include < vector>
2929
3030#include " db_util.h"
31+ #include " scope_exit.h"
3132#include " string_util.h"
3233#include " time_util.h"
3334
@@ -466,6 +467,7 @@ rocksdb::Status Stream::flushPendingNumberUpdates(
466467 engine::Context &ctx, const std::string &ns_key, const StreamMetadata &metadata, rocksdb::WriteBatchBase *batch,
467468 const std::map<std::string, uint64_t > &group_pending_decrements,
468469 const std::map<std::string, std::map<std::string, uint64_t >> &consumer_pending_decrements) {
470+ // Saturate pending counters to tolerate stale metadata without uint64_t underflow.
469471 for (const auto &[group_name, decrement] : group_pending_decrements) {
470472 auto group_key = internalKeyFromGroupName (ns_key, metadata, group_name);
471473 std::string group_value;
@@ -475,7 +477,7 @@ rocksdb::Status Stream::flushPendingNumberUpdates(
475477 }
476478 if (s.ok ()) {
477479 auto group_meta = decodeStreamConsumerGroupMetadataValue (group_value);
478- group_meta.pending_number -= decrement;
480+ group_meta.pending_number = group_meta. pending_number > decrement ? group_meta. pending_number - decrement : 0 ;
479481 s = batch->Put (stream_cf_handle_, group_key, encodeStreamConsumerGroupMetadataValue (group_meta));
480482 if (!s.ok ()) return s;
481483 }
@@ -491,7 +493,8 @@ rocksdb::Status Stream::flushPendingNumberUpdates(
491493 }
492494 if (s.ok ()) {
493495 auto consumer_meta = decodeStreamConsumerMetadataValue (consumer_value);
494- consumer_meta.pending_number -= decrement;
496+ consumer_meta.pending_number =
497+ consumer_meta.pending_number > decrement ? consumer_meta.pending_number - decrement : 0 ;
495498 s = batch->Put (stream_cf_handle_, consumer_key, encodeStreamConsumerMetadataValue (consumer_meta));
496499 if (!s.ok ()) return s;
497500 }
@@ -560,6 +563,11 @@ rocksdb::Status Stream::DeleteEntriesAndAck(engine::Context &ctx, const Slice &s
560563 }
561564
562565 auto batch = storage_->GetWriteBatchBase ();
566+ // PutLogData is added before knowing whether any ID mutates data;
567+ // roll it back on no-op or error to avoid phantom replay.
568+ batch->SetSavePoint ();
569+ auto rollback_batch = MakeScopeExit ([&batch] { batch->RollbackToSavePoint ().PermitUncheckedError (); });
570+
563571 std::string option_str;
564572 switch (option) {
565573 case StreamDeleteOption::DelRef:
@@ -626,11 +634,21 @@ rocksdb::Status Stream::DeleteEntriesAndAck(engine::Context &ctx, const Slice &s
626634 StreamEntryID original_first_entry_id = metadata.first_entry_id ;
627635 StreamEntryID original_last_entry_id = metadata.last_entry_id ;
628636
637+ std::vector<std::string> other_groups;
638+ if (need_groups) {
639+ other_groups.reserve (all_groups.empty () ? 0 : all_groups.size () - 1 );
640+ for (const auto &candidate_group_name : all_groups) {
641+ if (candidate_group_name != group_name) other_groups.push_back (candidate_group_name);
642+ }
643+ }
644+
629645 for (size_t i = 0 ; i < ids.size (); i++) {
630646 const auto &id = ids[i];
631647
632648 std::string entry_key = internalKeyFromEntryID (ns_key, metadata, id);
633649
650+ // Each ID can mutate the stream at most once per command;
651+ // duplicates report not-found after the first attempt.
634652 if (!seen_entry_keys.insert (entry_key).second ) {
635653 (*results)[i] = static_cast <int >(StreamEntryDeleteResult::kEntryNotFound );
636654 continue ;
@@ -663,13 +681,6 @@ rocksdb::Status Stream::DeleteEntriesAndAck(engine::Context &ctx, const Slice &s
663681 }
664682 bool stream_entry_exists = s.ok ();
665683
666- std::vector<std::string> other_groups;
667- if (need_groups) {
668- for (const auto &candidate_group_name : all_groups) {
669- if (candidate_group_name != group_name) other_groups.push_back (candidate_group_name);
670- }
671- }
672-
673684 if (!stream_entry_exists) {
674685 if (option == StreamDeleteOption::DelRef && !other_groups.empty ()) {
675686 s = cleanPelFromAllGroups (ctx, ns_key, metadata, id, batch.Get (), &batch_modified, other_groups,
@@ -777,9 +788,11 @@ rocksdb::Status Stream::DeleteEntriesAndAck(engine::Context &ctx, const Slice &s
777788 if (!s.ok ()) return s;
778789 }
779790
791+ // Saturate pending counters to tolerate stale metadata without uint64_t underflow.
780792 if (acknowledged_cnt > 0 ) {
781793 StreamConsumerGroupMetadata group_metadata = decodeStreamConsumerGroupMetadataValue (get_group_value);
782- group_metadata.pending_number -= acknowledged_cnt;
794+ group_metadata.pending_number =
795+ group_metadata.pending_number > acknowledged_cnt ? group_metadata.pending_number - acknowledged_cnt : 0 ;
783796 std::string group_value = encodeStreamConsumerGroupMetadataValue (group_metadata);
784797 s = batch->Put (stream_cf_handle_, group_key, group_value);
785798 if (!s.ok ()) return s;
@@ -793,7 +806,8 @@ rocksdb::Status Stream::DeleteEntriesAndAck(engine::Context &ctx, const Slice &s
793806 }
794807 if (s.ok ()) {
795808 auto consumer_metadata = decodeStreamConsumerMetadataValue (consumer_meta_original);
796- consumer_metadata.pending_number -= ack_count;
809+ consumer_metadata.pending_number =
810+ consumer_metadata.pending_number > ack_count ? consumer_metadata.pending_number - ack_count : 0 ;
797811 s = batch->Put (stream_cf_handle_, consumer_meta_key, encodeStreamConsumerMetadataValue (consumer_metadata));
798812 if (!s.ok ()) return s;
799813 }
@@ -811,6 +825,10 @@ rocksdb::Status Stream::DeleteEntriesAndAck(engine::Context &ctx, const Slice &s
811825 return rocksdb::Status::OK ();
812826 }
813827
828+ s = batch->PopSavePoint ();
829+ if (!s.ok ()) return s;
830+ rollback_batch.Disable ();
831+
814832 return storage_->Write (ctx, storage_->DefaultWriteOptions (), batch->GetWriteBatch ());
815833}
816834
0 commit comments