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
{
///
/// Manages caching for search results and frequently accessed context data
///
public class CacheManager : IDisposable
{
private readonly MemoryCache _searchCache;
private readonly MemoryCache _contextCache;
private readonly ContextConfiguration _configuration;
private readonly ILogger _logger;
private readonly ConcurrentDictionary _cacheLocks;
private readonly Timer _cleanupTimer;
public CacheManager(ContextConfiguration configuration, ILogger logger)
{
_configuration = configuration;
_logger = logger;
_cacheLocks = new ConcurrentDictionary();
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);
}
///
/// Gets cached search results for a query
///
public async Task 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;
}
///
/// Caches search results for future use
///
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();
}
}
///
/// Gets cached context entries for a file
///
public async Task?> GetCachedContextEntriesAsync(string filePath)
{
var cacheKey = GenerateFileCacheKey(filePath);
if (_contextCache.TryGetValue(cacheKey, out List? 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;
}
///
/// Caches context entries for a file
///
public async Task CacheContextEntriesAsync(string filePath, List 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();
}
}
///
/// Invalidates cache entries for a specific file
///
public void InvalidateFileCache(string filePath)
{
var cacheKey = GenerateFileCacheKey(filePath);
_contextCache.Remove(cacheKey);
_contextCache.Remove($"{cacheKey}_timestamp");
_logger.LogDebug("Invalidated cache for file: {FilePath}", filePath);
}
///
/// Invalidates all search cache entries for a project
///
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");
}
///
/// Gets cache statistics
///
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 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();
}
}
///
/// Parameters for search operations used in cache key generation
///
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; }
}
///
/// Cache performance and configuration statistics
///
public class CacheStatistics
{
public bool SearchCacheEnabled { get; set; }
public bool ContextCacheEnabled { get; set; }
public int CacheSizeLimit { get; set; }
public double CompactionPercentage { get; set; }
}
}