using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using StellaOps.Scanner.Cache.Abstractions; namespace StellaOps.Scanner.Cache.Maintenance; public sealed class ScannerCacheMaintenanceService : BackgroundService { private readonly ILayerCacheStore _layerCache; private readonly IFileContentAddressableStore _fileCas; private readonly IOptions _options; private readonly ILogger _logger; private readonly TimeProvider _timeProvider; public ScannerCacheMaintenanceService( ILayerCacheStore layerCache, IFileContentAddressableStore fileCas, IOptions options, ILogger logger, TimeProvider? timeProvider = null) { _layerCache = layerCache ?? throw new ArgumentNullException(nameof(layerCache)); _fileCas = fileCas ?? throw new ArgumentNullException(nameof(fileCas)); _options = options ?? throw new ArgumentNullException(nameof(options)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _timeProvider = timeProvider ?? TimeProvider.System; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { var settings = _options.Value; if (!settings.Enabled) { _logger.LogInformation("Scanner cache disabled; maintenance loop will not start."); return; } if (!settings.EnableAutoEviction) { _logger.LogInformation("Scanner cache automatic eviction disabled by configuration."); return; } var interval = settings.MaintenanceInterval > TimeSpan.Zero ? settings.MaintenanceInterval : TimeSpan.FromMinutes(15); _logger.LogInformation("Scanner cache maintenance loop started with interval {Interval}", interval); await RunMaintenanceAsync(stoppingToken).ConfigureAwait(false); using var timer = new PeriodicTimer(interval, _timeProvider); while (await timer.WaitForNextTickAsync(stoppingToken).ConfigureAwait(false)) { await RunMaintenanceAsync(stoppingToken).ConfigureAwait(false); } } private async Task RunMaintenanceAsync(CancellationToken cancellationToken) { try { var layerExpired = await _layerCache.EvictExpiredAsync(cancellationToken).ConfigureAwait(false); var layerCompacted = await _layerCache.CompactAsync(cancellationToken).ConfigureAwait(false); var casExpired = await _fileCas.EvictExpiredAsync(cancellationToken).ConfigureAwait(false); var casCompacted = await _fileCas.CompactAsync(cancellationToken).ConfigureAwait(false); _logger.LogDebug( "Scanner cache maintenance tick complete (layers expired={LayersExpired}, layers compacted={LayersCompacted}, cas expired={CasExpired}, cas compacted={CasCompacted})", layerExpired, layerCompacted, casExpired, casCompacted); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { // Shutting down; ignore. } catch (Exception ex) { _logger.LogError(ex, "Scanner cache maintenance tick failed"); } } }