Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 99 additions & 2 deletions BlitzCache.Tests/Examples/BasicUsageExamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,11 +285,108 @@ string SimulateExpensiveDatabaseCall(int userId)
}

/// <summary>
/// Example 11: Accessing Top Slowest Queries statistics
/// Example 11: Thundering Herd / Cache Stampede Prevention
/// DEMONSTRATES THE CORE PROBLEM BLITZCACHE SOLVES:
/// Without BlitzCache: 100 concurrent requests = 100 database executions
/// With BlitzCache: 100 concurrent requests = 1 database execution, 99 wait for result
/// This is the "cache stampede" or "thundering herd" problem that BlitzCache automatically prevents.
/// </summary>
[Test]
public async Task Example11_ThunderingHerdPrevention()
{
var databaseCallCount = 0;
var executionTimes = new System.Collections.Generic.List<DateTime>();

async Task<string> ExpensiveDatabaseQuery()
{
// Record when this executes
lock (executionTimes)
{
executionTimes.Add(DateTime.Now);
}

System.Threading.Interlocked.Increment(ref databaseCallCount);
await Task.Delay(100); // Simulate slow database query (100ms)
return $"Expensive database result (execution #{databaseCallCount})";
}

Console.WriteLine("=== Thundering Herd / Cache Stampede Prevention Demo ===");
Console.WriteLine("Simulating 100 concurrent requests for the same data...");
Console.WriteLine();

// THE PROBLEM SCENARIO:
// Imagine 100 users hit your API at the exact same moment requesting the same data
// Without proper protection, all 100 would execute your expensive database query
// This is called "cache stampede", "thundering herd", or "dog-pile effect"

var concurrentRequestCount = 100; // 100 concurrent requests
var tasks = new Task<string>[concurrentRequestCount];

var startTime = DateTime.Now;

// Launch 100 concurrent requests for the same cache key
for (int i = 0; i < concurrentRequestCount; i++)
{
var requestNumber = i + 1;
tasks[i] = Task.Run(async () =>
{
// Each request tries to get the same data
// WITHOUT BlitzCache: All 100 would hit the database
// WITH BlitzCache: Only 1 hits the database, 99 wait and share the result
var result = await cache.BlitzGet("shared_expensive_data", ExpensiveDatabaseQuery, TestConstants.StandardTimeoutMs);
return result;
});
}

// Wait for all concurrent requests to complete
var results = await Task.WhenAll(tasks);
var endTime = DateTime.Now;
var totalTime = (endTime - startTime).TotalMilliseconds;

Console.WriteLine($"βœ… All {concurrentRequestCount} concurrent requests completed in {totalTime:F0}ms");
Console.WriteLine();
Console.WriteLine("=== BlitzCache Thundering Herd Protection Results ===");
Console.WriteLine($"Total concurrent requests: {concurrentRequestCount}");
Console.WriteLine($"Actual database executions: {databaseCallCount}");
Console.WriteLine($"Database queries prevented: {concurrentRequestCount - databaseCallCount}");
Console.WriteLine($"Efficiency gain: {((concurrentRequestCount - databaseCallCount) / (double)concurrentRequestCount * 100):F1}% reduction in database load");
Console.WriteLine();

// VERIFICATION: BlitzCache ensures only ONE execution despite concurrent requests
Assert.AreEqual(1, databaseCallCount,
$"THUNDERING HERD PREVENTION: Only 1 database call should execute despite {concurrentRequestCount} concurrent requests!");

Assert.AreEqual(1, executionTimes.Count,
"Only one execution timestamp should exist");

// All requests should return the exact same result
Assert.IsTrue(results.All(r => r == results[0]),
"All concurrent requests should receive identical results");

Assert.AreEqual("Expensive database result (execution #1)", results[0],
"Result should be from the single execution");

Console.WriteLine("✨ BlitzCache successfully prevented cache stampede!");
Console.WriteLine($" Instead of {concurrentRequestCount} expensive database queries,");
Console.WriteLine(" only 1 executed while the other 99 waited and shared the result.");
Console.WriteLine();
Console.WriteLine("πŸ’‘ This is what BlitzCache does automatically:");
Console.WriteLine(" - Prevents duplicate execution under concurrent load");
Console.WriteLine(" - Protects your database/API from being overwhelmed");
Console.WriteLine(" - Eliminates race conditions in cache population");
Console.WriteLine(" - Zero configuration, completely thread-safe");
Console.WriteLine();
Console.WriteLine("🎯 Without BlitzCache, you'd need 15+ lines of SemaphoreSlim");
Console.WriteLine(" boilerplate PER CACHED OPERATION to achieve this protection.");
Console.WriteLine(" With BlitzCache: Just use BlitzGet() and it works automatically!");
}

