Files
allstarr/allstarr/Services/Common/RedisCacheService.cs
Josh Patra 2b09484c0b Release v1.0.0 - Production Ready
Major Features:
- Spotify playlist injection with missing tracks search
- Transparent proxy authentication system
- WebSocket session management for external tracks
- Manual track mapping and favorites system
- Lyrics support (Spotify + LRCLib) with prefetching
- Admin dashboard with analytics and configuration
- Performance optimizations with health checks and endpoint racing
- Comprehensive caching and memory management

Performance Improvements:
- Quick health checks (3s timeout) before trying endpoints
- Health check results cached for 30 seconds
- 5 minute timeout for large artist responses
- Background Odesli conversion after streaming starts
- Parallel lyrics prefetching
- Endpoint benchmarking and racing
- 16 SquidWTF endpoints with load balancing

Reliability:
- Automatic endpoint fallback and failover
- Token expiration handling
- Concurrent request optimization
- Memory leak fixes
- Proper session cleanup

User Experience:
- Web UI for configuration and playlist management
- Real-time progress tracking
- API analytics dashboard
- Manual track mapping interface
- Playlist statistics and health monitoring
2026-02-08 00:43:47 -05:00

203 lines
5.4 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.LogWarning(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.LogWarning(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.LogWarning(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
{
var result = await _db!.StringSetAsync(key, value, expiry);
if (result)
{
_logger.LogDebug("Redis cache SET: {Key} (TTL: {Expiry})", key, expiry?.ToString() ?? "none");
}
return result;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Redis SET failed for key: {Key}", key);
return false;
}
}
/// <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.LogWarning(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.LogWarning(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.LogWarning(ex, "Redis EXISTS failed for key: {Key}", key);
return false;
}
}
/// <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.LogInformation("Deleted {Count} Redis keys matching pattern: {Pattern}", deleted, pattern);
return (int)deleted;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Redis DELETE BY PATTERN failed for pattern: {Pattern}", pattern);
return 0;
}
}
}