Skip to content

Commit 817ebed

Browse files
committed
Adds hybrid cache invalidation tests
Adds tests to verify that hybrid cache invalidation works correctly across multiple cache instances for increment, removeIfEqual, replaceIfEqual and set operations. This ensures data consistency in distributed caching scenarios.
1 parent 3098a35 commit 817ebed

File tree

4 files changed

+230
-0
lines changed

4 files changed

+230
-0
lines changed

src/Foundatio.TestHarness/Caching/HybridCacheClientTestBase.cs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,42 @@ public virtual async Task GetExpirationAsync_WithLocalCache_ChecksLocalCacheFirs
178178
Assert.Equal(2, cache.LocalCacheHits); // No increment since it was a miss
179179
}
180180

181+
public virtual async Task IncrementAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
182+
{
183+
using var firstCache = GetDistributedHybridCacheClient();
184+
Assert.NotNull(firstCache);
185+
186+
using var secondCache = GetDistributedHybridCacheClient();
187+
Assert.NotNull(secondCache);
188+
189+
const string cacheKey = "increment-invalidate-test";
190+
191+
// Arrange: Client A sets key to 10
192+
await firstCache.SetAsync(cacheKey, 10L);
193+
Assert.Equal(1, firstCache.LocalCache.Count);
194+
195+
// Client B reads key (populates B's local cache with 10)
196+
var result = await secondCache.GetAsync<long>(cacheKey);
197+
Assert.True(result.HasValue);
198+
Assert.Equal(10L, result.Value);
199+
Assert.Equal(1, secondCache.LocalCache.Count);
200+
201+
// Act: Client A increments by 5
202+
var newValue = await firstCache.IncrementAsync(cacheKey, 5L);
203+
Assert.Equal(15L, newValue);
204+
205+
// Wait for invalidation message to propagate
206+
await Task.Delay(250);
207+
208+
// Assert: Client B's local cache should be invalidated
209+
Assert.Equal(0, secondCache.LocalCache.Count);
210+
211+
// Client B reads key - should get 15 (not stale 10)
212+
result = await secondCache.GetAsync<long>(cacheKey);
213+
Assert.True(result.HasValue);
214+
Assert.Equal(15L, result.Value);
215+
}
216+
181217
public virtual async Task ListAddAsync_WithMultipleInstances_WorksCorrectly()
182218
{
183219
using var firstCache = GetDistributedHybridCacheClient();
@@ -247,6 +283,110 @@ public virtual async Task RemoveByPrefixAsync_WithLocalCache_InvalidatesLocalCac
247283
Assert.Equal(0, secondCache.LocalCache.Count);
248284
}
249285

