Files
Data-Coupler/Data_Coupler/BackgroundServices/ScheduledJobService.cs
T
Alessio d042863a56 feat: Implementazione completa sistema schedulazione con intervalli personalizzati
- Aggiunto supporto schedulazione con intervalli flessibili (secondi/minuti/ore/giorni/settimane/mesi)
- Esteso modello ProfileSchedule con campi IntervalValue e IntervalUnit
- Ottimizzato ScheduledJobService per controlli ogni 30s con esecuzione parallela
- Implementata interfaccia UI completa con anteprima real-time in italiano
- Aggiunta migrazione database AddIntervalSchedulingFields
- Implementati metodi calcolo NextExecutionTime per intervalli
- Aggiunta gestione tracking anti-duplicati e cleanup automatico
- Creata documentazione completa (6 file, 2500+ righe)

Modifiche tecniche:
- ProfileSchedule.cs: Nuovi campi e metodi CalculateNextInterval/GetScheduleDescription
- ScheduledJobService.cs: Ridotto check interval a 30s, aggiunto parallel processing
- ProfileScheduleService.cs: Supporto calcolo intervalli in UpdateNextExecutionTimeAsync
- Scheduling.razor: Aggiunta sezione UI per configurazione intervalli
- Scheduling.razor.cs: Implementato GetIntervalPreview() e gestione stato campi
2025-10-02 01:12:39 +02:00

327 lines
13 KiB
C#

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;
/// <summary>
/// Background service per l'esecuzione automatica delle schedulazioni
/// </summary>
public class ScheduledJobService : BackgroundService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly ILogger<ScheduledJobService> _logger;
private TimeSpan _checkInterval = TimeSpan.FromSeconds(30); // Controlla ogni 30 secondi per supportare intervalli brevi
private readonly Dictionary<int, DateTime> _runningSchedules = new(); // Tiene traccia delle schedulazioni in esecuzione
public ScheduledJobService(
IServiceScopeFactory serviceScopeFactory,
ILogger<ScheduledJobService> 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<IProfileScheduleService>();
var dataTransferService = scope.ServiceProvider.GetRequiredService<IDataTransferService>();
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<IProfileScheduleService>();
var dataTransferService = scope.ServiceProvider.GetRequiredService<IDataTransferService>();
_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<IProfileScheduleService>();
// 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);
}
}