a5f8943c72
- Aggiunta validazione percorsi file prima del salvataggio profili - Implementati metodi di lettura file CSV e Excel per schedulazioni - Supporto doppia modalità: caricamento browser (preview) e percorso manuale (schedulazione) - Gestione completa deletion sync anche per file CSV/Excel - Rilevamento automatico separatori CSV (virgola, punto e virgola, tab, pipe) - Supporto formati Excel legacy (.xls) e moderni (.xlsx) - Abilitati profili file nella UI di schedulazione - Logging dettagliato per troubleshooting - Documentazione completa in CSV_SCHEDULING_IMPLEMENTATION.md - Aggiornati README.md e copilot-instructions.md con nuove feature - Rimosso testo 'TEST' dalla pagina di login
328 lines
13 KiB
C#
328 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,
|
|
schedule.EnableDeletionSync);
|
|
|
|
// 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);
|
|
}
|
|
} |