mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-10 07:58:39 -05:00
Add startup cache warming service
- Proactively loads all file caches into Redis on container startup - Warms genre cache (30-day expiration) - Warms playlist items cache (24-hour expiration) - Logs warming progress and duration - Ensures fast access immediately after restart - Cleans up expired genre cache files automatically - All 225 tests passing
This commit is contained in:
@@ -482,6 +482,9 @@ builder.Services.AddHostedService<StartupValidationOrchestrator>();
|
||||
// Register cache cleanup service (only runs when StorageMode is Cache)
|
||||
builder.Services.AddHostedService<CacheCleanupService>();
|
||||
|
||||
// Register cache warming service (loads file caches into Redis on startup)
|
||||
builder.Services.AddHostedService<CacheWarmingService>();
|
||||
|
||||
// Register Spotify API client, lyrics service, and settings for direct API access
|
||||
// Configure from environment variables with SPOTIFY_API_ prefix
|
||||
builder.Services.Configure<allstarr.Models.Settings.SpotifyApiSettings>(options =>
|
||||
|
||||
172
allstarr/Services/Common/CacheWarmingService.cs
Normal file
172
allstarr/Services/Common/CacheWarmingService.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace allstarr.Services.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Background service that warms up Redis cache from file system on startup.
|
||||
/// Ensures fast access to cached data after container restarts.
|
||||
/// </summary>
|
||||
public class CacheWarmingService : IHostedService
|
||||
{
|
||||
private readonly RedisCacheService _cache;
|
||||
private readonly ILogger<CacheWarmingService> _logger;
|
||||
private const string GenreCacheDirectory = "/app/cache/genres";
|
||||
private const string PlaylistCacheDirectory = "/app/cache/spotify";
|
||||
|
||||
public CacheWarmingService(
|
||||
RedisCacheService cache,
|
||||
ILogger<CacheWarmingService> logger)
|
||||
{
|
||||
_cache = cache;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_logger.LogInformation("🔥 Starting cache warming from file system...");
|
||||
|
||||
var startTime = DateTime.UtcNow;
|
||||
var genresWarmed = 0;
|
||||
var playlistsWarmed = 0;
|
||||
|
||||
try
|
||||
{
|
||||
// Warm genre cache
|
||||
genresWarmed = await WarmGenreCacheAsync(cancellationToken);
|
||||
|
||||
// Warm playlist cache
|
||||
playlistsWarmed = await WarmPlaylistCacheAsync(cancellationToken);
|
||||
|
||||
var duration = DateTime.UtcNow - startTime;
|
||||
_logger.LogInformation(
|
||||
"✅ Cache warming complete in {Duration:F1}s: {Genres} genres, {Playlists} playlists",
|
||||
duration.TotalSeconds, genresWarmed, playlistsWarmed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to warm cache from file system");
|
||||
}
|
||||
}
|
||||
|
||||
public Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warms genre cache from file system.
|
||||
/// </summary>
|
||||
private async Task<int> WarmGenreCacheAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Directory.Exists(GenreCacheDirectory))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var files = Directory.GetFiles(GenreCacheDirectory, "*.json");
|
||||
var warmedCount = 0;
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
try
|
||||
{
|
||||
// Check if cache is expired (30 days)
|
||||
var fileInfo = new FileInfo(file);
|
||||
if (DateTime.UtcNow - fileInfo.LastWriteTimeUtc > TimeSpan.FromDays(30))
|
||||
{
|
||||
File.Delete(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(file, cancellationToken);
|
||||
var cacheEntry = JsonSerializer.Deserialize<GenreCacheEntry>(json);
|
||||
|
||||
if (cacheEntry != null && !string.IsNullOrEmpty(cacheEntry.CacheKey))
|
||||
{
|
||||
var redisKey = $"genre:{cacheEntry.CacheKey}";
|
||||
await _cache.SetAsync(redisKey, cacheEntry.Genre, TimeSpan.FromDays(30));
|
||||
warmedCount++;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to warm genre cache from file: {File}", file);
|
||||
}
|
||||
}
|
||||
|
||||
if (warmedCount > 0)
|
||||
{
|
||||
_logger.LogInformation("🔥 Warmed {Count} genre entries from file cache", warmedCount);
|
||||
}
|
||||
|
||||
return warmedCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Warms playlist cache from file system.
|
||||
/// </summary>
|
||||
private async Task<int> WarmPlaylistCacheAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (!Directory.Exists(PlaylistCacheDirectory))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var files = Directory.GetFiles(PlaylistCacheDirectory, "*_items.json");
|
||||
var warmedCount = 0;
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
try
|
||||
{
|
||||
// Check if cache is expired (24 hours)
|
||||
var fileInfo = new FileInfo(file);
|
||||
if (DateTime.UtcNow - fileInfo.LastWriteTimeUtc > TimeSpan.FromHours(24))
|
||||
{
|
||||
continue; // Don't delete, let the normal flow handle it
|
||||
}
|
||||
|
||||
var json = await File.ReadAllTextAsync(file, cancellationToken);
|
||||
var items = JsonSerializer.Deserialize<List<Dictionary<string, object?>>>(json);
|
||||
|
||||
if (items != null && items.Count > 0)
|
||||
{
|
||||
// Extract playlist name from filename
|
||||
var fileName = Path.GetFileNameWithoutExtension(file);
|
||||
var playlistName = fileName.Replace("_items", "");
|
||||
|
||||
var redisKey = $"spotify:playlist:items:{playlistName}";
|
||||
await _cache.SetAsync(redisKey, items, TimeSpan.FromHours(24));
|
||||
warmedCount++;
|
||||
|
||||
_logger.LogDebug("🔥 Warmed playlist cache for {Playlist} ({Count} items)",
|
||||
playlistName, items.Count);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to warm playlist cache from file: {File}", file);
|
||||
}
|
||||
}
|
||||
|
||||
if (warmedCount > 0)
|
||||
{
|
||||
_logger.LogInformation("🔥 Warmed {Count} playlist caches from file system", warmedCount);
|
||||
}
|
||||
|
||||
return warmedCount;
|
||||
}
|
||||
|
||||
private class GenreCacheEntry
|
||||
{
|
||||
public string CacheKey { get; set; } = "";
|
||||
public string Genre { get; set; } = "";
|
||||
public DateTime CachedAt { get; set; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user