namespace JobTrackerApi.Services { public sealed class DatabaseBackupHostedService : BackgroundService { private readonly IDatabaseBackupRunner _runner; private readonly ILogger _logger; private readonly IConfiguration _cfg; private readonly IStartupReadiness _startupReadiness; public DatabaseBackupHostedService( IDatabaseBackupRunner runner, ILogger logger, IConfiguration cfg, IStartupReadiness startupReadiness) { _runner = runner; _logger = logger; _cfg = cfg; _startupReadiness = startupReadiness; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await _startupReadiness.WaitUntilReadyAsync(stoppingToken); if (!_cfg.GetValue("Backups:Enabled", true)) { _logger.LogInformation("Automated database backups disabled (Backups:Enabled=false)."); return; } if (!_runner.IsSupported) { _logger.LogWarning("Automated database backups are unavailable for the configured provider. Configure external backups for MySQL/MariaDB."); return; } var hour = _cfg.GetValue("Backups:HourLocal", 3); if (hour < 0 || hour > 23) hour = 3; // Catch-up: guarantee at least one recent backup exists even if the // process never stays up long enough to reach the scheduled hour. var latest = _runner.GetLatestBackupUtc(); if (latest is null || latest < DateTime.UtcNow.AddHours(-24)) { await TryBackupAsync(stoppingToken); } while (!stoppingToken.IsCancellationRequested) { var now = DateTime.Now; var next = new DateTime(now.Year, now.Month, now.Day, hour, 0, 0); if (next <= now) next = next.AddDays(1); _logger.LogInformation("Next database backup scheduled at {Next}.", next); try { await Task.Delay(next - now, stoppingToken); } catch (TaskCanceledException) { break; } await TryBackupAsync(stoppingToken); } } private async Task TryBackupAsync(CancellationToken ct) { try { await _runner.RunOnceAsync(ct); } catch (OperationCanceledException) { } catch (Exception ex) { _logger.LogError(ex, "Database backup failed."); } } } }