MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.Context/Performance/CacheManager.cs

284 lines
8.7 KiB
C#
Executable File

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using MarketAlly.AIPlugin.Context.Configuration;
using System.Collections.Concurrent;
using System.Security.Cryptography;
using System.Text;
namespace MarketAlly.AIPlugin.Context.Performance
{
/// <summary>
/// Manages caching for search results and frequently accessed context data
/// </summary>
public class CacheManager : IDisposable
{
private readonly MemoryCache _searchCache;
private readonly MemoryCache _contextCache;
private readonly ContextConfiguration _configuration;
private readonly ILogger<CacheManager> _logger;
private readonly ConcurrentDictionary<string, SemaphoreSlim> _cacheLocks;
private readonly Timer _cleanupTimer;
public CacheManager(ContextConfiguration configuration, ILogger<CacheManager> logger)
{
_configuration = configuration;
_logger = logger;
_cacheLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
var cacheOptions = new MemoryCacheOptions
{
SizeLimit = _configuration.Performance.CacheSizeLimit,
CompactionPercentage = _configuration.Performance.CacheCompactionPercentage
};
_searchCache = new MemoryCache(cacheOptions);
_contextCache = new MemoryCache(cacheOptions);
// Setup cleanup timer to run every 10 minutes
_cleanupTimer = new Timer(CleanupExpiredEntries, null, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10));
_logger.LogDebug("Cache manager initialized with size limit: {SizeLimit}", cacheOptions.SizeLimit);
}
/// <summary>
/// Gets cached search results for a query
/// </summary>
public async Task<SearchResults?> GetCachedSearchResultsAsync(string query, string projectPath, SearchParameters parameters)
{
if (!_configuration.Search.EnableCaching)
return null;
var cacheKey = GenerateSearchCacheKey(query, projectPath, parameters);
if (_searchCache.TryGetValue(cacheKey, out SearchResults? results))
{
_logger.LogDebug("Cache hit for search query: {Query}", query);
return results;
}
_logger.LogDebug("Cache miss for search query: {Query}", query);
return null;
}
/// <summary>
/// Caches search results for future use
/// </summary>
public async Task CacheSearchResultsAsync(string query, string projectPath, SearchParameters parameters, SearchResults results)
{
if (!_configuration.Search.EnableCaching)
return;
var cacheKey = GenerateSearchCacheKey(query, projectPath, parameters);
var lockKey = $"search_{cacheKey}";
var lockObject = _cacheLocks.GetOrAdd(lockKey, _ => new SemaphoreSlim(1, 1));
await lockObject.WaitAsync();
try
{
var cacheEntryOptions = new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_configuration.Search.CacheExpirationMinutes),
Size = EstimateSearchResultsSize(results),
Priority = CacheItemPriority.Normal
};
_searchCache.Set(cacheKey, results, cacheEntryOptions);
_logger.LogDebug("Cached search results for query: {Query}, expires in {Minutes} minutes",
query, _configuration.Search.CacheExpirationMinutes);
}
finally
{
lockObject.Release();
}
}
/// <summary>
/// Gets cached context entries for a file
/// </summary>
public async Task<List<StoredContextEntry>?> GetCachedContextEntriesAsync(string filePath)
{
var cacheKey = GenerateFileCacheKey(filePath);
if (_contextCache.TryGetValue(cacheKey, out List<StoredContextEntry>? entries))
{
var fileInfo = new System.IO.FileInfo(filePath);
if (_contextCache.TryGetValue($"{cacheKey}_timestamp", out DateTime cachedTimestamp) &&
cachedTimestamp >= fileInfo.LastWriteTime)
{
_logger.LogDebug("Cache hit for context file: {FilePath}", filePath);
return entries;
}
else
{
// File has been modified, remove from cache
_contextCache.Remove(cacheKey);
_contextCache.Remove($"{cacheKey}_timestamp");
}
}
_logger.LogDebug("Cache miss for context file: {FilePath}", filePath);
return null;
}
/// <summary>
/// Caches context entries for a file
/// </summary>
public async Task CacheContextEntriesAsync(string filePath, List<StoredContextEntry> entries)
{
var cacheKey = GenerateFileCacheKey(filePath);
var lockKey = $"context_{cacheKey}";
var lockObject = _cacheLocks.GetOrAdd(lockKey, _ => new SemaphoreSlim(1, 1));
await lockObject.WaitAsync();
try
{
var cacheEntryOptions = new MemoryCacheEntryOptions
{
SlidingExpiration = TimeSpan.FromMinutes(30), // Keep in cache if accessed within 30 minutes
Size = EstimateContextEntriesSize(entries),
Priority = CacheItemPriority.High // Context entries are high priority
};
_contextCache.Set(cacheKey, entries, cacheEntryOptions);
_contextCache.Set($"{cacheKey}_timestamp", File.GetLastWriteTime(filePath), cacheEntryOptions);
_logger.LogDebug("Cached {Count} context entries for file: {FilePath}", entries.Count, filePath);
}
finally
{
lockObject.Release();
}
}
/// <summary>
/// Invalidates cache entries for a specific file
/// </summary>
public void InvalidateFileCache(string filePath)
{
var cacheKey = GenerateFileCacheKey(filePath);
_contextCache.Remove(cacheKey);
_contextCache.Remove($"{cacheKey}_timestamp");
_logger.LogDebug("Invalidated cache for file: {FilePath}", filePath);
}
/// <summary>
/// Invalidates all search cache entries for a project
/// </summary>
public void InvalidateProjectSearchCache(string projectPath)
{
// Unfortunately, MemoryCache doesn't support wildcard removal
// In a production system, you might want to use a more sophisticated cache like Redis
_searchCache.Clear();
_logger.LogInformation("Cleared search cache for project changes");
}
/// <summary>
/// Gets cache statistics
/// </summary>
public CacheStatistics GetStatistics()
{
// Note: MemoryCache doesn't expose detailed statistics
// In production, you might want to track these manually
return new CacheStatistics
{
SearchCacheEnabled = _configuration.Search.EnableCaching,
ContextCacheEnabled = true,
CacheSizeLimit = _configuration.Performance.CacheSizeLimit,
CompactionPercentage = _configuration.Performance.CacheCompactionPercentage
};
}
private string GenerateSearchCacheKey(string query, string projectPath, SearchParameters parameters)
{
var keyBuilder = new StringBuilder();
keyBuilder.Append($"search_{query}_{projectPath}");
keyBuilder.Append($"_{parameters.ContextType}_{parameters.Priority}");
keyBuilder.Append($"_{parameters.DaysBack}_{parameters.MaxResults}");
keyBuilder.Append($"_{parameters.IncludeContent}");
if (!string.IsNullOrEmpty(parameters.Tags))
{
keyBuilder.Append($"_{parameters.Tags}");
}
return GenerateHashKey(keyBuilder.ToString());
}
private string GenerateFileCacheKey(string filePath)
{
return GenerateHashKey($"file_{filePath}");
}
private string GenerateHashKey(string input)
{
using var sha256 = SHA256.Create();
var hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(input));
return Convert.ToBase64String(hashBytes).Replace("/", "_").Replace("+", "-").TrimEnd('=');
}
private long EstimateSearchResultsSize(SearchResults results)
{
// Rough estimation: each result ~1KB
return results.Results.Count * 1024;
}
private long EstimateContextEntriesSize(List<StoredContextEntry> entries)
{
// Rough estimation based on content length
return entries.Sum(e => e.Content.Length + e.Summary.Length + 500); // 500 bytes overhead per entry
}
private void CleanupExpiredEntries(object? state)
{
try
{
_searchCache.Compact(1.0); // Force cleanup of expired entries
_contextCache.Compact(1.0);
_logger.LogDebug("Completed cache cleanup cycle");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during cache cleanup");
}
}
public void Dispose()
{
_cleanupTimer?.Dispose();
_searchCache?.Dispose();
_contextCache?.Dispose();
foreach (var lockObject in _cacheLocks.Values)
{
lockObject.Dispose();
}
_cacheLocks.Clear();
}
}
/// <summary>
/// Parameters for search operations used in cache key generation
/// </summary>
public class SearchParameters
{
public string ContextType { get; set; } = "all";
public string Priority { get; set; } = "all";
public int DaysBack { get; set; } = 0;
public int MaxResults { get; set; } = 10;
public bool IncludeContent { get; set; } = true;
public string? Tags { get; set; }
}
/// <summary>
/// Cache performance and configuration statistics
/// </summary>
public class CacheStatistics
{
public bool SearchCacheEnabled { get; set; }
public bool ContextCacheEnabled { get; set; }
public int CacheSizeLimit { get; set; }
public double CompactionPercentage { get; set; }
}
}