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:
2025-10-02 01:12:39 +02:00
parent b76a6760fb
commit d042863a56
71 changed files with 17860 additions and 144 deletions
@@ -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);
}
}