255 lines
8.4 KiB
C#
Executable File
255 lines
8.4 KiB
C#
Executable File
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.IO;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace MarketAlly.AIPlugin.DevOps.Performance
|
|
{
|
|
public class AnalysisCache
|
|
{
|
|
private readonly ConcurrentDictionary<string, CacheEntry> _cache;
|
|
private readonly ILogger<AnalysisCache> _logger;
|
|
private readonly TimeSpan _defaultExpiry;
|
|
|
|
public AnalysisCache(ILogger<AnalysisCache> logger = null, TimeSpan? defaultExpiry = null)
|
|
{
|
|
_cache = new ConcurrentDictionary<string, CacheEntry>();
|
|
_logger = logger;
|
|
_defaultExpiry = defaultExpiry ?? TimeSpan.FromHours(1);
|
|
}
|
|
|
|
public async Task<T> GetOrSetAsync<T>(string key, Func<Task<T>> factory, TimeSpan? expiry = null) where T : class
|
|
{
|
|
var cacheKey = GenerateCacheKey(key);
|
|
var expiryTime = expiry ?? _defaultExpiry;
|
|
|
|
if (_cache.TryGetValue(cacheKey, out var existingEntry))
|
|
{
|
|
if (existingEntry.ExpiresAt > DateTime.UtcNow)
|
|
{
|
|
try
|
|
{
|
|
var cachedResult = JsonSerializer.Deserialize<T>(existingEntry.Data);
|
|
_logger?.LogDebug("Cache hit for key: {CacheKey}", cacheKey);
|
|
return cachedResult;
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
_logger?.LogWarning(ex, "Failed to deserialize cached data for key: {CacheKey}", cacheKey);
|
|
_cache.TryRemove(cacheKey, out _);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_cache.TryRemove(cacheKey, out _);
|
|
_logger?.LogDebug("Cache entry expired for key: {CacheKey}", cacheKey);
|
|
}
|
|
}
|
|
|
|
_logger?.LogDebug("Cache miss for key: {CacheKey}, executing factory", cacheKey);
|
|
var result = await factory();
|
|
|
|
if (result != null)
|
|
{
|
|
await SetAsync(cacheKey, result, expiryTime);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
public async Task<T> GetAsync<T>(string key) where T : class
|
|
{
|
|
var cacheKey = GenerateCacheKey(key);
|
|
|
|
if (_cache.TryGetValue(cacheKey, out var entry))
|
|
{
|
|
if (entry.ExpiresAt > DateTime.UtcNow)
|
|
{
|
|
try
|
|
{
|
|
var result = JsonSerializer.Deserialize<T>(entry.Data);
|
|
_logger?.LogDebug("Retrieved from cache: {CacheKey}", cacheKey);
|
|
return result;
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
_logger?.LogWarning(ex, "Failed to deserialize cached data for key: {CacheKey}", cacheKey);
|
|
_cache.TryRemove(cacheKey, out _);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_cache.TryRemove(cacheKey, out _);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public async Task SetAsync<T>(string key, T value, TimeSpan? expiry = null)
|
|
{
|
|
var cacheKey = GenerateCacheKey(key);
|
|
var expiryTime = expiry ?? _defaultExpiry;
|
|
|
|
try
|
|
{
|
|
var jsonData = JsonSerializer.Serialize(value, new JsonSerializerOptions
|
|
{
|
|
WriteIndented = false,
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
});
|
|
|
|
var entry = new CacheEntry
|
|
{
|
|
Data = jsonData,
|
|
CreatedAt = DateTime.UtcNow,
|
|
ExpiresAt = DateTime.UtcNow.Add(expiryTime)
|
|
};
|
|
|
|
_cache.AddOrUpdate(cacheKey, entry, (k, v) => entry);
|
|
_logger?.LogDebug("Cached data for key: {CacheKey}, expires at: {ExpiresAt}", cacheKey, entry.ExpiresAt);
|
|
|
|
await Task.CompletedTask;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogError(ex, "Failed to cache data for key: {CacheKey}", cacheKey);
|
|
}
|
|
}
|
|
|
|
public async Task<string> GenerateFileBasedCacheKeyAsync(string filePath, string operation)
|
|
{
|
|
if (!File.Exists(filePath))
|
|
{
|
|
return $"{operation}:{filePath}:notfound";
|
|
}
|
|
|
|
try
|
|
{
|
|
var fileInfo = new FileInfo(filePath);
|
|
var lastWriteTime = fileInfo.LastWriteTimeUtc.Ticks;
|
|
var fileSize = fileInfo.Length;
|
|
|
|
// Create a hash of file metadata for cache key
|
|
var keyData = $"{operation}:{filePath}:{lastWriteTime}:{fileSize}";
|
|
|
|
using var sha256 = SHA256.Create();
|
|
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(keyData));
|
|
var hashString = Convert.ToHexString(hashBytes)[..16]; // Use first 16 characters
|
|
|
|
return $"{operation}:{hashString}";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogWarning(ex, "Failed to generate file-based cache key for: {FilePath}", filePath);
|
|
return $"{operation}:{filePath}:error";
|
|
}
|
|
}
|
|
|
|
public void InvalidateByPattern(string pattern)
|
|
{
|
|
var keysToRemove = new List<string>();
|
|
|
|
foreach (var kvp in _cache)
|
|
{
|
|
if (kvp.Key.Contains(pattern, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
keysToRemove.Add(kvp.Key);
|
|
}
|
|
}
|
|
|
|
foreach (var key in keysToRemove)
|
|
{
|
|
_cache.TryRemove(key, out _);
|
|
_logger?.LogDebug("Invalidated cache entry: {CacheKey}", key);
|
|
}
|
|
|
|
_logger?.LogInformation("Invalidated {Count} cache entries matching pattern: {Pattern}",
|
|
keysToRemove.Count, pattern);
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
var count = _cache.Count;
|
|
_cache.Clear();
|
|
_logger?.LogInformation("Cleared {Count} cache entries", count);
|
|
}
|
|
|
|
public void CleanupExpired()
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var expiredKeys = new List<string>();
|
|
|
|
foreach (var kvp in _cache)
|
|
{
|
|
if (kvp.Value.ExpiresAt <= now)
|
|
{
|
|
expiredKeys.Add(kvp.Key);
|
|
}
|
|
}
|
|
|
|
foreach (var key in expiredKeys)
|
|
{
|
|
_cache.TryRemove(key, out _);
|
|
}
|
|
|
|
if (expiredKeys.Count > 0)
|
|
{
|
|
_logger?.LogDebug("Cleaned up {Count} expired cache entries", expiredKeys.Count);
|
|
}
|
|
}
|
|
|
|
public CacheStatistics GetStatistics()
|
|
{
|
|
var now = DateTime.UtcNow;
|
|
var validEntries = 0;
|
|
var expiredEntries = 0;
|
|
|
|
foreach (var kvp in _cache)
|
|
{
|
|
if (kvp.Value.ExpiresAt > now)
|
|
{
|
|
validEntries++;
|
|
}
|
|
else
|
|
{
|
|
expiredEntries++;
|
|
}
|
|
}
|
|
|
|
return new CacheStatistics
|
|
{
|
|
TotalEntries = _cache.Count,
|
|
ValidEntries = validEntries,
|
|
ExpiredEntries = expiredEntries,
|
|
HitRate = 0 // Would need to track hits/misses for actual hit rate
|
|
};
|
|
}
|
|
|
|
private string GenerateCacheKey(string key)
|
|
{
|
|
using var sha256 = SHA256.Create();
|
|
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(key));
|
|
return Convert.ToHexString(hashBytes)[..16]; // Use first 16 characters for shorter keys
|
|
}
|
|
|
|
private class CacheEntry
|
|
{
|
|
public string Data { get; set; }
|
|
public DateTime CreatedAt { get; set; }
|
|
public DateTime ExpiresAt { get; set; }
|
|
}
|
|
}
|
|
|
|
public class CacheStatistics
|
|
{
|
|
public int TotalEntries { get; set; }
|
|
public int ValidEntries { get; set; }
|
|
public int ExpiredEntries { get; set; }
|
|
public double HitRate { get; set; }
|
|
}
|
|
} |