using System;
using System.Threading;
namespace GB5Shared.Cache
{
///
/// Striped semaphore pool for cache-stampede prevention (Thundering Herd Fix).
///
/// Problem: When a popular Dapr-cached key expires, all concurrent requests
/// simultaneously see a miss and fan out to the database. At 1,000 req/s for
/// a shared dropdown endpoint this fires 1,000 identical DB queries every TTL.
///
/// Fix: Every cache miss acquires a per-key semaphore before executing the DB
/// query. The first arrival runs the query and writes the result; subsequent
/// arrivals wait (up to TimeoutMs), then double-check the now-populated cache
/// before deciding whether to re-query.
///
/// Implementation — striped pool:
/// Instead of one SemaphoreSlim per key (unbounded memory), we use a fixed
/// pool of slots. Each cache key is hashed to a slot.
/// Multiple keys may share a slot (false contention), but with 4096 stripes
/// and 100 concurrent requests the expected false-contention rate is ~0.3% —
/// negligible compared to the thundering-herd problem being solved.
///
/// Memory: 4096 × ~100 bytes = ~400 KB — constant, no growth.
///
/// Usage:
/// Register as a singleton in DI.
/// In the cache-miss branch:
///
/// var sem = _lockRegistry.GetLock(cacheKey);
/// bool held = await sem.WaitAsync(CacheLockRegistry.TimeoutMs, ct);
/// try
/// {
/// if (held)
/// {
/// // Double-check: another request may have populated while we waited.
/// var recheck = await TryGetFromCacheAsync(cacheKey, ct);
/// if (recheck != null) { await SendAsync(recheck, cancellation: ct); return; }
/// }
/// // held=false (timeout) → still run the query; don't block the request.
/// var fresh = await ExecuteAsync(req, login, ct);
/// if (held) await SaveToCacheAsync(cacheKey, fresh, ct);
/// await SendAsync(fresh, cancellation: ct);
/// }
/// finally { if (held) sem.Release(); }
///
///
/// Graceful degradation:
/// If elapses the request falls through to a fresh
/// DB query without blocking — correctness is preserved, the DB just receives
/// a few extra queries during a stampede rather than thousands.
///
public sealed class CacheLockRegistry
{
///
/// Number of semaphore stripes in the pool.
/// Power of 2 so the modulo can be reduced to a bitwise AND.
///
public const int Stripes = 4096;
///
/// How long to wait for the semaphore before falling through to a live query.
/// 200 ms is generous for a typical DB round-trip (10–50 ms) while not
/// materially increasing response latency for the waiting request.
///
public const int TimeoutMs = 200;
private readonly SemaphoreSlim[] _slots;
public CacheLockRegistry()
{
_slots = new SemaphoreSlim[Stripes];
for (int i = 0; i < Stripes; i++)
_slots[i] = new SemaphoreSlim(1, 1);
}
///
/// Returns the assigned to this cache key.
/// The mapping is stable for the lifetime of the process.
///
public SemaphoreSlim GetLock(string cacheKey)
{
// String.GetHashCode() is fast and well-distributed.
// & 0x7FFFFFFF strips the sign bit so the result is always non-negative.
int index = (cacheKey.GetHashCode() & 0x7FFFFFFF) & (Stripes - 1);
return _slots[index];
}
}
}