mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-04-25 03:12:54 -04:00
300 lines
8.0 KiB
C#
300 lines
8.0 KiB
C#
using Microsoft.Extensions.Options;
|
|
using allstarr.Models.Settings;
|
|
using StackExchange.Redis;
|
|
using System.Text.Json;
|
|
|
|
namespace allstarr.Services.Common;
|
|
|
|
/// <summary>
|
|
/// Redis caching service for metadata and images.
|
|
/// </summary>
|
|
public class RedisCacheService
|
|
{
|
|
private readonly RedisSettings _settings;
|
|
private readonly ILogger<RedisCacheService> _logger;
|
|
private IConnectionMultiplexer? _redis;
|
|
private IDatabase? _db;
|
|
private readonly object _lock = new();
|
|
|
|
public RedisCacheService(
|
|
IOptions<RedisSettings> settings,
|
|
ILogger<RedisCacheService> logger)
|
|
{
|
|
_settings = settings.Value;
|
|
_logger = logger;
|
|
|
|
if (_settings.Enabled)
|
|
{
|
|
InitializeConnection();
|
|
}
|
|
}
|
|
|
|
private void InitializeConnection()
|
|
{
|
|
try
|
|
{
|
|
_redis = ConnectionMultiplexer.Connect(_settings.ConnectionString);
|
|
_db = _redis.GetDatabase();
|
|
_logger.LogInformation("Redis connected: {ConnectionString}", _settings.ConnectionString);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Redis connection failed. Caching disabled.");
|
|
_redis = null;
|
|
_db = null;
|
|
}
|
|
}
|
|
|
|
public bool IsEnabled => _settings.Enabled && _db != null;
|
|
|
|
/// <summary>
|
|
/// Gets a cached value as a string.
|
|
/// </summary>
|
|
public async Task<string?> GetStringAsync(string key)
|
|
{
|
|
if (!IsEnabled) return null;
|
|
|
|
try
|
|
{
|
|
var value = await _db!.StringGetAsync(key);
|
|
|
|
if (value.HasValue)
|
|
{
|
|
_logger.LogDebug("Redis cache HIT: {Key}", key);
|
|
}
|
|
else
|
|
{
|
|
_logger.LogDebug("Redis cache MISS: {Key}", key);
|
|
}
|
|
return value;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Redis GET failed for key: {Key}", key);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets a cached value and deserializes it.
|
|
/// </summary>
|
|
public async Task<T?> GetAsync<T>(string key) where T : class
|
|
{
|
|
var json = await GetStringAsync(key);
|
|
if (string.IsNullOrEmpty(json)) return null;
|
|
|
|
try
|
|
{
|
|
return JsonSerializer.Deserialize<T>(json);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to deserialize cached value for key: {Key}", key);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a cached value with TTL.
|
|
/// </summary>
|
|
public async Task<bool> SetStringAsync(string key, string value, TimeSpan? expiry = null)
|
|
{
|
|
if (!IsEnabled) return false;
|
|
|
|
try
|
|
{
|
|
return await SetStringInternalAsync(key, value, expiry);
|
|
}
|
|
catch (RedisTimeoutException ex)
|
|
{
|
|
return await RetrySetAfterReconnectAsync(
|
|
key,
|
|
value,
|
|
expiry,
|
|
ex,
|
|
"Redis SET timeout for key: {Key}. Reconnecting and retrying once.");
|
|
}
|
|
catch (RedisConnectionException ex)
|
|
{
|
|
return await RetrySetAfterReconnectAsync(
|
|
key,
|
|
value,
|
|
expiry,
|
|
ex,
|
|
"Redis SET connection error for key: {Key}. Reconnecting and retrying once.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Redis SET failed for key: {Key}", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async Task<bool> SetStringInternalAsync(string key, string value, TimeSpan? expiry)
|
|
{
|
|
var result = await _db!.StringSetAsync(key, value, expiry);
|
|
if (result)
|
|
{
|
|
_logger.LogDebug("Redis cache SET: {Key} (TTL: {Expiry})", key, expiry?.ToString() ?? "none");
|
|
}
|
|
else
|
|
{
|
|
_logger.LogWarning("Redis SET returned false for key: {Key}", key);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private async Task<bool> RetrySetAfterReconnectAsync(
|
|
string key,
|
|
string value,
|
|
TimeSpan? expiry,
|
|
Exception ex,
|
|
string warningMessage)
|
|
{
|
|
_logger.LogWarning(ex, warningMessage, key);
|
|
|
|
if (!TryReconnect())
|
|
{
|
|
_logger.LogError("Redis reconnect failed; cannot retry SET for key: {Key}", key);
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
return await SetStringInternalAsync(key, value, expiry);
|
|
}
|
|
catch (Exception retryEx)
|
|
{
|
|
_logger.LogError(retryEx, "Redis SET retry failed for key: {Key}", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private bool TryReconnect()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
if (!_settings.Enabled)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
_redis?.Dispose();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogDebug(ex, "Error disposing Redis connection during reconnect");
|
|
}
|
|
|
|
_redis = null;
|
|
_db = null;
|
|
InitializeConnection();
|
|
return _db != null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets a cached value by serializing it with TTL.
|
|
/// </summary>
|
|
public async Task<bool> SetAsync<T>(string key, T value, TimeSpan? expiry = null) where T : class
|
|
{
|
|
try
|
|
{
|
|
var json = JsonSerializer.Serialize(value);
|
|
return await SetStringAsync(key, json, expiry);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed to serialize value for key: {Key}", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes a cached value.
|
|
/// </summary>
|
|
public async Task<bool> DeleteAsync(string key)
|
|
{
|
|
if (!IsEnabled) return false;
|
|
|
|
try
|
|
{
|
|
return await _db!.KeyDeleteAsync(key);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Redis DELETE failed for key: {Key}", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a key exists.
|
|
/// </summary>
|
|
public async Task<bool> ExistsAsync(string key)
|
|
{
|
|
if (!IsEnabled) return false;
|
|
|
|
try
|
|
{
|
|
return await _db!.KeyExistsAsync(key);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Redis EXISTS failed for key: {Key}", key);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets all keys matching a pattern.
|
|
/// </summary>
|
|
public IEnumerable<string> GetKeysByPattern(string pattern)
|
|
{
|
|
if (!IsEnabled) return Array.Empty<string>();
|
|
|
|
try
|
|
{
|
|
var server = _redis!.GetServer(_redis.GetEndPoints().First());
|
|
return server.Keys(pattern: pattern).Select(k => (string)k!);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Redis GET KEYS BY PATTERN failed for pattern: {Pattern}", pattern);
|
|
return Array.Empty<string>();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Deletes all keys matching a pattern (e.g., "search:*").
|
|
/// WARNING: Use with caution as this scans all keys.
|
|
/// </summary>
|
|
public async Task<int> DeleteByPatternAsync(string pattern)
|
|
{
|
|
if (!IsEnabled) return 0;
|
|
|
|
try
|
|
{
|
|
var server = _redis!.GetServer(_redis.GetEndPoints().First());
|
|
var keys = server.Keys(pattern: pattern).ToArray();
|
|
|
|
if (keys.Length == 0)
|
|
{
|
|
_logger.LogDebug("No keys found matching pattern: {Pattern}", pattern);
|
|
return 0;
|
|
}
|
|
|
|
var deleted = await _db!.KeyDeleteAsync(keys);
|
|
_logger.LogDebug("Deleted {Count} Redis keys matching pattern: {Pattern}", deleted, pattern);
|
|
return (int)deleted;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Redis DELETE BY PATTERN failed for pattern: {Pattern}", pattern);
|
|
return 0;
|
|
}
|
|
}
|
|
}
|