From b2ff699170db9df3ff335bdfa425edef4b6dc57d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 5 Sep 2026 16:47:51 +0000 Subject: [PATCH 1/5] Pass cancellation token to semaphore.Wait in MemoryCacheBase Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs b/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs index cf4e54c61..d515d36c5 100644 --- a/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs +++ b/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs @@ -46,7 +46,7 @@ public virtual T Get(string key, Func acquire, int cacheTime) { if (_cache.TryGetValue(key, out T cacheEntry)) return cacheEntry; var semaphore = CacheEntries.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); - semaphore.Wait(); + semaphore.Wait(_resetCacheToken.Token); try { if (!_cache.TryGetValue(key, out cacheEntry)) From 57f1beca537e9d895c4c7aa8b5d215266ffa5295 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:04:57 +0000 Subject: [PATCH 2/5] Handle canceled cache wait in synchronous Get Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- .../Caching/MemoryCacheBase.cs | 13 +++++++++- .../Caching/MemoryCacheBaseTests.cs | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs b/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs index d515d36c5..84839b8fe 100644 --- a/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs +++ b/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs @@ -46,7 +46,18 @@ public virtual T Get(string key, Func acquire, int cacheTime) { if (_cache.TryGetValue(key, out T cacheEntry)) return cacheEntry; var semaphore = CacheEntries.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); - semaphore.Wait(_resetCacheToken.Token); + while (true) + { + var resetToken = _resetCacheToken.Token; + try + { + semaphore.Wait(resetToken); + break; + } + catch (OperationCanceledException) when (resetToken.IsCancellationRequested) + { + } + } try { if (!_cache.TryGetValue(key, out cacheEntry)) diff --git a/src/Tests/Grand.Infrastructure.Tests/Caching/MemoryCacheBaseTests.cs b/src/Tests/Grand.Infrastructure.Tests/Caching/MemoryCacheBaseTests.cs index dd8c7742b..ba0676936 100644 --- a/src/Tests/Grand.Infrastructure.Tests/Caching/MemoryCacheBaseTests.cs +++ b/src/Tests/Grand.Infrastructure.Tests/Caching/MemoryCacheBaseTests.cs @@ -44,6 +44,31 @@ public void GetTest_CacheTimeMinutes() Assert.AreEqual("test", result); } + [TestMethod] + [Timeout(10000)] + public async Task Get_WhenClearCancelsWaitingSemaphore_DoesNotThrow() + { + var acquireStarted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var releaseAcquire = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + var firstRead = _service.GetAsync("key", async () => + { + acquireStarted.SetResult(true); + await releaseAcquire.Task; + return "test"; + }, 1); + + await acquireStarted.Task; + + var secondRead = Task.Run(() => _service.Get("key", () => "fallback", 1)); + + _service.Clear(false).GetAwaiter().GetResult(); + releaseAcquire.SetResult(true); + + Assert.AreEqual("test", await firstRead); + Assert.AreEqual("test", await secondRead); + } + [TestMethod] public async Task GetAsyncTest() { From 2c554ec427ab0dfff8ef33095c78ceba03fd9dfd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 6 Sep 2026 11:05:39 +0000 Subject: [PATCH 3/5] Retry cache semaphore wait once after reset cancellation Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- .../Caching/MemoryCacheBase.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs b/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs index 84839b8fe..916b0fa96 100644 --- a/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs +++ b/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs @@ -46,17 +46,18 @@ public virtual T Get(string key, Func acquire, int cacheTime) { if (_cache.TryGetValue(key, out T cacheEntry)) return cacheEntry; var semaphore = CacheEntries.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); - while (true) + var resetToken = _resetCacheToken.Token; + try { - var resetToken = _resetCacheToken.Token; - try - { - semaphore.Wait(resetToken); - break; - } - catch (OperationCanceledException) when (resetToken.IsCancellationRequested) - { - } + semaphore.Wait(resetToken); + } + catch (OperationCanceledException) when (resetToken.IsCancellationRequested) + { + var refreshedToken = _resetCacheToken.Token; + if (refreshedToken == resetToken) + throw; + + semaphore.Wait(refreshedToken); } try { From 9b01951e9c64c6fdcdf5b4d72cb8c4b7cb79ace6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 6 Sep 2026 13:43:54 +0000 Subject: [PATCH 4/5] Handle repeated cache reset cancellation in MemoryCacheBase.Get Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- .../Caching/MemoryCacheBase.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs b/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs index 916b0fa96..759a2892c 100644 --- a/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs +++ b/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs @@ -46,18 +46,19 @@ public virtual T Get(string key, Func acquire, int cacheTime) { if (_cache.TryGetValue(key, out T cacheEntry)) return cacheEntry; var semaphore = CacheEntries.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); - var resetToken = _resetCacheToken.Token; - try - { - semaphore.Wait(resetToken); - } - catch (OperationCanceledException) when (resetToken.IsCancellationRequested) + while (true) { - var refreshedToken = _resetCacheToken.Token; - if (refreshedToken == resetToken) - throw; - - semaphore.Wait(refreshedToken); + var resetToken = _resetCacheToken.Token; + try + { + semaphore.Wait(resetToken); + break; + } + catch (OperationCanceledException) when (resetToken.IsCancellationRequested) + { + if (_resetCacheToken.Token == resetToken) + throw; + } } try { From 709a3563adcc229174f3abaaef583629b77cc826 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 6 Sep 2026 14:03:39 +0000 Subject: [PATCH 5/5] Use non-cancelable wait in MemoryCacheBase.Get Co-authored-by: KrzysztofPajak <16772986+KrzysztofPajak@users.noreply.github.com> --- .../Caching/MemoryCacheBase.cs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs b/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs index 759a2892c..ba132dcc9 100644 --- a/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs +++ b/src/Core/Grand.Infrastructure/Caching/MemoryCacheBase.cs @@ -46,20 +46,7 @@ public virtual T Get(string key, Func acquire, int cacheTime) { if (_cache.TryGetValue(key, out T cacheEntry)) return cacheEntry; var semaphore = CacheEntries.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); - while (true) - { - var resetToken = _resetCacheToken.Token; - try - { - semaphore.Wait(resetToken); - break; - } - catch (OperationCanceledException) when (resetToken.IsCancellationRequested) - { - if (_resetCacheToken.Token == resetToken) - throw; - } - } + semaphore.Wait(CancellationToken.None); try { if (!_cache.TryGetValue(key, out cacheEntry))