using CredentialManager.Services; using CredentialManager.Models; using Data_Coupler.Services; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; namespace Data_Coupler.BackgroundServices; /// /// Background service per l'esecuzione automatica delle schedulazioni /// public class ScheduledJobService : BackgroundService { private readonly IServiceScopeFactory _serviceScopeFactory; private readonly ILogger _logger; private TimeSpan _checkInterval = TimeSpan.FromSeconds(30); // Controlla ogni 30 secondi per supportare intervalli brevi private readonly Dictionary _runningSchedules = new(); // Tiene traccia delle schedulazioni in esecuzione public ScheduledJobService( IServiceScopeFactory serviceScopeFactory, ILogger logger) { _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("ScheduledJobService avviato"); // Attendi alcuni secondi prima di iniziare per permettere la completa inizializzazione dell'app try { await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken); } catch (OperationCanceledException) { _logger.LogInformation("ScheduledJobService cancellato durante l'inizializzazione"); return; } while (!stoppingToken.IsCancellationRequested) { try { await CheckAndExecutePendingSchedules(stoppingToken); } catch (OperationCanceledException) { _logger.LogInformation("ScheduledJobService cancellato"); break; } catch (Exception ex) { _logger.LogError(ex, "Errore durante il controllo delle schedulazioni"); // In caso di errore grave, attendi di più prima del prossimo tentativo try { await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); } catch (OperationCanceledException) { break; } continue; } try { await Task.Delay(_checkInterval, stoppingToken); } catch (OperationCanceledException) { break; } } _logger.LogInformation("ScheduledJobService arrestato"); } private async Task CheckAndExecutePendingSchedules(CancellationToken cancellationToken) { using var scope = _serviceScopeFactory.CreateScope(); var scheduleService = scope.ServiceProvider.GetRequiredService(); var dataTransferService = scope.ServiceProvider.GetRequiredService(); try { // Ottieni le schedulazioni che devono essere eseguite var pendingSchedules = await scheduleService.GetPendingExecutionsAsync(); if (!pendingSchedules.Any()) { _logger.LogTrace("Nessuna schedulazione in sospeso trovata"); return; } _logger.LogInformation("Trovate {Count} schedulazioni da eseguire", pendingSchedules.Count); // Pulisci le schedulazioni completate dal tracking CleanupRunningSchedules(); foreach (var schedule in pendingSchedules) { if (cancellationToken.IsCancellationRequested) break; // Verifica se la schedulazione è già in esecuzione if (IsScheduleRunning(schedule.Id)) { _logger.LogDebug("Schedulazione {ScheduleId} già in esecuzione, salto", schedule.Id); continue; } // Esegui la schedulazione in modo asincrono senza attendere // Questo permette di eseguire più schedulazioni in parallelo _ = Task.Run(async () => { try { await ExecuteScheduleAsync(schedule, cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "Errore nell'esecuzione asincrona della schedulazione {ScheduleId}", schedule.Id); } }, cancellationToken); } } catch (Exception ex) { _logger.LogError(ex, "Errore durante il controllo delle schedulazioni pendenti"); } } private bool IsScheduleRunning(int scheduleId) { lock (_runningSchedules) { return _runningSchedules.ContainsKey(scheduleId); } } private void MarkScheduleAsRunning(int scheduleId) { lock (_runningSchedules) { _runningSchedules[scheduleId] = DateTime.Now; } } private void MarkScheduleAsCompleted(int scheduleId) { lock (_runningSchedules) { _runningSchedules.Remove(scheduleId); } } private void CleanupRunningSchedules() { lock (_runningSchedules) { var timeout = DateTime.Now.AddHours(-1); // Se una schedulazione è "running" da più di 1 ora, considerala bloccata var staleSchedules = _runningSchedules.Where(x => x.Value < timeout).Select(x => x.Key).ToList(); foreach (var scheduleId in staleSchedules) { _logger.LogWarning("Rimozione schedulazione {ScheduleId} da tracking (timeout esecuzione)", scheduleId); _runningSchedules.Remove(scheduleId); } } } private async Task ExecuteScheduleAsync( ProfileSchedule schedule, CancellationToken cancellationToken) { ScheduleExecutionHistory? executionHistory = null; // Marca la schedulazione come in esecuzione MarkScheduleAsRunning(schedule.Id); try { // Crea un nuovo scope per questa esecuzione using var scope = _serviceScopeFactory.CreateScope(); var scheduleService = scope.ServiceProvider.GetRequiredService(); var dataTransferService = scope.ServiceProvider.GetRequiredService(); _logger.LogInformation("Esecuzione automatica schedulazione {ScheduleId} - {ScheduleName}", schedule.Id, schedule.Name); // Controlla se la schedulazione è ancora valida per l'esecuzione if (!IsScheduleReadyForExecution(schedule)) { _logger.LogDebug("Schedulazione {ScheduleId} non più pronta per l'esecuzione", schedule.Id); return; } // Crea record nello storico executionHistory = new ScheduleExecutionHistory { ScheduleId = schedule.Id, ProfileId = schedule.ProfileId, ProfileName = schedule.Profile?.Name ?? "Unknown", StartTime = DateTime.Now, Status = "running", TriggerType = "automatic", TriggeredBy = "System", SourceType = schedule.Profile?.SourceType, DestinationType = schedule.Profile?.DestinationType, SourceInfo = schedule.SourceDatabaseOverride != null ? $"Database Override: {schedule.SourceDatabaseOverride}" : null, DestinationInfo = schedule.DestinationDatabaseOverride != null ? $"Database Override: {schedule.DestinationDatabaseOverride}" : null, Message = "Esecuzione automatica avviata" }; executionHistory = await scheduleService.CreateExecutionHistoryAsync(executionHistory); // Aggiorna lo status della schedulazione await scheduleService.UpdateExecutionStatusAsync(schedule.Id, "running", "Esecuzione automatica avviata"); // Esegui il trasferimento dati if (schedule.Profile == null) { throw new InvalidOperationException($"Profilo non trovato per la schedulazione {schedule.Id}"); } var result = await dataTransferService.ExecuteProfileAsync( schedule.Profile, schedule.SourceDatabaseOverride, schedule.DestinationDatabaseOverride); // Aggiorna lo storico con il risultato executionHistory.EndTime = DateTime.Now; executionHistory.Status = result.IsSuccess ? "success" : "failed"; executionHistory.RecordsProcessed = result.RecordsProcessed; executionHistory.Message = result.IsSuccess ? $"Esecuzione automatica completata con successo. {result.RecordsProcessed} record elaborati in {result.Duration.TotalSeconds:F2} secondi." : $"Esecuzione automatica fallita: {result.ErrorMessage}"; if (!result.IsSuccess) { executionHistory.ErrorDetails = string.Join(Environment.NewLine, result.ErrorDetails); } // Aggiungi informazioni aggiuntive se disponibili if (result.AdditionalInfo.Any()) { executionHistory.AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(result.AdditionalInfo); } await scheduleService.UpdateExecutionHistoryAsync(executionHistory); // Aggiorna lo status della schedulazione var status = result.IsSuccess ? "success" : "failed"; var message = result.IsSuccess ? $"Esecuzione automatica completata con successo. {result.RecordsProcessed} record elaborati." : $"Esecuzione automatica fallita: {result.ErrorMessage}"; await scheduleService.UpdateExecutionStatusAsync(schedule.Id, status, message, result.RecordsProcessed); // Aggiorna la prossima data di esecuzione await scheduleService.UpdateNextExecutionTimeAsync(schedule.Id); _logger.LogInformation("Schedulazione {ScheduleId} eseguita con successo: {RecordsProcessed} record, durata {Duration}s", schedule.Id, result.RecordsProcessed, result.Duration.TotalSeconds); } catch (Exception ex) { _logger.LogError(ex, "Errore durante l'esecuzione automatica della schedulazione {ScheduleId}", schedule.Id); // Crea un nuovo scope per gestire l'errore try { using var errorScope = _serviceScopeFactory.CreateScope(); var scheduleService = errorScope.ServiceProvider.GetRequiredService(); // Aggiorna lo storico in caso di eccezione if (executionHistory != null) { executionHistory.EndTime = DateTime.Now; executionHistory.Status = "failed"; executionHistory.Message = $"Errore durante l'esecuzione automatica: {ex.Message}"; executionHistory.ErrorDetails = ex.ToString(); await scheduleService.UpdateExecutionHistoryAsync(executionHistory); } await scheduleService.UpdateExecutionStatusAsync(schedule.Id, "failed", $"Errore durante l'esecuzione automatica: {ex.Message}"); } catch (Exception innerEx) { _logger.LogError(innerEx, "Errore durante l'aggiornamento dello stato di errore per la schedulazione {ScheduleId}", schedule.Id); } } finally { // Rimuovi la schedulazione dal tracking MarkScheduleAsCompleted(schedule.Id); } } private bool IsScheduleReadyForExecution(ProfileSchedule schedule) { // Verifica che la schedulazione sia attiva e abilitata if (!schedule.IsActive || !schedule.IsEnabled) return false; // Verifica che ci sia una prossima esecuzione programmata if (!schedule.NextExecutionTime.HasValue) return false; // Per schedulazioni a intervalli, usa una tolleranza più stretta var tolerance = schedule.ScheduleType == "interval" ? TimeSpan.FromSeconds(30) // 30 secondi per intervalli : TimeSpan.FromMinutes(1); // 1 minuto per altre schedulazioni var now = DateTime.Now; var nextExecution = schedule.NextExecutionTime.Value; return nextExecution <= now.Add(tolerance); } }