complete refactor
This commit is contained in:
208
Blueberry.Redmine/RedmineCache.cs
Normal file
208
Blueberry.Redmine/RedmineCache.cs
Normal file
@@ -0,0 +1,208 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Blueberry.Redmine
|
||||
{
|
||||
class RedmineCache<T>
|
||||
{
|
||||
private List<T> Items { get; set; } = [];
|
||||
|
||||
public DateTime LastUpdated { get; set; } = DateTime.MinValue;
|
||||
private readonly ILogger _logger;
|
||||
private readonly TimeSpan _cacheDuration;
|
||||
private readonly object _lock = new();
|
||||
private int _maxCapacity = int.MaxValue;
|
||||
private bool _useSlidingExpiration = false;
|
||||
private DateTime _lastAccessed = DateTime.MinValue;
|
||||
private string? _cacheFilePath;
|
||||
private Func<Task<List<T>>>? _refreshCallback;
|
||||
|
||||
private class CacheData
|
||||
{
|
||||
public List<T> Items { get; set; } = [];
|
||||
public DateTime LastUpdated { get; set; }
|
||||
public DateTime LastAccessed { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the RedmineCache class.
|
||||
/// </summary>
|
||||
/// <param name="cacheDuration">The time span for which the cache remains valid. After this duration, the cache is considered expired.</param>
|
||||
/// <param name="logger">The logger instance for logging cache operations.</param>
|
||||
/// <param name="maxCapacity">The maximum number of items to store in the cache. Defaults to int.MaxValue (unlimited).</param>
|
||||
/// <param name="useSlidingExpiration">If true, the cache expiration resets on each access (sliding expiration). If false, uses absolute expiration from the last update. Defaults to false.</param>
|
||||
/// <param name="cacheFilePath">Optional file path for persisting the cache to disk. If provided, the cache loads from and saves to this file. Defaults to null (no persistence).</param>
|
||||
/// <param name="refreshCallback">Optional asynchronous callback function to refresh the cache when it expires. Called automatically in GetItemsAsync if the cache is invalid. Defaults to null.</param>
|
||||
public RedmineCache(TimeSpan cacheDuration, ILogger<RedmineCache<T>> logger, int maxCapacity = int.MaxValue, bool useSlidingExpiration = false,
|
||||
string? cacheFilePath = null, Func<Task<List<T>>>? refreshCallback = null)
|
||||
{
|
||||
if (logger == null) throw new ArgumentNullException(nameof(logger));
|
||||
if (cacheDuration <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(cacheDuration));
|
||||
|
||||
_logger = logger;
|
||||
_cacheDuration = cacheDuration;
|
||||
_maxCapacity = maxCapacity;
|
||||
_useSlidingExpiration = useSlidingExpiration;
|
||||
_cacheFilePath = cacheFilePath;
|
||||
_refreshCallback = refreshCallback;
|
||||
|
||||
LoadFromFile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the RedmineCache class with cache duration in seconds.
|
||||
/// </summary>
|
||||
/// <param name="cacheDurationSec">The cache duration in seconds. Converted to a TimeSpan internally.</param>
|
||||
/// <param name="logger">The logger instance for logging cache operations.</param>
|
||||
/// <param name="maxCapacity">The maximum number of items to store in the cache. Defaults to int.MaxValue (unlimited).</param>
|
||||
/// <param name="useSlidingExpiration">If true, the cache expiration resets on each access (sliding expiration). If false, uses absolute expiration from the last update. Defaults to false.</param>
|
||||
/// <param name="cacheFilePath">Optional file path for persisting the cache to disk. If provided, the cache loads from and saves to this file. Defaults to null (no persistence).</param>
|
||||
/// <param name="refreshCallback">Optional asynchronous callback function to refresh the cache when it expires. Called automatically in GetItemsAsync if the cache is invalid. Defaults to null.</param>
|
||||
public RedmineCache(int cacheDurationSec, ILogger<RedmineCache<T>> logger, int maxCapacity = int.MaxValue, bool useSlidingExpiration = false,
|
||||
string? cacheFilePath = null, Func<Task<List<T>>>? refreshCallback = null)
|
||||
: this(new TimeSpan(0, 0, cacheDurationSec), logger, maxCapacity, useSlidingExpiration, cacheFilePath, refreshCallback) { }
|
||||
|
||||
private void LoadFromFile()
|
||||
{
|
||||
if (_cacheFilePath == null || !File.Exists(_cacheFilePath)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_cacheFilePath);
|
||||
var data = JsonSerializer.Deserialize<CacheData>(json);
|
||||
if (data != null)
|
||||
{
|
||||
Items = data.Items ?? [];
|
||||
LastUpdated = data.LastUpdated;
|
||||
_lastAccessed = data.LastAccessed;
|
||||
}
|
||||
_logger.LogDebug("Loaded cache from {path}", _cacheFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to load cache from {path}", _cacheFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveToFile()
|
||||
{
|
||||
if (_cacheFilePath == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var data = new CacheData { Items = Items, LastUpdated = LastUpdated, LastAccessed = _lastAccessed };
|
||||
var json = JsonSerializer.Serialize(data);
|
||||
var dir = Path.GetDirectoryName(_cacheFilePath) ?? throw new NullReferenceException();
|
||||
Directory.CreateDirectory(dir);
|
||||
File.WriteAllText(_cacheFilePath, json);
|
||||
_logger.LogDebug("Saved cache to {path}", _cacheFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to save cache to {path}", _cacheFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshCache(List<T> newItems)
|
||||
{
|
||||
_logger.LogDebug("Refreshing cache with {count} items", newItems.Count);
|
||||
lock (_lock)
|
||||
{
|
||||
Items = newItems.Count > _maxCapacity ? newItems.Take(_maxCapacity).ToList() : newItems;
|
||||
LastUpdated = DateTime.UtcNow;
|
||||
_lastAccessed = DateTime.UtcNow;
|
||||
SaveToFile();
|
||||
_logger.LogDebug("Cache refreshed");
|
||||
}
|
||||
}
|
||||
|
||||
public void InvalidateCache()
|
||||
{
|
||||
_logger.LogDebug("Invalidating cache");
|
||||
lock (_lock)
|
||||
{
|
||||
LastUpdated = DateTime.MinValue;
|
||||
_lastAccessed = DateTime.MinValue;
|
||||
Items.Clear();
|
||||
SaveToFile();
|
||||
_logger.LogDebug("Cache invalidated");
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsCacheValid()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
bool valid = !_useSlidingExpiration ? DateTime.UtcNow - LastUpdated <= _cacheDuration : DateTime.UtcNow - _lastAccessed <= _cacheDuration;
|
||||
_logger.LogDebug("Cache valid: {valid}", valid);
|
||||
return valid;
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<T> GetItems()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_lastAccessed = DateTime.UtcNow;
|
||||
_logger.LogDebug("Returning {count} cached items", Items.Count);
|
||||
return Items.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
public Task RefreshCacheAsync(List<T> newItems) => Task.Run(() => RefreshCache(newItems));
|
||||
|
||||
public async Task<IReadOnlyList<T>> GetItemsAsync()
|
||||
{
|
||||
bool needsRefresh = false;
|
||||
lock (_lock)
|
||||
{
|
||||
_lastAccessed = DateTime.UtcNow;
|
||||
if (!IsCacheValid() && _refreshCallback != null)
|
||||
{
|
||||
needsRefresh = true;
|
||||
}
|
||||
}
|
||||
if (needsRefresh)
|
||||
{
|
||||
await TryRefreshAsync();
|
||||
}
|
||||
lock (_lock)
|
||||
{
|
||||
_logger.LogDebug("Returning {count} cached items", Items.Count);
|
||||
return Items.AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEmpty()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return Items.Count == 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int GetCount()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return Items.Count;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task TryRefreshAsync()
|
||||
{
|
||||
if (_refreshCallback != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newItems = await _refreshCallback();
|
||||
RefreshCache(newItems);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to refresh cache via callback");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user