using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Dapr.Client;
using GB5Shared.Cache;
using Microsoft.Extensions.Logging;
using static GB5Shared.GB5Constant.Constant;
namespace GB5Shared.DALCache
{
///
/// DAL-level cache backed by the same Dapr state store used by the SL endpoint cache.
/// Keys are prefixed with "dal:" to isolate DAL entries from SL endpoint responses
/// stored under identical key formulas in the same Redis-backed store.
///
/// Usage pattern — read-through:
/// var key = new CacheKeyGeneration().KeyGeneration(id, EntityConstant.OBJECTFOO, CacheKeyLevel.CLIENT_LEVEL, login);
/// var result = await _dalCache.GetOrSetAsync<FooDTO>(key, async ct => await _qe.QuerySingleAsync<FooDTO>(...), DALCache.TtlForLevel(CacheKeyLevel.CLIENT_LEVEL), ct);
///
/// Usage pattern — invalidation (BLL, after write):
/// var key = new CacheKeyGeneration().KeyGeneration(id, EntityConstant.OBJECTFOO, CacheKeyLevel.CLIENT_LEVEL, login);
/// await _dalCache.InvalidateAsync(key, ct);
///
public class DALCache : IDALCache
{
private readonly DaprClient _daprClient;
private readonly ILogger _logger;
private static readonly CacheLockRegistry _locks = new();
private const string StateStore = "statestore";
private const string KeyPrefix = "dal:";
public DALCache(DaprClient daprClient, ILogger logger)
{
_daprClient = daprClient;
_logger = logger;
}
/// TTL in seconds for each cache level — mirrors BaseEndpoint.TtlForLevel.
public static int TtlForLevel(int level) => level switch
{
CacheKeyLevel.OVERALL => 3600,
CacheKeyLevel.DB_SERVER => 1800,
CacheKeyLevel.CLIENT_LEVEL => 300,
CacheKeyLevel.ROLE_LEVEL => 300,
CacheKeyLevel.USER_LEVEL => 60,
_ => 60
};
public async Task GetOrSetAsync(
string cacheKey,
Func> factory,
int ttlSeconds,
CancellationToken ct = default) where T : class
{
var dalKey = $"{KeyPrefix}{cacheKey}";
var sem = _locks.GetLock(dalKey);
var acquired = await sem.WaitAsync(CacheLockRegistry.TimeoutMs, ct).ConfigureAwait(false);
try
{
// Double-check after acquiring — another request may have populated while waiting.
// A Dapr/statestore outage here must not fail the request — fall through to a live read.
try
{
var cached = await _daprClient.GetStateAsync(StateStore, dalKey,
cancellationToken: ct).ConfigureAwait(false);
if (cached is not null)
return JsonSerializer.Deserialize(cached);
}
catch (Exception cacheEx)
{
_logger.LogWarning(cacheEx, "DALCache read skipped — Dapr unavailable for key: {CacheKey}", dalKey);
}
var result = await factory(ct).ConfigureAwait(false);
// Only the lock holder writes; null results are not cached (record not found).
if (result is not null && acquired)
{
try
{
var json = JsonSerializer.Serialize(result);
var meta = new Dictionary { ["ttlInSeconds"] = ttlSeconds.ToString() };
await _daprClient.SaveStateAsync(StateStore, dalKey, json,
metadata: meta, cancellationToken: ct).ConfigureAwait(false);
}
catch (Exception cacheEx)
{
_logger.LogWarning(cacheEx, "DALCache write skipped — Dapr unavailable for key: {CacheKey}", dalKey);
}
}
return result;
}
finally
{
if (acquired) sem.Release();
}
}
public async Task InvalidateAsync(string cacheKey, CancellationToken ct = default)
{
try
{
await _daprClient.DeleteStateAsync(StateStore, $"{KeyPrefix}{cacheKey}",
cancellationToken: ct).ConfigureAwait(false);
}
catch (Exception cacheEx)
{
_logger.LogWarning(cacheEx, "DALCache invalidation skipped — Dapr unavailable for key: {CacheKey}", cacheKey);
}
}
}
}