/// <summary>
/// Example 12: Accessing Top Slowest Queries statistics
/// Demonstrates how to retrieve and inspect the slowest cache operations using BlitzCache statistics (v2.0.2+)
/// </summary>
[Test]
public async Task Example11_TopSlowestQueries()
public async Task Example12_TopSlowestQueries()
{
cache.InitializeStatistics();
// Simulate several cache operations with varying delays
Expand Down
230 changes: 230 additions & 0 deletions BlitzCache.Tests/NullBlitzCacheForTestingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
using BlitzCacheCore.Tests.Helpers;
using NUnit.Framework;
using System.Threading.Tasks;

namespace BlitzCacheCore.Tests
{
/// <summary>
/// Tests for NullBlitzCacheForTesting utility class.
/// Validates that NullBlitzCacheForTesting behaves correctly for testing scenarios
/// where cache behavior should be bypassed.
/// </summary>
[TestFixture]
public class NullBlitzCacheForTestingTests
{
[Test]
public void NullCache_AlwaysExecutesFunction_NeverCaches()
{
// Arrange
var cache = new NullBlitzCacheForTesting();
var callCount = 0;
string TestFunction()
{
callCount++;
return $"result_{callCount}";
}

// Act
var result1 = cache.BlitzGet("key", TestFunction, 1000);
var result2 = cache.BlitzGet("key", TestFunction, 1000);
var result3 = cache.BlitzGet("key", TestFunction, 1000);

// Assert
Assert.AreEqual(3, callCount, "NullCache should execute function every time");
Assert.AreEqual("result_1", result1);
Assert.AreEqual("result_2", result2);
Assert.AreEqual("result_3", result3);
}

[Test]
public async Task NullCache_AlwaysExecutesAsyncFunction_NeverCaches()
{
// Arrange
var cache = new NullBlitzCacheForTesting();
var callCount = 0;
async Task<string> TestFunctionAsync()
{
callCount++;
await Task.Delay(1);
return $"async_result_{callCount}";
}

// Act
var result1 = await cache.BlitzGet("async_key", TestFunctionAsync, 1000);
var result2 = await cache.BlitzGet("async_key", TestFunctionAsync, 1000);
var result3 = await cache.BlitzGet("async_key", TestFunctionAsync, 1000);

// Assert
Assert.AreEqual(3, callCount, "NullCache should execute async function every time");
Assert.AreEqual("async_result_1", result1);
Assert.AreEqual("async_result_2", result2);
Assert.AreEqual("async_result_3", result3);
}

[Test]
public void NullCache_WithDifferentKeys_ExecutesFunctionForEach()
{
// Arrange
var cache = new NullBlitzCacheForTesting();
var callCount = 0;
string TestFunction()
{
callCount++;
return $"result_{callCount}";
}

// Act
var result1 = cache.BlitzGet("key1", TestFunction, 1000);
var result2 = cache.BlitzGet("key2", TestFunction, 1000);
var result3 = cache.BlitzGet("key3", TestFunction, 1000);

// Assert
Assert.AreEqual(3, callCount, "NullCache should execute function for each key");
Assert.AreEqual("result_1", result1);
Assert.AreEqual("result_2", result2);
Assert.AreEqual("result_3", result3);
}

[Test]
public void NullCache_Statistics_ReturnsNullByDefault()
{
// Arrange
var cache = new NullBlitzCacheForTesting();

// Act & Assert
Assert.IsNull(cache.Statistics, "NullCache should return null statistics by default");
}

[Test]
public void NullCache_AfterInitializeStatistics_ReturnsNullStatistics()
{
// Arrange
IBlitzCache cache = new NullBlitzCacheForTesting();

// Act
cache.InitializeStatistics();

// Assert
Assert.IsNotNull(cache.Statistics, "After InitializeStatistics, should return NullCacheStatistics instance");
Assert.AreEqual(0, cache.Statistics.HitCount);
Assert.AreEqual(0, cache.Statistics.MissCount);
Assert.AreEqual(0, cache.Statistics.TotalOperations);
Assert.AreEqual(0.0, cache.Statistics.HitRatio);
}

[Test]
public void NullCache_Statistics_AlwaysReturnsZero_EvenAfterOperations()
{
// Arrange
IBlitzCache cache = new NullBlitzCacheForTesting();
cache.InitializeStatistics();

// Act
cache.BlitzGet("key1", () => "value1", 1000);
cache.BlitzGet("key1", () => "value2", 1000);
cache.BlitzGet("key2", () => "value3", 1000);

// Assert - Statistics remain zero because NullCache doesn't track
Assert.AreEqual(0, cache.Statistics.HitCount, "NullCacheStatistics always returns 0");
Assert.AreEqual(0, cache.Statistics.MissCount, "NullCacheStatistics always returns 0");
Assert.AreEqual(0, cache.Statistics.TotalOperations, "NullCacheStatistics always returns 0");
Assert.AreEqual(0, cache.Statistics.EntryCount, "NullCacheStatistics always returns 0");
}

[Test]
public void NullCache_BlitzUpdate_DoesNothing()
{
// Arrange
var cache = new NullBlitzCacheForTesting();
var callCount = 0;
string UpdateFunction()
{
callCount++;
return "updated";
}

// Act
cache.BlitzUpdate("key", UpdateFunction, 1000);

// Assert
Assert.AreEqual(0, callCount, "BlitzUpdate on NullCache should not execute the function");
}

[Test]
public async Task NullCache_BlitzUpdateAsync_DoesNothing()
{
// Arrange
var cache = new NullBlitzCacheForTesting();
var callCount = 0;
async Task<string> UpdateFunctionAsync()
{
callCount++;
await Task.Delay(1);
return "updated";
}

// Act
await cache.BlitzUpdate("key", UpdateFunctionAsync, 1000);

// Assert
Assert.AreEqual(0, callCount, "BlitzUpdate async on NullCache should not execute the function");
}

[Test]
public void NullCache_Remove_DoesNothing()
{
// Arrange
var cache = new NullBlitzCacheForTesting();

// Act & Assert - Should not throw
Assert.DoesNotThrow(() => cache.Remove("any_key"));
}

[Test]
public void NullCache_GetSemaphoreCount_ReturnsZero()
{
// Arrange
var cache = new NullBlitzCacheForTesting();

// Act
var count = cache.GetSemaphoreCount();

// Assert
Assert.AreEqual(0, count, "NullCache should always return 0 semaphores");
}

[Test]
public void NullCache_Dispose_DoesNotThrow()
{
// Arrange
var cache = new NullBlitzCacheForTesting();

// Act & Assert
Assert.DoesNotThrow(() => cache.Dispose());
}

[Test]
public void NullCache_WithAutomaticCacheKey_ExecutesEveryTime()
{
// Arrange
var cache = new NullBlitzCacheForTesting();
var callCount = 0;
string TestFunction()
{
callCount++;
return $"result_{callCount}";
}

// Act - Using automatic cache key (CallerMemberName)
var result1 = cache.BlitzGet(TestFunction, 1000);
var result2 = cache.BlitzGet(TestFunction, 1000);
var result3 = cache.BlitzGet(TestFunction, 1000);

// Assert
Assert.AreEqual(3, callCount, "NullCache should execute function every time even with auto keys");
Assert.AreEqual("result_1", result1);
Assert.AreEqual("result_2", result2);
Assert.AreEqual("result_3", result3);
}
}
}
Loading