From 95768fe873306e5b2b2c549f2568231c00f417f6 Mon Sep 17 00:00:00 2001 From: Benjamin Grabkowitz Date: Wed, 16 Apr 2025 23:03:56 -0400 Subject: [PATCH 1/9] Adding an entryId for taggedEntries so that replaced items in the cache do not have their tags removed on eviction. Fixes #61524 --- .../src/Memory/MemoryOutputCacheStore.cs | 30 +++++++++------ .../test/MemoryOutputCacheStoreTests.cs | 37 +++++++++++++++++++ 2 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index a75546b6793f..04f85f7c1498 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Linq; using Microsoft.Extensions.Caching.Memory; namespace Microsoft.AspNetCore.OutputCaching.Memory; @@ -9,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Memory; internal sealed class MemoryOutputCacheStore : IOutputCacheStore { private readonly MemoryCache _cache; - private readonly Dictionary> _taggedEntries = new(); + private readonly Dictionary>> _taggedEntries = new(); private readonly object _tagsLock = new(); internal MemoryOutputCacheStore(MemoryCache cache) @@ -20,7 +21,7 @@ internal MemoryOutputCacheStore(MemoryCache cache) } // For testing - internal Dictionary> TaggedEntries => _taggedEntries; + internal Dictionary> TaggedEntries => _taggedEntries.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(t => t.Item1).ToHashSet()); public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken) { @@ -40,9 +41,9 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken while (i > 0) { var oldCount = keys.Count; - foreach (var key in keys) + foreach (var tuple in keys) { - _cache.Remove(key); + _cache.Remove(tuple.Item1); i--; if (oldCount != keys.Count) { @@ -74,6 +75,8 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(value); + var entryId = Guid.NewGuid().ToString(); + if (tags != null) { // Lock with SetEntry() to prevent EvictByTagAsync() from trying to remove a tag whose entry hasn't been added yet. @@ -90,27 +93,27 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val if (!_taggedEntries.TryGetValue(tag, out var keys)) { - keys = new HashSet(); + keys = new HashSet>(); _taggedEntries[tag] = keys; } Debug.Assert(keys != null); - keys.Add(key); + keys.Add(Tuple.Create(key, entryId)); } - SetEntry(key, value, tags, validFor); + SetEntry(key, value, tags, validFor, entryId); } } else { - SetEntry(key, value, tags, validFor); + SetEntry(key, value, tags, validFor, entryId); } return ValueTask.CompletedTask; } - void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor) + private void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor, string entryId) { Debug.Assert(key != null); @@ -123,7 +126,7 @@ void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor) if (tags != null && tags.Length > 0) { // Remove cache keys from tag lists when the entry is evicted - options.RegisterPostEvictionCallback(RemoveFromTags, tags); + options.RegisterPostEvictionCallback(RemoveFromTags, Tuple.Create(tags, entryId)); } _cache.Set(key, value, options); @@ -131,11 +134,14 @@ void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor) void RemoveFromTags(object key, object? value, EvictionReason reason, object? state) { - var tags = state as string[]; + var stateTuple = state as Tuple; + string[]? tags = stateTuple?.Item1; + string? entryId = stateTuple?.Item2; Debug.Assert(tags != null); Debug.Assert(tags.Length > 0); Debug.Assert(key is string); + Debug.Assert(entryId != null); lock (_tagsLock) { @@ -143,7 +149,7 @@ void RemoveFromTags(object key, object? value, EvictionReason reason, object? st { if (_taggedEntries.TryGetValue(tag, out var tagged)) { - tagged.Remove((string)key); + tagged.Remove(Tuple.Create((string) key, entryId)); // Remove the collection if there is no more keys in it if (tagged.Count == 0) diff --git a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs index e8c809911add..0eed649b2d1b 100644 --- a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs +++ b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs @@ -197,6 +197,43 @@ public async Task ExpiredEntries_AreRemovedFromTags() Assert.Single(tag2s); } + [Fact] + public async Task ReplacedEntries_AreNotRemovedFromTags() + { + var testClock = new TestMemoryOptionsClock { UtcNow = DateTimeOffset.UtcNow }; + var cache = new MemoryCache(new MemoryCacheOptions { SizeLimit = 1000, Clock = testClock, ExpirationScanFrequency = TimeSpan.FromMilliseconds(1) }); + var store = new MemoryOutputCacheStore(cache); + var value = "abc"u8.ToArray(); + + await store.SetAsync("a", value, new[] { "tag1", "tag2" }, TimeSpan.FromMilliseconds(5), default); + await store.SetAsync("a", value, new[] { "tag1" }, TimeSpan.FromMilliseconds(20), default); + + testClock.Advance(TimeSpan.FromMilliseconds(10)); + + // Background expiration checks are triggered by misc cache activity. + _ = cache.Get("a"); + + var resulta = await store.GetAsync("a", default); + + Assert.NotNull(resulta); + + HashSet tag1s, tag2s; + + // Wait for the hashset to be removed as it happens on a separate thread + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); + + while (store.TaggedEntries.TryGetValue("tag2", out tag2s) && !cts.IsCancellationRequested) + { + await Task.Yield(); + } + + store.TaggedEntries.TryGetValue("tag1", out tag1s); + + Assert.Null(tag2s); + Assert.Single(tag1s); + } + [Theory] [InlineData(null)] public async Task Store_Throws_OnInvalidTag(string tag) From 0ed59eb64bc227553f2d2b68cdc65e7191e07b50 Mon Sep 17 00:00:00 2001 From: Benjamin Grabkowitz Date: Thu, 17 Apr 2025 06:49:20 -0400 Subject: [PATCH 2/9] Removing string allocation and refactoring to ValueTuple. --- .../src/Memory/MemoryOutputCacheStore.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index 04f85f7c1498..b5febf8d9308 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Memory; internal sealed class MemoryOutputCacheStore : IOutputCacheStore { private readonly MemoryCache _cache; - private readonly Dictionary>> _taggedEntries = new(); + private readonly Dictionary>> _taggedEntries = new(); private readonly object _tagsLock = new(); internal MemoryOutputCacheStore(MemoryCache cache) @@ -75,7 +75,7 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(value); - var entryId = Guid.NewGuid().ToString(); + var entryId = Guid.NewGuid(); if (tags != null) { @@ -93,13 +93,13 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val if (!_taggedEntries.TryGetValue(tag, out var keys)) { - keys = new HashSet>(); + keys = new HashSet>(); _taggedEntries[tag] = keys; } Debug.Assert(keys != null); - keys.Add(Tuple.Create(key, entryId)); + keys.Add(ValueTuple.Create(key, entryId)); } SetEntry(key, value, tags, validFor, entryId); @@ -113,7 +113,7 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val return ValueTask.CompletedTask; } - private void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor, string entryId) + private void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFor, Guid entryId) { Debug.Assert(key != null); @@ -126,7 +126,7 @@ private void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFo if (tags != null && tags.Length > 0) { // Remove cache keys from tag lists when the entry is evicted - options.RegisterPostEvictionCallback(RemoveFromTags, Tuple.Create(tags, entryId)); + options.RegisterPostEvictionCallback(RemoveFromTags, ValueTuple.Create(tags, entryId)); } _cache.Set(key, value, options); @@ -134,14 +134,15 @@ private void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFo void RemoveFromTags(object key, object? value, EvictionReason reason, object? state) { - var stateTuple = state as Tuple; - string[]? tags = stateTuple?.Item1; - string? entryId = stateTuple?.Item2; + Debug.Assert(state != null); + + var stateTuple = (ValueTuple) state; + string[] tags = stateTuple.Item1; + Guid entryId = stateTuple.Item2; Debug.Assert(tags != null); Debug.Assert(tags.Length > 0); Debug.Assert(key is string); - Debug.Assert(entryId != null); lock (_tagsLock) { @@ -149,7 +150,7 @@ void RemoveFromTags(object key, object? value, EvictionReason reason, object? st { if (_taggedEntries.TryGetValue(tag, out var tagged)) { - tagged.Remove(Tuple.Create((string) key, entryId)); + tagged.Remove(ValueTuple.Create((string) key, entryId)); // Remove the collection if there is no more keys in it if (tagged.Count == 0) From a85372daf81fda8f217064a06cdd5118380ad237 Mon Sep 17 00:00:00 2001 From: Benjamin Grabkowitz Date: Thu, 17 Apr 2025 06:51:29 -0400 Subject: [PATCH 3/9] Code cleanup. --- .../OutputCaching/src/Memory/MemoryOutputCacheStore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index b5febf8d9308..95c079a7467f 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Memory; internal sealed class MemoryOutputCacheStore : IOutputCacheStore { private readonly MemoryCache _cache; - private readonly Dictionary>> _taggedEntries = new(); + private readonly Dictionary>> _taggedEntries = []; private readonly object _tagsLock = new(); internal MemoryOutputCacheStore(MemoryCache cache) From 987807d978da5fea73bc9e583568b7bd66076350 Mon Sep 17 00:00:00 2001 From: Benjamin Grabkowitz Date: Thu, 17 Apr 2025 07:29:39 -0400 Subject: [PATCH 4/9] Using named ValueTuples --- .../src/Memory/MemoryOutputCacheStore.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index 95c079a7467f..e2be02734d8e 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Memory; internal sealed class MemoryOutputCacheStore : IOutputCacheStore { private readonly MemoryCache _cache; - private readonly Dictionary>> _taggedEntries = []; + private readonly Dictionary> _taggedEntries = []; private readonly object _tagsLock = new(); internal MemoryOutputCacheStore(MemoryCache cache) @@ -21,7 +21,7 @@ internal MemoryOutputCacheStore(MemoryCache cache) } // For testing - internal Dictionary> TaggedEntries => _taggedEntries.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(t => t.Item1).ToHashSet()); + internal Dictionary> TaggedEntries => _taggedEntries.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(t => t.key).ToHashSet()); public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken) { @@ -43,7 +43,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken var oldCount = keys.Count; foreach (var tuple in keys) { - _cache.Remove(tuple.Item1); + _cache.Remove(tuple.key); i--; if (oldCount != keys.Count) { @@ -93,7 +93,7 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val if (!_taggedEntries.TryGetValue(tag, out var keys)) { - keys = new HashSet>(); + keys = new HashSet<(string, Guid)>(); _taggedEntries[tag] = keys; } @@ -123,7 +123,7 @@ private void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFo Size = value.Length }; - if (tags != null && tags.Length > 0) + if (tags is { Length: > 0 }) { // Remove cache keys from tag lists when the entry is evicted options.RegisterPostEvictionCallback(RemoveFromTags, ValueTuple.Create(tags, entryId)); @@ -136,9 +136,9 @@ void RemoveFromTags(object key, object? value, EvictionReason reason, object? st { Debug.Assert(state != null); - var stateTuple = (ValueTuple) state; - string[] tags = stateTuple.Item1; - Guid entryId = stateTuple.Item2; + var stateTuple = ((string[] tags, Guid entryId))state; + var tags = stateTuple.tags; + var entryId = stateTuple.entryId; Debug.Assert(tags != null); Debug.Assert(tags.Length > 0); @@ -150,7 +150,7 @@ void RemoveFromTags(object key, object? value, EvictionReason reason, object? st { if (_taggedEntries.TryGetValue(tag, out var tagged)) { - tagged.Remove(ValueTuple.Create((string) key, entryId)); + tagged.Remove((key: (string)key, entryId)); // Remove the collection if there is no more keys in it if (tagged.Count == 0) From 22ea9a82b346635dbea25ab0279742a07f3f734b Mon Sep 17 00:00:00 2001 From: Benjamin Grabkowitz Date: Thu, 17 Apr 2025 07:33:32 -0400 Subject: [PATCH 5/9] Adding assertion and deconstruction. --- .../OutputCaching/src/Memory/MemoryOutputCacheStore.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index e2be02734d8e..2f49c52d8bb3 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -136,13 +136,12 @@ void RemoveFromTags(object key, object? value, EvictionReason reason, object? st { Debug.Assert(state != null); - var stateTuple = ((string[] tags, Guid entryId))state; - var tags = stateTuple.tags; - var entryId = stateTuple.entryId; + var (tags, entryId) = ((string[] tags, Guid entryId))state; Debug.Assert(tags != null); Debug.Assert(tags.Length > 0); Debug.Assert(key is string); + Debug.Assert(entryId != Guid.Empty); lock (_tagsLock) { From ee019d95f1c34a924362816dc7909a8263fef366 Mon Sep 17 00:00:00 2001 From: Benjamin Grabkowitz Date: Thu, 17 Apr 2025 07:58:31 -0400 Subject: [PATCH 6/9] Refactoring tuple naming to PascalCase. Other code style changes. --- .../src/Memory/MemoryOutputCacheStore.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index 2f49c52d8bb3..6f5bea17a4fc 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Memory; internal sealed class MemoryOutputCacheStore : IOutputCacheStore { private readonly MemoryCache _cache; - private readonly Dictionary> _taggedEntries = []; + private readonly Dictionary> _taggedEntries = []; private readonly object _tagsLock = new(); internal MemoryOutputCacheStore(MemoryCache cache) @@ -21,7 +21,7 @@ internal MemoryOutputCacheStore(MemoryCache cache) } // For testing - internal Dictionary> TaggedEntries => _taggedEntries.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(t => t.key).ToHashSet()); + internal Dictionary> TaggedEntries => _taggedEntries.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Select(t => t.Key).ToHashSet()); public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken) { @@ -31,7 +31,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken { if (_taggedEntries.TryGetValue(tag, out var keys)) { - if (keys != null && keys.Count > 0) + if (keys is { Count: > 0 }) { // If MemoryCache changed to run eviction callbacks inline in Remove, iterating over keys could throw // To prevent allocating a copy of the keys we check if the eviction callback ran, @@ -41,9 +41,9 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken while (i > 0) { var oldCount = keys.Count; - foreach (var tuple in keys) + foreach (var (key, _) in keys) { - _cache.Remove(tuple.key); + _cache.Remove(key); i--; if (oldCount != keys.Count) { @@ -99,7 +99,7 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val Debug.Assert(keys != null); - keys.Add(ValueTuple.Create(key, entryId)); + keys.Add((key, entryId)); } SetEntry(key, value, tags, validFor, entryId); @@ -126,17 +126,17 @@ private void SetEntry(string key, byte[] value, string[]? tags, TimeSpan validFo if (tags is { Length: > 0 }) { // Remove cache keys from tag lists when the entry is evicted - options.RegisterPostEvictionCallback(RemoveFromTags, ValueTuple.Create(tags, entryId)); + options.RegisterPostEvictionCallback(RemoveFromTags, (tags, entryId)); } _cache.Set(key, value, options); } - void RemoveFromTags(object key, object? value, EvictionReason reason, object? state) + private void RemoveFromTags(object key, object? value, EvictionReason reason, object? state) { Debug.Assert(state != null); - var (tags, entryId) = ((string[] tags, Guid entryId))state; + var (tags, entryId) = ((string[] Tags, Guid EntryId))state; Debug.Assert(tags != null); Debug.Assert(tags.Length > 0); @@ -149,7 +149,7 @@ void RemoveFromTags(object key, object? value, EvictionReason reason, object? st { if (_taggedEntries.TryGetValue(tag, out var tagged)) { - tagged.Remove((key: (string)key, entryId)); + tagged.Remove((Key: (string)key, entryId)); // Remove the collection if there is no more keys in it if (tagged.Count == 0) From b1ccd76ed7df770f3a5afe4f2233dd7ad2819aad Mon Sep 17 00:00:00 2001 From: Benjamin Grabkowitz Date: Thu, 1 May 2025 12:36:30 -0400 Subject: [PATCH 7/9] Refactor tagged entries to use a record type. --- .../OutputCaching/src/Memory/MemoryOutputCacheStore.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index 6f5bea17a4fc..38a38069b32c 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Memory; internal sealed class MemoryOutputCacheStore : IOutputCacheStore { private readonly MemoryCache _cache; - private readonly Dictionary> _taggedEntries = []; + private readonly Dictionary> _taggedEntries = []; private readonly object _tagsLock = new(); internal MemoryOutputCacheStore(MemoryCache cache) @@ -93,13 +93,13 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val if (!_taggedEntries.TryGetValue(tag, out var keys)) { - keys = new HashSet<(string, Guid)>(); + keys = new HashSet(); _taggedEntries[tag] = keys; } Debug.Assert(keys != null); - keys.Add((key, entryId)); + keys.Add(new TaggedEntry(key, entryId)); } SetEntry(key, value, tags, validFor, entryId); @@ -149,7 +149,7 @@ private void RemoveFromTags(object key, object? value, EvictionReason reason, ob { if (_taggedEntries.TryGetValue(tag, out var tagged)) { - tagged.Remove((Key: (string)key, entryId)); + tagged.Remove(new TaggedEntry((string)key, entryId)); // Remove the collection if there is no more keys in it if (tagged.Count == 0) @@ -160,4 +160,6 @@ private void RemoveFromTags(object key, object? value, EvictionReason reason, ob } } } + + private record TaggedEntry(string Key, Guid EntryId); } From 5903653794affb2d21b5f71deb4032e9b543bbea Mon Sep 17 00:00:00 2001 From: Ben Grabkowitz Date: Thu, 1 May 2025 13:27:55 -0400 Subject: [PATCH 8/9] Update src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs Co-authored-by: Rick Anderson <3605364+Rick-Anderson@users.noreply.github.com> --- .../OutputCaching/test/MemoryOutputCacheStoreTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs index 0eed649b2d1b..b637e6318455 100644 --- a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs +++ b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs @@ -210,7 +210,7 @@ public async Task ReplacedEntries_AreNotRemovedFromTags() testClock.Advance(TimeSpan.FromMilliseconds(10)); - // Background expiration checks are triggered by misc cache activity. + // Trigger background expiration by accessing the cache. _ = cache.Get("a"); var resulta = await store.GetAsync("a", default); From f48759bd78aa3bdea97d68a9918a7ba54b6699a2 Mon Sep 17 00:00:00 2001 From: Ben Grabkowitz Date: Thu, 1 May 2025 13:28:07 -0400 Subject: [PATCH 9/9] Update src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs Co-authored-by: Rick Anderson <3605364+Rick-Anderson@users.noreply.github.com> --- .../OutputCaching/test/MemoryOutputCacheStoreTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs index b637e6318455..c1ad1d708f4b 100644 --- a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs +++ b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs @@ -219,7 +219,7 @@ public async Task ReplacedEntries_AreNotRemovedFromTags() HashSet tag1s, tag2s; - // Wait for the hashset to be removed as it happens on a separate thread + // Wait for the tag2 HashSet to be removed by the background expiration thread. using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));