286+
public virtual async Task RemoveIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
287+
{
288+
using var firstCache = GetDistributedHybridCacheClient();
289+
Assert.NotNull(firstCache);
290+
291+
using var secondCache = GetDistributedHybridCacheClient();
292+
Assert.NotNull(secondCache);
293+
294+
const string cacheKey = "remove-if-equal-invalidate-test";
295+
296+
// Arrange: Client A sets key to "value"
297+
await firstCache.SetAsync(cacheKey, "value");
298+
Assert.Equal(1, firstCache.LocalCache.Count);
299+
300+
// Client B reads key (populates B's local cache)
301+
var result = await secondCache.GetAsync<string>(cacheKey);
302+
Assert.True(result.HasValue);
303+
Assert.Equal("value", result.Value);
304+
Assert.Equal(1, secondCache.LocalCache.Count);
305+
306+
// Act: Client A removes key (expected="value")
307+
Assert.True(await firstCache.RemoveIfEqualAsync(cacheKey, "value"));
308+
309+
// Wait for invalidation message to propagate
310+
await Task.Delay(250);
311+
312+
// Assert: Client B's local cache should be invalidated
313+
Assert.Equal(0, secondCache.LocalCache.Count);
314+
315+
// Client B reads key - should get NoValue
316+
result = await secondCache.GetAsync<string>(cacheKey);
317+
Assert.False(result.HasValue);
318+
}
319+
320+
public virtual async Task ReplaceIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
321+
{
322+
using var firstCache = GetDistributedHybridCacheClient();
323+
Assert.NotNull(firstCache);
324+
325+
using var secondCache = GetDistributedHybridCacheClient();
326+
Assert.NotNull(secondCache);
327+
328+
const string cacheKey = "replace-if-equal-invalidate-test";
329+
330+
// Arrange: Client A sets key to "original"
331+
await firstCache.SetAsync(cacheKey, "original");
332+
Assert.Equal(1, firstCache.LocalCache.Count);
333+
334+
// Client B reads key (populates B's local cache)
335+
var result = await secondCache.GetAsync<string>(cacheKey);
336+
Assert.True(result.HasValue);
337+
Assert.Equal("original", result.Value);
338+
Assert.Equal(1, secondCache.LocalCache.Count);
339+
340+
// Act: Client A replaces key with "new" (expected="original")
341+
Assert.True(await firstCache.ReplaceIfEqualAsync(cacheKey, "new", "original"));
342+
343+
// Wait for invalidation message to propagate
344+
await Task.Delay(250);
345+
346+
// Assert: Client B's local cache should be invalidated
347+
Assert.Equal(0, secondCache.LocalCache.Count);
348+
349+
// Client B reads key - should get "new"
350+
result = await secondCache.GetAsync<string>(cacheKey);
351+
Assert.True(result.HasValue);
352+
Assert.Equal("new", result.Value);
353+
}
354+
355+
public virtual async Task SetAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
356+
{
357+
using var firstCache = GetDistributedHybridCacheClient();
358+
Assert.NotNull(firstCache);
359+
360+
using var secondCache = GetDistributedHybridCacheClient();
361+
Assert.NotNull(secondCache);
362+
363+
const string cacheKey = "set-invalidate-test";
364+
365+
// Arrange: Client A sets key to "value1"
366+
await firstCache.SetAsync(cacheKey, "value1");
367+
Assert.Equal(1, firstCache.LocalCache.Count);
368+
369+
// Client B reads key (populates B's local cache with "value1")
370+
var result = await secondCache.GetAsync<string>(cacheKey);
371+
Assert.True(result.HasValue);
372+
Assert.Equal("value1", result.Value);
373+
Assert.Equal(1, secondCache.LocalCache.Count);
374+
375+
// Act: Client A sets key to "value2" (should invalidate B's local cache)
376+
await firstCache.SetAsync(cacheKey, "value2");
377+
378+
// Wait for invalidation message to propagate
379+
await Task.Delay(250);
380+
381+
// Assert: Client B's local cache should be invalidated
382+
Assert.Equal(0, secondCache.LocalCache.Count);
383+
384+
// Client B reads key - should get "value2" (not stale "value1")
385+
result = await secondCache.GetAsync<string>(cacheKey);
386+
Assert.True(result.HasValue);
387+
Assert.Equal("value2", result.Value);
388+
}
389+
250390
public virtual async Task SetAsync_WithMultipleInstances_UsesLocalCache()
251391
{
252392
using var firstCache = GetDistributedHybridCacheClient();

tests/Foundatio.Tests/Caching/InMemoryHybridAwareCacheClientTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ public override Task ExistsAsync_WithVariousKeys_ReturnsCorrectExistenceStatus()
7070
[Fact]
7171
public override Task ExistsAsync_WithInvalidKey_ThrowsArgumentException() => base.ExistsAsync_WithInvalidKey_ThrowsArgumentException();
7272

73+
[Fact]
74+
public override Task ExistsAsync_WithLocalCache_ChecksLocalCacheFirst()
75+
{
76+
return base.ExistsAsync_WithLocalCache_ChecksLocalCacheFirst();
77+
}
78+
7379
[Fact]
7480
public override Task GetAllAsync_WithInvalidKeys_ValidatesCorrectly()
7581
{
@@ -216,6 +222,12 @@ public override Task IncrementAsync_WithScopedCache_WorksWithinScope()
216222
return base.IncrementAsync_WithScopedCache_WorksWithinScope();
217223
}
218224

225+
[Fact]
226+
public override Task IncrementAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
227+
{
228+
return base.IncrementAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
229+
}
230+
219231
[Fact]
220232
public override Task ListAddAsync_WithExpiration_SetsExpirationCorrectly()
221233
{
@@ -438,6 +450,12 @@ public override Task RemoveIfEqualAsync_WithMismatchedValue_ReturnsFalseAndDoesN
438450
return base.RemoveIfEqualAsync_WithMismatchedValue_ReturnsFalseAndDoesNotRemove();
439451
}
440452

453+
[Fact]
454+
public override Task RemoveIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
455+
{
456+
return base.RemoveIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
457+
}
458+
441459
[Fact]
442460
public override Task ReplaceAsync_WithExistingKey_ReturnsTrueAndReplacesValue()
443461
{
@@ -486,6 +504,12 @@ public override Task ReplaceIfEqualAsync_WithMismatchedOldValue_ReturnsFalseAndD
486504
return base.ReplaceIfEqualAsync_WithMismatchedOldValue_ReturnsFalseAndDoesNotReplace();
487505
}
488506

507+
[Fact]
508+
public override Task ReplaceIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
509+
{
510+
return base.ReplaceIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
511+
}
512+
489513
[Fact]
490514
public override Task Serialization_WithComplexObjectsAndValidation_MeasuresThroughput()
491515
{
@@ -566,6 +590,12 @@ public override Task SetAsync_WithMultipleInstances_UsesLocalCache()
566590
return base.SetAsync_WithMultipleInstances_UsesLocalCache();
567591
}
568592

593+
[Fact]
594+
public override Task SetAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
595+
{
596+
return base.SetAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
597+
}
598+
569599
[Fact]
570600
public override Task SetAsync_WithNullValue_StoresAsNullValue()
571601
{

tests/Foundatio.Tests/Caching/InMemoryHybridCacheClientTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ public override Task ExistsAsync_WithVariousKeys_ReturnsCorrectExistenceStatus()
6666
[Fact]
6767
public override Task ExistsAsync_WithInvalidKey_ThrowsArgumentException() => base.ExistsAsync_WithInvalidKey_ThrowsArgumentException();
6868

69+
[Fact]
70+
public override Task ExistsAsync_WithLocalCache_ChecksLocalCacheFirst()
71+
{
72+
return base.ExistsAsync_WithLocalCache_ChecksLocalCacheFirst();
73+
}
74+
6975
[Fact]
7076
public override Task GetAllAsync_WithInvalidKeys_ValidatesCorrectly()
7177
{
@@ -212,6 +218,12 @@ public override Task IncrementAsync_WithScopedCache_WorksWithinScope()
212218
return base.IncrementAsync_WithScopedCache_WorksWithinScope();
213219
}
214220

221+
[Fact]
222+
public override Task IncrementAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
223+
{
224+
return base.IncrementAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
225+
}
226+
215227
[Fact]
216228
public override Task ListAddAsync_WithExpiration_SetsExpirationCorrectly()
217229
{
@@ -434,6 +446,12 @@ public override Task RemoveIfEqualAsync_WithMismatchedValue_ReturnsFalseAndDoesN
434446
return base.RemoveIfEqualAsync_WithMismatchedValue_ReturnsFalseAndDoesNotRemove();
435447
}
436448

449+
[Fact]
450+
public override Task RemoveIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
451+
{
452+
return base.RemoveIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
453+
}
454+
437455
[Fact]
438456
public override Task ReplaceAsync_WithExistingKey_ReturnsTrueAndReplacesValue()
439457
{
@@ -482,6 +500,12 @@ public override Task ReplaceIfEqualAsync_WithMismatchedOldValue_ReturnsFalseAndD
482500
return base.ReplaceIfEqualAsync_WithMismatchedOldValue_ReturnsFalseAndDoesNotReplace();
483501
}
484502

503+
[Fact]
504+
public override Task ReplaceIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
505+
{
506+
return base.ReplaceIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
507+
}
508+
485509
[Fact]
486510
public override Task Serialization_WithComplexObjectsAndValidation_MeasuresThroughput()
487511
{
@@ -562,6 +586,12 @@ public override Task SetAsync_WithMultipleInstances_UsesLocalCache()
562586
return base.SetAsync_WithMultipleInstances_UsesLocalCache();
563587
}
564588

589+
[Fact]
590+
public override Task SetAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
591+
{
592+
return base.SetAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
593+
}
594+
565595
[Fact]
566596
public override Task SetAsync_WithNullValue_StoresAsNullValue()
567597
{

tests/Foundatio.Tests/Caching/ScopedInMemoryHybridCacheClientTests.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ public override Task ExistsAsync_WithVariousKeys_ReturnsCorrectExistenceStatus()
5757
[Fact]
5858
public override Task ExistsAsync_WithInvalidKey_ThrowsArgumentException() => base.ExistsAsync_WithInvalidKey_ThrowsArgumentException();
5959

60+
[Fact]
61+
public override Task ExistsAsync_WithLocalCache_ChecksLocalCacheFirst()
62+
{
63+
return base.ExistsAsync_WithLocalCache_ChecksLocalCacheFirst();
64+
}
65+
6066
[Fact]
6167
public override Task GetAllAsync_WithInvalidKeys_ValidatesCorrectly()
6268
{
@@ -203,6 +209,12 @@ public override Task IncrementAsync_WithScopedCache_WorksWithinScope()
203209
return base.IncrementAsync_WithScopedCache_WorksWithinScope();
204210
}
205211

212+
[Fact]
213+
public override Task IncrementAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
214+
{
215+
return base.IncrementAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
216+
}
217+
206218
[Fact]
207219
public override Task ListAddAsync_WithExpiration_SetsExpirationCorrectly()
208220
{
@@ -425,6 +437,12 @@ public override Task RemoveIfEqualAsync_WithMismatchedValue_ReturnsFalseAndDoesN
425437
return base.RemoveIfEqualAsync_WithMismatchedValue_ReturnsFalseAndDoesNotRemove();
426438
}
427439

440+
[Fact]
441+
public override Task RemoveIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
442+
{
443+
return base.RemoveIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
444+
}
445+
428446
[Fact]
429447
public override Task ReplaceAsync_WithExistingKey_ReturnsTrueAndReplacesValue()
430448
{
@@ -473,6 +491,12 @@ public override Task ReplaceIfEqualAsync_WithMismatchedOldValue_ReturnsFalseAndD
473491
return base.ReplaceIfEqualAsync_WithMismatchedOldValue_ReturnsFalseAndDoesNotReplace();
474492
}
475493

494+
[Fact]
495+
public override Task ReplaceIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
496+
{
497+
return base.ReplaceIfEqualAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
498+
}
499+
476500
[Fact]
477501
public override Task Serialization_WithComplexObjectsAndValidation_MeasuresThroughput()
478502
{
@@ -553,6 +577,12 @@ public override Task SetAsync_WithMultipleInstances_UsesLocalCache()
553577
return base.SetAsync_WithMultipleInstances_UsesLocalCache();
554578
}
555579

580+
[Fact]
581+
public override Task SetAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache()
582+
{
583+
return base.SetAsync_WithMultipleInstances_InvalidatesOtherClientLocalCache();
584+
}
585+
556586
[Fact]
557587
public override Task SetAsync_WithNullValue_StoresAsNullValue()
558588
{

0 commit comments

Comments
 (0)