This is an automated email from the ASF dual-hosted git repository.

aleksraiden pushed a commit to branch unstable
in repository https://gitbox.apache.org/repos/asf/kvrocks.git


The following commit(s) were added to refs/heads/unstable by this push:
     new 4d364df9c fix: XDEL double-counting repeated IDs and stream length 
underflow (#3701)
4d364df9c is described below

commit 4d364df9c59afd6e71b6adaf8973f3905b48fd2f
Author: GAURAV NAGALAPURAM VENKATARAMANAN <[email protected]>
AuthorDate: Wed Oct 21 10:34:47 2026 -0700

    fix: XDEL double-counting repeated IDs and stream length underflow (#3701)
    
    Stream::DeleteEntries counted a repeated ID once per occurrence because
    the read did not see deletes staged in the same batch, desyncing
    metadata.size and underflowing it to 2^64-1. Dedupe IDs per call and
    clamp the size subtraction.
    
    Fixes #3700
    
    AI-use disclosure
    An AI assistant (Muse) was used during this contribution. It helped
    with:
    - Verifying the root cause in Stream::DeleteEntries against the reported
    issue
    - Suggesting the fix approach (deduplicating IDs within a call, clamping
    the length subtraction against underflow)
    -  Reviewing the implementation
    -  Drafting the Go regression test
    
    The C++ implementation was written by me, and I verified the logic
    against the issue's reproduction cases.
    Parts needing extra review: the regression test has not been executed
    locally yet — my macOS build is blocked by an unrelated range-v3/libc
    toolchain incompatibility in the repo's pinned dependencies, so the test
    relies on CI validation once the workflows are approved.
    
    ---------
    
    Co-authored-by: Aleks Lozovyuk <[email protected]>
---
 src/types/redis_stream.cc                    |  8 +++++++-
 tests/gocase/unit/type/stream/stream_test.go | 14 ++++++++++++++
 2 files changed, 21 insertions(+), 1 deletion(-)

diff --git a/src/types/redis_stream.cc b/src/types/redis_stream.cc
index 66e05bbda..f506d9b00 100644
--- a/src/types/redis_stream.cc
+++ b/src/types/redis_stream.cc
@@ -23,6 +23,7 @@
 #include <rocksdb/status.h>
 
 #include <memory>
+#include <set>
 #include <utility>
 #include <vector>
 
@@ -1017,7 +1018,12 @@ rocksdb::Status Stream::DeleteEntries(engine::Context 
&ctx, const Slice &stream_
 
   auto iter = util::UniqueIterator(ctx, read_options, stream_cf_handle_);
 
+  std::set<StreamEntryID> seen_ids;
   for (const auto &id : ids) {
+    if (!seen_ids.insert(id).second) {
+      continue;
+    }
+
     std::string entry_key = internalKeyFromEntryID(ns_key, metadata, id);
     std::string value;
     s = storage_->Get(ctx, read_options, stream_cf_handle_, entry_key, &value);
@@ -1062,7 +1068,7 @@ rocksdb::Status Stream::DeleteEntries(engine::Context 
&ctx, const Slice &stream_
   }
 
   if (*deleted_cnt > 0) {
-    metadata.size -= *deleted_cnt;
+    metadata.size = *deleted_cnt > metadata.size ? 0 : metadata.size - 
*deleted_cnt;
 
     std::string bytes;
     metadata.Encode(&bytes);
diff --git a/tests/gocase/unit/type/stream/stream_test.go 
b/tests/gocase/unit/type/stream/stream_test.go
index 3560ff066..0ed58f171 100644
--- a/tests/gocase/unit/type/stream/stream_test.go
+++ b/tests/gocase/unit/type/stream/stream_test.go
@@ -504,6 +504,20 @@ var streamTests = func(t *testing.T, configs 
util.KvrocksServerConfigs) {
                require.Subset(t, items[0].Values, 
map[string]interface{}{"foo": "value0"})
                require.Subset(t, items[1].Values, 
map[string]interface{}{"foo": "value2"})
        })
+       t.Run("XDEL with repeated IDs counts each entry once", func(t 
*testing.T) {
+               require.NoError(t, rdb.Del(ctx, "somestream").Err())
+               require.NoError(t, rdb.XAdd(ctx, &redis.XAddArgs{Stream: 
"somestream", ID: "1-0", Values: []string{"foo", "value0"}}).Err())
+               require.NoError(t, rdb.XAdd(ctx, &redis.XAddArgs{Stream: 
"somestream", ID: "2-0", Values: []string{"foo", "value1"}}).Err())
+               // A repeated ID must be counted once; the stream length must 
stay consistent.
+               require.EqualValues(t, 1, rdb.XDel(ctx, "somestream", "1-0", 
"1-0").Val())
+               require.EqualValues(t, 1, rdb.XLen(ctx, "somestream").Val())
+               items := rdb.XRange(ctx, "somestream", "-", "+").Val()
+               require.Len(t, items, 1)
+               require.Equal(t, "2-0", items[0].ID)
+               // Deleting the last entry with repeats must not underflow the 
length.
+               require.EqualValues(t, 1, rdb.XDel(ctx, "somestream", "2-0", 
"2-0", "2-0").Val())
+               require.EqualValues(t, 0, rdb.XLen(ctx, "somestream").Val())
+       })
 
        // Here the idea is to check the consistency of the stream data 
structure as we remove all the elements down to zero elements.
        t.Run("XDEL fuzz test", func(t *testing.T) {

Reply via email to