diff --git a/Directory.Build.props b/Directory.Build.props index 156a95ea..c7f3e83a 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ Ugo Lattanzi - 12.1.0 + 12.2.0 @@ -46,6 +46,10 @@ Features: Serializer packages (pick one): System.Text.Json, Newtonsoft, MemoryPack, MsgPack, Protobuf, ServiceStack, Utf8Json. +v12.2.0: +- Fixed SearchKeysAsync ignoring database number — SCAN now correctly targets the specified database instead of always scanning DB0 (#651) +- Fixed RemoveByTagAsync not deleting the tag Set key itself, causing a slow memory leak of empty tag Sets in Redis (#650) + v12.1.0: - Added VectorSet API for AI/ML similarity search (Redis 8.0+): VADD, VSIM, VREM, VCONTAINS, VCARD, VDIM, VGETATTR, VSETATTR, VINFO, VRANDMEMBER, VLINKS - Added llms.txt for AI coding assistant documentation indexing diff --git a/src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.Tags.cs b/src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.Tags.cs index c51d6fa2..f6ef3f64 100644 --- a/src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.Tags.cs +++ b/src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.Tags.cs @@ -38,7 +38,11 @@ public async Task RemoveByTagAsync(string tag, CommandFlags flag = Command var keys = await SetMembersAsync(tagKey, flag).ConfigureAwait(false); - return await RemoveAllAsync(keys, flag).ConfigureAwait(false); + var deletedCount = await RemoveAllAsync(keys, flag).ConfigureAwait(false); + + await Database.KeyDeleteAsync(tagKey, flag).ConfigureAwait(false); + + return deletedCount; } private Task ExecuteAddWithTagsAsync( diff --git a/src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.cs b/src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.cs index 4117c8c2..d1777d4e 100644 --- a/src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.cs +++ b/src/core/StackExchange.Redis.Extensions.Core/Implementations/RedisDatabase.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -482,25 +481,15 @@ public async Task> SearchKeysAsync(string pattern) pattern = $"{keyPrefix}{pattern}"; var keys = new HashSet(); - foreach (var unused in ServerIteratorFactory.GetServers(connectionPoolManager.GetConnection(), serverEnumerationStrategy)) + foreach (var server in ServerIteratorFactory.GetServers(connectionPoolManager.GetConnection(), serverEnumerationStrategy)) { - ulong nextCursor = 0; - do - { - var redisResult = await unused.ExecuteAsync("SCAN", nextCursor.ToString(CultureInfo.InvariantCulture), "MATCH", pattern, "COUNT", "1000").ConfigureAwait(false); - var innerResult = (RedisResult[])redisResult!; - - nextCursor = ulong.Parse((string)innerResult[0]!, CultureInfo.InvariantCulture); - - var resultLines = ((string[])innerResult[1]!).ToArray(); - keys.UnionWith(resultLines); - } - while (nextCursor != 0); + await foreach (var key in server.KeysAsync(dbNumber, pattern, 1000).ConfigureAwait(false)) + keys.Add(key!); } return !string.IsNullOrEmpty(keyPrefix) - ? keys.Select(k => k[keyPrefix.Length..]) - : keys; + ? keys.Select(k => k.ToString()[keyPrefix.Length..]) + : keys.Select(k => k.ToString()); } /// diff --git a/tests/StackExchange.Redis.Extensions.Core.Tests/CacheClientTestBase.Tags.cs b/tests/StackExchange.Redis.Extensions.Core.Tests/CacheClientTestBase.Tags.cs index d742755f..d8534325 100644 --- a/tests/StackExchange.Redis.Extensions.Core.Tests/CacheClientTestBase.Tags.cs +++ b/tests/StackExchange.Redis.Extensions.Core.Tests/CacheClientTestBase.Tags.cs @@ -212,4 +212,25 @@ public async Task RemoveByTagAsync_ShouldReturnOneDeletedValue_Async() Assert.Equal(1, result); } + + [Fact] + [Trait("Category", "Tags")] + public async Task RemoveByTagAsync_ShouldDeleteTagKey_Async() + { + const string testKey = "test_key"; + const string testValue = "test_value"; + var testClass = new Helpers.TestClass(testKey, testValue); + const string testTag = "test_tag"; + + await Sut.GetDefaultDatabase().AddAsync(testKey, testClass, tags: [testTag]); + + var tagKeyName = TagHelper.GenerateTagKey(testTag); + var tagExistsBefore = await db.KeyExistsAsync(tagKeyName); + Assert.True(tagExistsBefore); + + await Sut.GetDefaultDatabase().RemoveByTagAsync(testTag); + + var tagExistsAfter = await db.KeyExistsAsync(tagKeyName); + Assert.False(tagExistsAfter); + } } diff --git a/tests/StackExchange.Redis.Extensions.Core.Tests/CacheClientTestBase.cs b/tests/StackExchange.Redis.Extensions.Core.Tests/CacheClientTestBase.cs index 738b62f5..4964fc1c 100644 --- a/tests/StackExchange.Redis.Extensions.Core.Tests/CacheClientTestBase.cs +++ b/tests/StackExchange.Redis.Extensions.Core.Tests/CacheClientTestBase.cs @@ -338,6 +338,31 @@ public async Task SearchKeys_With_Key_Prefix_Should_Return_Keys_Without_Prefix_A Assert.Equal(keys[i], values[i].Key); } + [Fact] + public async Task SearchKeysAsync_Should_Respect_Database_Number_Async() + { + var db1 = Sut.Db1; + var db1Raw = db1.Database; + + try + { + await db1Raw.StringSetAsync("db1_key1", "value1"); + await db1Raw.StringSetAsync("db1_key2", "value2"); + + await db.StringSetAsync("db0_key1", serializer.Serialize("value_db0")); + + var keysFromDb1 = (await db1.SearchKeysAsync("db1_*")).ToList(); + + Assert.Equal(2, keysFromDb1.Count); + Assert.Contains("db1_key1", keysFromDb1); + Assert.Contains("db1_key2", keysFromDb1); + } + finally + { + await db1Raw.ExecuteAsync("FLUSHDB"); + } + } + [Fact] public async Task Exist_With_Valid_Object_Should_Return_The_Correct_Instance_Async() {