stabilizaiton work - projects rework for maintenanceability and ui livening
This commit is contained in:
@@ -9,3 +9,4 @@ Source of truth: `docs-archived/implplan/2025-12-29-csproj-audit/SPRINT_20251229
|
||||
| AUDIT-0100-T | DONE | Revalidated 2026-01-08; test coverage audit for Provcache.Valkey. |
|
||||
| AUDIT-0100-A | DONE | Applied 2026-01-13 (SCAN invalidation, cancellation propagation; test gaps tracked). |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
| REMED-08 | DONE | Split ValkeyProvcacheStore into partials <= 100 lines; added Valkey unit tests; `dotnet test src/__Libraries/__Tests/StellaOps.Provcache.Valkey.Tests/StellaOps.Provcache.Valkey.Tests.csproj` passed (11 tests) 2026-02-03. |
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StackExchange.Redis;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Provcache.Valkey;
|
||||
|
||||
public sealed partial class ValkeyProvcacheStore
|
||||
{
|
||||
public async ValueTask<ProvcacheLookupResult> GetAsync(string veriKey, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
var redisKey = BuildKey(veriKey);
|
||||
|
||||
var value = await db.StringGetAsync(redisKey).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
|
||||
if (value.IsNullOrEmpty)
|
||||
{
|
||||
_logger.LogDebug("Cache miss for VeriKey {VeriKey} in {ElapsedMs}ms", veriKey, sw.Elapsed.TotalMilliseconds);
|
||||
return ProvcacheLookupResult.Miss(sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
|
||||
var entry = JsonSerializer.Deserialize<ProvcacheEntry>((string)value!, _jsonOptions);
|
||||
if (entry is null)
|
||||
{
|
||||
_logger.LogWarning("Failed to deserialize cache entry for VeriKey {VeriKey}", veriKey);
|
||||
return ProvcacheLookupResult.Miss(sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
|
||||
if (_options.SlidingExpiration)
|
||||
{
|
||||
var ttl = entry.ExpiresAt - _timeProvider.GetUtcNow();
|
||||
if (ttl > TimeSpan.Zero)
|
||||
{
|
||||
await db.KeyExpireAsync(redisKey, ttl).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("Cache hit for VeriKey {VeriKey} in {ElapsedMs}ms", veriKey, sw.Elapsed.TotalMilliseconds);
|
||||
return ProvcacheLookupResult.Hit(entry, ProviderName, sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
_logger.LogError(ex, "Error getting cache entry for VeriKey {VeriKey}", veriKey);
|
||||
return ProvcacheLookupResult.Miss(sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StackExchange.Redis;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Provcache.Valkey;
|
||||
|
||||
public sealed partial class ValkeyProvcacheStore
|
||||
{
|
||||
public async ValueTask<ProvcacheBatchLookupResult> GetManyAsync(
|
||||
IEnumerable<string> veriKeys,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var keyList = veriKeys.ToList();
|
||||
|
||||
if (keyList.Count == 0)
|
||||
{
|
||||
return new ProvcacheBatchLookupResult
|
||||
{
|
||||
Hits = new Dictionary<string, ProvcacheEntry>(),
|
||||
Misses = [],
|
||||
ElapsedMs = 0
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
var redisKeys = keyList.Select(k => (RedisKey)BuildKey(k)).ToArray();
|
||||
|
||||
var values = await db.StringGetAsync(redisKeys).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
|
||||
var hits = new Dictionary<string, ProvcacheEntry>();
|
||||
var misses = new List<string>();
|
||||
|
||||
for (int i = 0; i < keyList.Count; i++)
|
||||
{
|
||||
var veriKey = keyList[i];
|
||||
var value = values[i];
|
||||
|
||||
if (value.IsNullOrEmpty)
|
||||
{
|
||||
misses.Add(veriKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
var entry = JsonSerializer.Deserialize<ProvcacheEntry>((string)value!, _jsonOptions);
|
||||
if (entry is not null)
|
||||
{
|
||||
hits[veriKey] = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
misses.Add(veriKey);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Batch lookup: {Hits} hits, {Misses} misses in {ElapsedMs}ms",
|
||||
hits.Count,
|
||||
misses.Count,
|
||||
sw.Elapsed.TotalMilliseconds);
|
||||
|
||||
return new ProvcacheBatchLookupResult
|
||||
{
|
||||
Hits = hits,
|
||||
Misses = misses,
|
||||
ElapsedMs = sw.Elapsed.TotalMilliseconds
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
_logger.LogError(ex, "Error in batch cache lookup");
|
||||
return new ProvcacheBatchLookupResult
|
||||
{
|
||||
Hits = new Dictionary<string, ProvcacheEntry>(),
|
||||
Misses = keyList,
|
||||
ElapsedMs = sw.Elapsed.TotalMilliseconds
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace StellaOps.Provcache.Valkey;
|
||||
|
||||
public sealed partial class ValkeyProvcacheStore
|
||||
{
|
||||
public async ValueTask<ProvcacheEntry> GetOrSetAsync(
|
||||
string veriKey,
|
||||
Func<CancellationToken, ValueTask<ProvcacheEntry>> factory,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var result = await GetAsync(veriKey, cancellationToken).ConfigureAwait(false);
|
||||
if (result.IsHit && result.Entry is not null)
|
||||
{
|
||||
return result.Entry;
|
||||
}
|
||||
|
||||
var entry = await factory(cancellationToken).ConfigureAwait(false);
|
||||
await SetAsync(entry, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Provcache.Valkey;
|
||||
|
||||
public sealed partial class ValkeyProvcacheStore
|
||||
{
|
||||
public async ValueTask<bool> InvalidateAsync(string veriKey, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
var redisKey = BuildKey(veriKey);
|
||||
|
||||
var deleted = await db.KeyDeleteAsync(redisKey).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Invalidated cache entry for VeriKey {VeriKey}: {Deleted}", veriKey, deleted);
|
||||
return deleted;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invalidating cache entry for VeriKey {VeriKey}", veriKey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace StellaOps.Provcache.Valkey;
|
||||
|
||||
public sealed partial class ValkeyProvcacheStore
|
||||
{
|
||||
public async ValueTask<long> InvalidateByPatternAsync(string pattern, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var endpoints = _connectionMultiplexer.GetEndPoints();
|
||||
if (endpoints.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var fullPattern = $"{_options.ValkeyKeyPrefix}{pattern}";
|
||||
long deleted = 0;
|
||||
|
||||
foreach (var endpoint in endpoints)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var server = _connectionMultiplexer.GetServer(endpoint);
|
||||
if (server is null || !server.IsConnected)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var batchKeys = new List<RedisKey>(DefaultDeleteBatchSize);
|
||||
foreach (var key in server.Keys(pattern: fullPattern, pageSize: DefaultScanPageSize))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
batchKeys.Add(key);
|
||||
if (batchKeys.Count >= DefaultDeleteBatchSize)
|
||||
{
|
||||
deleted += await db.KeyDeleteAsync(batchKeys.ToArray()).ConfigureAwait(false);
|
||||
batchKeys.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (batchKeys.Count > 0)
|
||||
{
|
||||
deleted += await db.KeyDeleteAsync(batchKeys.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Invalidated {Count} cache entries matching pattern {Pattern}", deleted, pattern);
|
||||
return deleted;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invalidating cache entries by pattern {Pattern}", pattern);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Provcache.Valkey;
|
||||
|
||||
public sealed partial class ValkeyProvcacheStore
|
||||
{
|
||||
public async ValueTask SetAsync(ProvcacheEntry entry, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
var redisKey = BuildKey(entry.VeriKey);
|
||||
var value = JsonSerializer.Serialize(entry, _jsonOptions);
|
||||
|
||||
var ttl = entry.ExpiresAt - _timeProvider.GetUtcNow();
|
||||
if (ttl <= TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogDebug("Skipping expired entry for VeriKey {VeriKey}", entry.VeriKey);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ttl > _options.MaxTtl)
|
||||
{
|
||||
ttl = _options.MaxTtl;
|
||||
}
|
||||
|
||||
await db.StringSetAsync(redisKey, value, ttl).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Stored cache entry for VeriKey {VeriKey} with TTL {Ttl}", entry.VeriKey, ttl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error storing cache entry for VeriKey {VeriKey}", entry.VeriKey);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Provcache.Valkey;
|
||||
|
||||
public sealed partial class ValkeyProvcacheStore
|
||||
{
|
||||
public async ValueTask SetManyAsync(IEnumerable<ProvcacheEntry> entries, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entryList = entries.ToList();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (entryList.Count == 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
var batch = db.CreateBatch();
|
||||
var tasks = new List<Task>();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
foreach (var entry in entryList)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var redisKey = BuildKey(entry.VeriKey);
|
||||
var value = JsonSerializer.Serialize(entry, _jsonOptions);
|
||||
|
||||
var ttl = entry.ExpiresAt - now;
|
||||
if (ttl <= TimeSpan.Zero)
|
||||
continue;
|
||||
|
||||
if (ttl > _options.MaxTtl)
|
||||
ttl = _options.MaxTtl;
|
||||
|
||||
tasks.Add(batch.StringSetAsync(redisKey, value, ttl));
|
||||
}
|
||||
|
||||
batch.Execute();
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Batch stored {Count} cache entries", entryList.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in batch cache store");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StackExchange.Redis;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace StellaOps.Provcache.Valkey;
|
||||
|
||||
/// <summary>
|
||||
/// Valkey/Redis implementation of <see cref="IProvcacheStore"/> with read-through caching.
|
||||
/// </summary>
|
||||
public sealed class ValkeyProvcacheStore : IProvcacheStore, IAsyncDisposable
|
||||
public sealed partial class ValkeyProvcacheStore : IProvcacheStore, IAsyncDisposable
|
||||
{
|
||||
private const int DefaultScanPageSize = 200;
|
||||
private const int DefaultDeleteBatchSize = 500;
|
||||
|
||||
private readonly IConnectionMultiplexer _connectionMultiplexer;
|
||||
private readonly ProvcacheOptions _options;
|
||||
private readonly ILogger<ValkeyProvcacheStore> _logger;
|
||||
@@ -19,10 +17,7 @@ public sealed class ValkeyProvcacheStore : IProvcacheStore, IAsyncDisposable
|
||||
private readonly JsonSerializerOptions _jsonOptions;
|
||||
private readonly SemaphoreSlim _connectionLock = new(1, 1);
|
||||
private IDatabase? _database;
|
||||
private const int DefaultScanPageSize = 200;
|
||||
private const int DefaultDeleteBatchSize = 500;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ProviderName => "valkey";
|
||||
|
||||
public ValkeyProvcacheStore(
|
||||
@@ -42,303 +37,6 @@ public sealed class ValkeyProvcacheStore : IProvcacheStore, IAsyncDisposable
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<ProvcacheLookupResult> GetAsync(string veriKey, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
var redisKey = BuildKey(veriKey);
|
||||
|
||||
var value = await db.StringGetAsync(redisKey).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
|
||||
if (value.IsNullOrEmpty)
|
||||
{
|
||||
_logger.LogDebug("Cache miss for VeriKey {VeriKey} in {ElapsedMs}ms", veriKey, sw.Elapsed.TotalMilliseconds);
|
||||
return ProvcacheLookupResult.Miss(sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
|
||||
var entry = JsonSerializer.Deserialize<ProvcacheEntry>((string)value!, _jsonOptions);
|
||||
if (entry is null)
|
||||
{
|
||||
_logger.LogWarning("Failed to deserialize cache entry for VeriKey {VeriKey}", veriKey);
|
||||
return ProvcacheLookupResult.Miss(sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
|
||||
// Optionally refresh TTL on read (sliding expiration)
|
||||
if (_options.SlidingExpiration)
|
||||
{
|
||||
var ttl = entry.ExpiresAt - _timeProvider.GetUtcNow();
|
||||
if (ttl > TimeSpan.Zero)
|
||||
{
|
||||
await db.KeyExpireAsync(redisKey, ttl).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("Cache hit for VeriKey {VeriKey} in {ElapsedMs}ms", veriKey, sw.Elapsed.TotalMilliseconds);
|
||||
return ProvcacheLookupResult.Hit(entry, ProviderName, sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
_logger.LogError(ex, "Error getting cache entry for VeriKey {VeriKey}", veriKey);
|
||||
return ProvcacheLookupResult.Miss(sw.Elapsed.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<ProvcacheBatchLookupResult> GetManyAsync(
|
||||
IEnumerable<string> veriKeys,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var keyList = veriKeys.ToList();
|
||||
|
||||
if (keyList.Count == 0)
|
||||
{
|
||||
return new ProvcacheBatchLookupResult
|
||||
{
|
||||
Hits = new Dictionary<string, ProvcacheEntry>(),
|
||||
Misses = [],
|
||||
ElapsedMs = 0
|
||||
};
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
var redisKeys = keyList.Select(k => (RedisKey)BuildKey(k)).ToArray();
|
||||
|
||||
var values = await db.StringGetAsync(redisKeys).ConfigureAwait(false);
|
||||
sw.Stop();
|
||||
|
||||
var hits = new Dictionary<string, ProvcacheEntry>();
|
||||
var misses = new List<string>();
|
||||
|
||||
for (int i = 0; i < keyList.Count; i++)
|
||||
{
|
||||
var veriKey = keyList[i];
|
||||
var value = values[i];
|
||||
|
||||
if (value.IsNullOrEmpty)
|
||||
{
|
||||
misses.Add(veriKey);
|
||||
continue;
|
||||
}
|
||||
|
||||
var entry = JsonSerializer.Deserialize<ProvcacheEntry>((string)value!, _jsonOptions);
|
||||
if (entry is not null)
|
||||
{
|
||||
hits[veriKey] = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
misses.Add(veriKey);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Batch lookup: {Hits} hits, {Misses} misses in {ElapsedMs}ms",
|
||||
hits.Count,
|
||||
misses.Count,
|
||||
sw.Elapsed.TotalMilliseconds);
|
||||
|
||||
return new ProvcacheBatchLookupResult
|
||||
{
|
||||
Hits = hits,
|
||||
Misses = misses,
|
||||
ElapsedMs = sw.Elapsed.TotalMilliseconds
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
sw.Stop();
|
||||
_logger.LogError(ex, "Error in batch cache lookup");
|
||||
return new ProvcacheBatchLookupResult
|
||||
{
|
||||
Hits = new Dictionary<string, ProvcacheEntry>(),
|
||||
Misses = keyList,
|
||||
ElapsedMs = sw.Elapsed.TotalMilliseconds
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask SetAsync(ProvcacheEntry entry, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
var redisKey = BuildKey(entry.VeriKey);
|
||||
var value = JsonSerializer.Serialize(entry, _jsonOptions);
|
||||
|
||||
var ttl = entry.ExpiresAt - _timeProvider.GetUtcNow();
|
||||
if (ttl <= TimeSpan.Zero)
|
||||
{
|
||||
_logger.LogDebug("Skipping expired entry for VeriKey {VeriKey}", entry.VeriKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// Cap TTL at MaxTtl
|
||||
if (ttl > _options.MaxTtl)
|
||||
{
|
||||
ttl = _options.MaxTtl;
|
||||
}
|
||||
|
||||
await db.StringSetAsync(redisKey, value, ttl).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Stored cache entry for VeriKey {VeriKey} with TTL {Ttl}", entry.VeriKey, ttl);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error storing cache entry for VeriKey {VeriKey}", entry.VeriKey);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask SetManyAsync(IEnumerable<ProvcacheEntry> entries, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entryList = entries.ToList();
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (entryList.Count == 0)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
var batch = db.CreateBatch();
|
||||
var tasks = new List<Task>();
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
|
||||
foreach (var entry in entryList)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var redisKey = BuildKey(entry.VeriKey);
|
||||
var value = JsonSerializer.Serialize(entry, _jsonOptions);
|
||||
|
||||
var ttl = entry.ExpiresAt - now;
|
||||
if (ttl <= TimeSpan.Zero)
|
||||
continue;
|
||||
|
||||
if (ttl > _options.MaxTtl)
|
||||
ttl = _options.MaxTtl;
|
||||
|
||||
tasks.Add(batch.StringSetAsync(redisKey, value, ttl));
|
||||
}
|
||||
|
||||
batch.Execute();
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Batch stored {Count} cache entries", entryList.Count);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in batch cache store");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<bool> InvalidateAsync(string veriKey, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
var redisKey = BuildKey(veriKey);
|
||||
|
||||
var deleted = await db.KeyDeleteAsync(redisKey).ConfigureAwait(false);
|
||||
|
||||
_logger.LogDebug("Invalidated cache entry for VeriKey {VeriKey}: {Deleted}", veriKey, deleted);
|
||||
return deleted;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invalidating cache entry for VeriKey {VeriKey}", veriKey);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<long> InvalidateByPatternAsync(string pattern, CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var db = await GetDatabaseAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var endpoints = _connectionMultiplexer.GetEndPoints();
|
||||
if (endpoints.Length == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var fullPattern = $"{_options.ValkeyKeyPrefix}{pattern}";
|
||||
long deleted = 0;
|
||||
|
||||
foreach (var endpoint in endpoints)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var server = _connectionMultiplexer.GetServer(endpoint);
|
||||
if (server is null || !server.IsConnected)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var batchKeys = new List<RedisKey>(DefaultDeleteBatchSize);
|
||||
foreach (var key in server.Keys(pattern: fullPattern, pageSize: DefaultScanPageSize))
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
batchKeys.Add(key);
|
||||
if (batchKeys.Count >= DefaultDeleteBatchSize)
|
||||
{
|
||||
deleted += await db.KeyDeleteAsync(batchKeys.ToArray()).ConfigureAwait(false);
|
||||
batchKeys.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (batchKeys.Count > 0)
|
||||
{
|
||||
deleted += await db.KeyDeleteAsync(batchKeys.ToArray()).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("Invalidated {Count} cache entries matching pattern {Pattern}", deleted, pattern);
|
||||
return deleted;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error invalidating cache entries by pattern {Pattern}", pattern);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask<ProvcacheEntry> GetOrSetAsync(
|
||||
string veriKey,
|
||||
Func<CancellationToken, ValueTask<ProvcacheEntry>> factory,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var result = await GetAsync(veriKey, cancellationToken).ConfigureAwait(false);
|
||||
if (result.IsHit && result.Entry is not null)
|
||||
{
|
||||
return result.Entry;
|
||||
}
|
||||
|
||||
var entry = await factory(cancellationToken).ConfigureAwait(false);
|
||||
await SetAsync(entry, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
private string BuildKey(string veriKey) => $"{_options.ValkeyKeyPrefix}{veriKey}";
|
||||
|
||||
private async Task<IDatabase> GetDatabaseAsync(CancellationToken cancellationToken)
|
||||
@@ -346,7 +44,9 @@ public sealed class ValkeyProvcacheStore : IProvcacheStore, IAsyncDisposable
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (_database is not null)
|
||||
{
|
||||
return _database;
|
||||
}
|
||||
|
||||
await _connectionLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user