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
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
using CredentialManager.Services;
|
||||
using Data_Coupler.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Data_Coupler.BackgroundServices;
|
||||
|
||||
/// <summary>
|
||||
/// Servizio di background per l'esecuzione automatica delle schedulazioni
|
||||
/// </summary>
|
||||
public class ScheduleExecutorService : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<ScheduleExecutorService> _logger;
|
||||
private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1); // Controlla ogni minuto
|
||||
|
||||
public ScheduleExecutorService(IServiceProvider serviceProvider, ILogger<ScheduleExecutorService> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("ScheduleExecutorService avviato. Controllo schedulazioni ogni {Interval} minuti.",
|
||||
_checkInterval.TotalMinutes);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await CheckAndExecuteSchedules();
|
||||
await Task.Delay(_checkInterval, stoppingToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("ScheduleExecutorService arrestato.");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore nel controllo schedulazioni");
|
||||
await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); // Attendi 5 minuti prima di riprovare
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CheckAndExecuteSchedules()
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var scheduleService = scope.ServiceProvider.GetRequiredService<IProfileScheduleService>();
|
||||
var dataTransferService = scope.ServiceProvider.GetRequiredService<IDataTransferService>();
|
||||
|
||||
try
|
||||
{
|
||||
var pendingSchedules = await scheduleService.GetPendingExecutionsAsync();
|
||||
|
||||
if (pendingSchedules.Any())
|
||||
{
|
||||
_logger.LogInformation("Trovate {Count} schedulazioni in attesa di esecuzione", pendingSchedules.Count);
|
||||
}
|
||||
|
||||
foreach (var schedule in pendingSchedules)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ExecuteSchedule(schedule, scheduleService, dataTransferService);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore nell'esecuzione della schedulazione {ScheduleName} (ID: {ScheduleId})",
|
||||
schedule.Name, schedule.Id);
|
||||
|
||||
// Aggiorna lo status dell'esecuzione fallita
|
||||
await scheduleService.UpdateExecutionStatusAsync(schedule.Id, "failed",
|
||||
$"Errore nell'esecuzione: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore nel recupero delle schedulazioni in attesa");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteSchedule(CredentialManager.Models.ProfileSchedule schedule,
|
||||
IProfileScheduleService scheduleService,
|
||||
IDataTransferService dataTransferService)
|
||||
{
|
||||
_logger.LogInformation("Esecuzione schedulazione {ScheduleName} (ID: {ScheduleId}) iniziata",
|
||||
schedule.Name, schedule.Id);
|
||||
|
||||
// Aggiorna lo status a "running"
|
||||
await scheduleService.UpdateExecutionStatusAsync(schedule.Id, "running",
|
||||
"Esecuzione automatica avviata");
|
||||
|
||||
try
|
||||
{
|
||||
if (schedule.Profile == null)
|
||||
{
|
||||
throw new InvalidOperationException("Profilo associato alla schedulazione non trovato");
|
||||
}
|
||||
|
||||
// Esegui il trasferimento dati
|
||||
var result = await dataTransferService.ExecuteProfileAsync(schedule.Profile);
|
||||
|
||||
// Aggiorna lo status con il risultato
|
||||
var status = result.IsSuccess ? "success" : "failed";
|
||||
var message = result.IsSuccess
|
||||
? $"Esecuzione completata con successo in {result.Duration.TotalSeconds:F1} secondi. " +
|
||||
$"{result.RecordsProcessed} record elaborati."
|
||||
: $"Esecuzione fallita: {result.ErrorMessage}";
|
||||
|
||||
await scheduleService.UpdateExecutionStatusAsync(schedule.Id, status, message, result.RecordsProcessed);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
_logger.LogInformation("Schedulazione {ScheduleName} completata con successo. " +
|
||||
"Record processati: {RecordsProcessed}, Durata: {Duration}s",
|
||||
schedule.Name, result.RecordsProcessed, result.Duration.TotalSeconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Schedulazione {ScheduleName} fallita: {ErrorMessage}",
|
||||
schedule.Name, result.ErrorMessage);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore durante l'esecuzione della schedulazione {ScheduleName}", schedule.Name);
|
||||
|
||||
await scheduleService.UpdateExecutionStatusAsync(schedule.Id, "failed",
|
||||
$"Errore nell'esecuzione: {ex.Message}");
|
||||
|
||||
throw; // Re-throw per permettere la gestione a livello superiore
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("ScheduleExecutorService in fase di arresto...");
|
||||
await base.StopAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user