Skip to content

Commit d2839d9

Browse files
committed
fix(stream): skip metadata subkeys during boundary recalculation
1 parent 7b2ab7f commit d2839d9

5 files changed

Lines changed: 67 additions & 4 deletions

File tree

src/commands/cmd_stream.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,10 @@ class CommandXDelEx : public Commander {
309309
entry_ids_.emplace_back(id);
310310
}
311311

312+
if (parser.Good()) {
313+
return parser.InvalidSyntax();
314+
}
315+
312316
return Status::OK();
313317
}
314318

src/storage/batch_extractor.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ rocksdb::Status WriteBatchExtractor::PutCF(uint32_t column_family_id, const Slic
270270
Slice entry_id_check = ikey.GetSubKey();
271271
uint64_t delimiter = 0;
272272
GetFixed64(&entry_id_check, &delimiter);
273+
// Non-entry stream metadata uses UINT64_MAX-prefixed subkeys.
273274
if (delimiter == UINT64_MAX) {
274275
return rocksdb::Status::OK();
275276
}
@@ -406,6 +407,7 @@ rocksdb::Status WriteBatchExtractor::DeleteCF(uint32_t column_family_id, const S
406407
redis::StreamEntryID entry_id;
407408
GetFixed64(&encoded_id, &entry_id.ms);
408409

410+
// Non-entry stream metadata uses UINT64_MAX-prefixed subkeys.
409411
if (entry_id.ms == UINT64_MAX) {
410412
return rocksdb::Status::OK();
411413
}

src/types/redis_stream.cc

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -416,8 +416,9 @@ rocksdb::Status Stream::getGroupNames(engine::Context &ctx, const std::string &n
416416

417417
auto iter = util::UniqueIterator(ctx, read_options, stream_cf_handle_);
418418
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
419+
// Group metadata keys sort before consumer/PEL keys, so no more groups once we see a different type.
419420
if (identifySubkeyType(iter->key()) != StreamSubkeyType::StreamConsumerGroupMetadata) {
420-
continue;
421+
break;
421422
}
422423
group_names->push_back(groupNameFromInternalKey(iter->key()));
423424
}
@@ -452,6 +453,7 @@ rocksdb::Status Stream::processOneEntryDeletion(
452453
}
453454

454455
if (s.IsNotFound()) {
456+
// DelRef cleans orphan PELs; ACKED preserves them without the entry.
455457
if (option == StreamDeleteOption::DelRef) {
456458
s = cleanPelFromAllGroups(ctx, ns_key, *metadata, id, batch, batch_modified, all_groups, group_pending_decrements,
457459
consumer_pending_decrements);
@@ -683,7 +685,9 @@ rocksdb::Status Stream::DeleteEntriesWithOption(engine::Context &ctx, const Slic
683685

684686
if (first_deleted) {
685687
iter->SeekToFirst();
686-
while (iter->Valid() && deleted_entry_keys.count(iter->key().ToString()) > 0) {
688+
// Skip non-entry subkeys (group/consumer/PEL metadata) when recalculating stream boundaries.
689+
while (iter->Valid() && (identifySubkeyType(iter->key()) != StreamSubkeyType::StreamEntry ||
690+
deleted_entry_keys.count(iter->key().ToString()) > 0)) {
687691
iter->Next();
688692
}
689693
if (iter->Valid()) {
@@ -696,7 +700,9 @@ rocksdb::Status Stream::DeleteEntriesWithOption(engine::Context &ctx, const Slic
696700
}
697701
if (last_deleted) {
698702
iter->SeekToLast();
699-
while (iter->Valid() && deleted_entry_keys.count(iter->key().ToString()) > 0) {
703+
// Skip non-entry subkeys (group/consumer/PEL metadata) when recalculating stream boundaries.
704+
while (iter->Valid() && (identifySubkeyType(iter->key()) != StreamSubkeyType::StreamEntry ||
705+
deleted_entry_keys.count(iter->key().ToString()) > 0)) {
700706
iter->Prev();
701707
}
702708
if (iter->Valid()) {

src/types/redis_stream.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
#include <string>
2828
#include <vector>
2929

30-
#include "common/db_util.h"
3130
#include "storage/redis_db.h"
3231
#include "storage/redis_metadata.h"
3332

tests/gocase/unit/type/stream/stream_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,58 @@ var streamTests = func(t *testing.T, configs util.KvrocksServerConfigs) {
10321032
require.Equal(t, "2-0", msgs[0].ID)
10331033
require.NoError(t, rdb.Del(ctx, streamName).Err())
10341034
})
1035+
1036+
t.Run("XDELEX KEEPREF delete last entry with consumer groups recalculates boundary", func(t *testing.T) {
1037+
streamName := "xdelex_boundary_last_" + strconv.Itoa(rand.Int())
1038+
require.NoError(t, rdb.XAdd(ctx, &redis.XAddArgs{
1039+
Stream: streamName, ID: "1-0", Values: []string{"f", "v"},
1040+
}).Err())
1041+
require.NoError(t, rdb.XAdd(ctx, &redis.XAddArgs{
1042+
Stream: streamName, ID: "2-0", Values: []string{"f", "v"},
1043+
}).Err())
1044+
require.NoError(t, rdb.XAdd(ctx, &redis.XAddArgs{
1045+
Stream: streamName, ID: "3-0", Values: []string{"f", "v"},
1046+
}).Err())
1047+
// Group/PEL keys sort after entry keys; SeekToLast must filter them out.
1048+
require.NoError(t, rdb.XGroupCreateMkStream(ctx, streamName, "g1", "0").Err())
1049+
r, err := rdb.Do(ctx, "XDELEX", streamName, "KEEPREF", "IDS", "1", "3-0").Result()
1050+
require.NoError(t, err)
1051+
require.Equal(t, []interface{}{int64(1)}, r)
1052+
require.Equal(t, int64(2), rdb.XLen(ctx, streamName).Val())
1053+
// XRANGE should end at 2-0 now, not parse garbage from group keys
1054+
msgs, err := rdb.XRange(ctx, streamName, "-", "+").Result()
1055+
require.NoError(t, err)
1056+
require.Equal(t, 2, len(msgs))
1057+
require.Equal(t, "2-0", msgs[1].ID)
1058+
require.NoError(t, rdb.Del(ctx, streamName).Err())
1059+
})
1060+
1061+
t.Run("XDELEX DELREF delete last entry with consumer groups recalculates boundary", func(t *testing.T) {
1062+
streamName := "xdelex_boundary_last_delref_" + strconv.Itoa(rand.Int())
1063+
require.NoError(t, rdb.XAdd(ctx, &redis.XAddArgs{
1064+
Stream: streamName, ID: "1-0", Values: []string{"f", "v"},
1065+
}).Err())
1066+
require.NoError(t, rdb.XAdd(ctx, &redis.XAddArgs{
1067+
Stream: streamName, ID: "2-0", Values: []string{"f", "v"},
1068+
}).Err())
1069+
require.NoError(t, rdb.XAdd(ctx, &redis.XAddArgs{
1070+
Stream: streamName, ID: "3-0", Values: []string{"f", "v"},
1071+
}).Err())
1072+
require.NoError(t, rdb.XGroupCreateMkStream(ctx, streamName, "g1", "0").Err())
1073+
_, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{
1074+
Group: "g1", Consumer: "c1", Streams: []string{streamName, ">"}, Count: 1,
1075+
}).Result()
1076+
require.NoError(t, err)
1077+
r, err := rdb.Do(ctx, "XDELEX", streamName, "DELREF", "IDS", "1", "3-0").Result()
1078+
require.NoError(t, err)
1079+
require.Equal(t, []interface{}{int64(1)}, r)
1080+
require.Equal(t, int64(2), rdb.XLen(ctx, streamName).Val())
1081+
msgs, err := rdb.XRange(ctx, streamName, "-", "+").Result()
1082+
require.NoError(t, err)
1083+
require.Equal(t, 2, len(msgs))
1084+
require.Equal(t, "2-0", msgs[1].ID)
1085+
require.NoError(t, rdb.Del(ctx, streamName).Err())
1086+
})
10351087
}
10361088

10371089
// streamSimulateXRANGE simulates Redis XRANGE implementation in Golang.

0 commit comments

Comments
 (0)