using System.Text.Json; using Microsoft.EntityFrameworkCore; using StackExchange.Redis; public class ThresholdCacheLoader : IHostedService { private readonly IServiceProvider _services; private readonly IConnectionMultiplexer _redis; private readonly ILogger _logger; public ThresholdCacheLoader( IServiceProvider services, IConnectionMultiplexer redis, ILogger logger) { _services = services; _redis = redis; _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { using var scope = _services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var thresholds = await db.AlertThresholds.ToListAsync(cancellationToken); const int maxAttempts = 3; int[] backoffMs = [2000, 4000, 8000]; for (var attempt = 0; attempt < maxAttempts; attempt++) { try { var cache = _redis.GetDatabase(); var batch = cache.CreateBatch(); foreach (var t in thresholds) { var json = JsonSerializer.Serialize(new { t.ObservationCode, t.CriticalLow, t.WarningLow, t.WarningHigh, t.CriticalHigh }); _ = batch.StringSetAsync($"threshold:{t.ObservationCode}", json); } batch.Execute(); _logger.LogInformation("Loaded {Count} alert thresholds into Redis cache", thresholds.Count); return; } catch (RedisException ex) { _logger.LogWarning(ex, "Redis unavailable during threshold cache load — attempt {Attempt}/{Max}", attempt + 1, maxAttempts); if (attempt < maxAttempts - 1) await Task.Delay(backoffMs[attempt], cancellationToken); } } _logger.LogError( "Failed to load thresholds into Redis after {Max} attempts — " + "application will start without cache; observation ingest falls back to PostgreSQL", maxAttempts); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; }