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,388 @@
|
||||
using CredentialManager.Models;
|
||||
using CredentialManager.Services;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Data_Coupler.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Servizio background che gestisce l'esecuzione automatica dei profili schedulati
|
||||
/// Controlla ogni minuto se ci sono profili da eseguire secondo la schedulazione impostata
|
||||
/// </summary>
|
||||
public class ScheduledExecutionBackgroundService : BackgroundService
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<ScheduledExecutionBackgroundService> _logger;
|
||||
private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1); // Controlla ogni minuto
|
||||
|
||||
public ScheduledExecutionBackgroundService(
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<ScheduledExecutionBackgroundService> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
_logger.LogInformation("Servizio di schedulazione automatica avviato. Controllo ogni {CheckInterval} minuti.",
|
||||
_checkInterval.TotalMinutes);
|
||||
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await CheckAndExecuteScheduledProfiles();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore durante il controllo delle schedulazioni");
|
||||
}
|
||||
|
||||
// Attendi il prossimo controllo
|
||||
await Task.Delay(_checkInterval, stoppingToken);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Servizio di schedulazione automatica arrestato.");
|
||||
}
|
||||
|
||||
private async Task CheckAndExecuteScheduledProfiles()
|
||||
{
|
||||
using var scope = _serviceProvider.CreateScope();
|
||||
var scheduleService = scope.ServiceProvider.GetRequiredService<IProfileScheduleService>();
|
||||
var executionService = scope.ServiceProvider.GetRequiredService<IScheduledProfileExecutionService>();
|
||||
var profileService = scope.ServiceProvider.GetRequiredService<IDataCouplerProfileService>();
|
||||
|
||||
try
|
||||
{
|
||||
// Ottieni tutte le schedulazioni attive
|
||||
var activeSchedules = await scheduleService.GetActiveSchedulesAsync();
|
||||
var currentTime = DateTime.Now; // Usa l'ora locale per il confronto con le schedulazioni
|
||||
|
||||
_logger.LogDebug("Controllo schedulazioni: {ScheduleCount} schedulazioni attive alle {CurrentTime}",
|
||||
activeSchedules.Count, DateTimeHelper.FormatDateTime24H(currentTime));
|
||||
|
||||
foreach (var schedule in activeSchedules)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (ShouldExecuteSchedule(schedule, currentTime))
|
||||
{
|
||||
_logger.LogInformation("Esecuzione schedulata per profilo: {ProfileName} (Schedule: {ScheduleName})",
|
||||
schedule.Profile?.Name ?? "N/A", schedule.Name);
|
||||
|
||||
// Esegui il profilo
|
||||
var result = await executionService.ExecuteProfileAsync(schedule.ProfileId);
|
||||
|
||||
// Aggiorna la schedulazione
|
||||
await UpdateScheduleAfterExecution(scheduleService, schedule, currentTime, result.Success);
|
||||
|
||||
// Log del risultato
|
||||
if (result.Success)
|
||||
{
|
||||
_logger.LogInformation("Esecuzione schedulata completata con successo per {ProfileName}. " +
|
||||
"Record processati: {RecordsProcessed}, Durata: {Duration}ms",
|
||||
schedule.Profile?.Name ?? "N/A", result.RecordsProcessed, result.Duration.TotalMilliseconds);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogError("Esecuzione schedulata fallita per {ProfileName}: {ErrorMessage}",
|
||||
schedule.Profile?.Name ?? "N/A", result.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore durante l'esecuzione della schedulazione {ScheduleName} per il profilo {ProfileName}",
|
||||
schedule.Name, schedule.Profile?.Name ?? "N/A");
|
||||
|
||||
// Aggiorna la schedulazione anche in caso di errore
|
||||
try
|
||||
{
|
||||
await UpdateScheduleAfterExecution(scheduleService, schedule, currentTime, false);
|
||||
}
|
||||
catch (Exception updateEx)
|
||||
{
|
||||
_logger.LogError(updateEx, "Errore durante l'aggiornamento della schedulazione {ScheduleName} dopo l'errore",
|
||||
schedule.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore durante il recupero delle schedulazioni attive");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determina se una schedulazione deve essere eseguita in base all'orario corrente
|
||||
/// </summary>
|
||||
private bool ShouldExecuteSchedule(ProfileSchedule schedule, DateTime currentTime)
|
||||
{
|
||||
if (!schedule.IsEnabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Controllo per evitare esecuzioni multiple nella stessa finestra temporale (tolleranza di 1 minuto)
|
||||
if (schedule.LastExecutionTime.HasValue)
|
||||
{
|
||||
var timeSinceLastExecution = currentTime - schedule.LastExecutionTime.Value;
|
||||
if (timeSinceLastExecution.TotalMinutes < 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return schedule.ScheduleType.ToLower() switch
|
||||
{
|
||||
"once" => ShouldExecuteOnceSchedule(schedule, currentTime),
|
||||
"daily" => ShouldExecuteDailySchedule(schedule, currentTime),
|
||||
"weekly" => ShouldExecuteWeeklySchedule(schedule, currentTime),
|
||||
"monthly" => ShouldExecuteMonthlySchedule(schedule, currentTime),
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private bool ShouldExecuteOnceSchedule(ProfileSchedule schedule, DateTime currentTime)
|
||||
{
|
||||
if (!schedule.ScheduledDateTime.HasValue)
|
||||
{
|
||||
_logger.LogWarning("Schedulazione 'once' senza data/ora specificata: {ScheduleName}", schedule.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
var scheduledTime = schedule.ScheduledDateTime.Value;
|
||||
|
||||
// Esegui se l'orario programmato è passato e non è mai stato eseguito
|
||||
return currentTime >= scheduledTime && !schedule.LastExecutionTime.HasValue;
|
||||
}
|
||||
|
||||
private bool ShouldExecuteDailySchedule(ProfileSchedule schedule, DateTime currentTime)
|
||||
{
|
||||
if (string.IsNullOrEmpty(schedule.DailyTime))
|
||||
{
|
||||
_logger.LogWarning("Schedulazione 'daily' senza orario specificato: {ScheduleName}", schedule.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
|
||||
{
|
||||
_logger.LogWarning("Formato orario non valido per schedulazione daily {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)",
|
||||
schedule.Name, schedule.DailyTime);
|
||||
return false;
|
||||
}
|
||||
|
||||
var currentTimeOfDay = currentTime.TimeOfDay;
|
||||
var scheduledTimeOfDay = scheduledTime;
|
||||
|
||||
// Verifica se siamo nell'orario giusto (con tolleranza di ±1 minuto)
|
||||
var timeDifference = Math.Abs((currentTimeOfDay - scheduledTimeOfDay).TotalMinutes);
|
||||
|
||||
if (timeDifference > 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Se non è mai stato eseguito, esegui
|
||||
if (!schedule.LastExecutionTime.HasValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Se è stato eseguito, verifica che sia passato almeno un giorno
|
||||
var lastExecutionDate = schedule.LastExecutionTime.Value.Date;
|
||||
var currentDate = currentTime.Date;
|
||||
|
||||
return currentDate > lastExecutionDate;
|
||||
}
|
||||
|
||||
private bool ShouldExecuteWeeklySchedule(ProfileSchedule schedule, DateTime currentTime)
|
||||
{
|
||||
if (!schedule.DayOfWeek.HasValue || string.IsNullOrEmpty(schedule.DailyTime))
|
||||
{
|
||||
_logger.LogWarning("Schedulazione 'weekly' senza giorno della settimana o orario specificato: {ScheduleName}",
|
||||
schedule.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
|
||||
{
|
||||
_logger.LogWarning("Formato orario non valido per schedulazione weekly {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)",
|
||||
schedule.Name, schedule.DailyTime);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verifica il giorno della settimana (0=Domenica, 1=Lunedì, etc.)
|
||||
var currentDayOfWeek = (int)currentTime.DayOfWeek;
|
||||
if (currentDayOfWeek != schedule.DayOfWeek.Value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verifica l'orario (con tolleranza di ±1 minuto)
|
||||
var currentTimeOfDay = currentTime.TimeOfDay;
|
||||
var timeDifference = Math.Abs((currentTimeOfDay - scheduledTime).TotalMinutes);
|
||||
|
||||
if (timeDifference > 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Se non è mai stato eseguito, esegui
|
||||
if (!schedule.LastExecutionTime.HasValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Se è stato eseguito, verifica che sia passata almeno una settimana
|
||||
var daysSinceLastExecution = (currentTime.Date - schedule.LastExecutionTime.Value.Date).TotalDays;
|
||||
|
||||
return daysSinceLastExecution >= 7;
|
||||
}
|
||||
|
||||
private bool ShouldExecuteMonthlySchedule(ProfileSchedule schedule, DateTime currentTime)
|
||||
{
|
||||
if (!schedule.DayOfMonth.HasValue || string.IsNullOrEmpty(schedule.DailyTime))
|
||||
{
|
||||
_logger.LogWarning("Schedulazione 'monthly' senza giorno del mese o orario specificato: {ScheduleName}",
|
||||
schedule.Name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
|
||||
{
|
||||
_logger.LogWarning("Formato orario non valido per schedulazione monthly {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)",
|
||||
schedule.Name, schedule.DailyTime);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verifica il giorno del mese
|
||||
var currentDayOfMonth = currentTime.Day;
|
||||
var scheduledDayOfMonth = schedule.DayOfMonth.Value;
|
||||
|
||||
// Gestione per mesi con meno giorni (es. 31 in febbraio diventa ultimo giorno del mese)
|
||||
var daysInCurrentMonth = DateTime.DaysInMonth(currentTime.Year, currentTime.Month);
|
||||
var effectiveScheduledDay = Math.Min(scheduledDayOfMonth, daysInCurrentMonth);
|
||||
|
||||
if (currentDayOfMonth != effectiveScheduledDay)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verifica l'orario (con tolleranza di ±1 minuto)
|
||||
var currentTimeOfDay = currentTime.TimeOfDay;
|
||||
var timeDifference = Math.Abs((currentTimeOfDay - scheduledTime).TotalMinutes);
|
||||
|
||||
if (timeDifference > 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Se non è mai stato eseguito, esegui
|
||||
if (!schedule.LastExecutionTime.HasValue)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Se è stato eseguito, verifica che sia passato almeno un mese
|
||||
var lastExecutionDate = schedule.LastExecutionTime.Value;
|
||||
var monthsSinceLastExecution = ((currentTime.Year - lastExecutionDate.Year) * 12) + currentTime.Month - lastExecutionDate.Month;
|
||||
|
||||
return monthsSinceLastExecution >= 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggiorna la schedulazione dopo l'esecuzione
|
||||
/// </summary>
|
||||
private async Task UpdateScheduleAfterExecution(IProfileScheduleService scheduleService,
|
||||
ProfileSchedule schedule, DateTime executionTime, bool wasSuccessful)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Aggiorna i dati della schedulazione
|
||||
schedule.LastExecutionTime = executionTime;
|
||||
schedule.ExecutionCount++;
|
||||
|
||||
// Calcola il prossimo tempo di esecuzione
|
||||
schedule.NextExecutionTime = CalculateNextExecutionTime(schedule, executionTime);
|
||||
|
||||
// Per schedulazioni "once", disabilita dopo l'esecuzione
|
||||
if (schedule.ScheduleType.ToLower() == "once")
|
||||
{
|
||||
schedule.IsEnabled = false;
|
||||
_logger.LogInformation("Schedulazione 'once' {ScheduleName} disabilitata dopo l'esecuzione", schedule.Name);
|
||||
}
|
||||
|
||||
await scheduleService.UpdateScheduleAsync(schedule);
|
||||
|
||||
_logger.LogDebug("Schedulazione {ScheduleName} aggiornata. Prossima esecuzione: {NextExecution}",
|
||||
schedule.Name, schedule.NextExecutionTime.HasValue ? DateTimeHelper.FormatDateTime24H(schedule.NextExecutionTime.Value) : "N/A");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Errore durante l'aggiornamento della schedulazione {ScheduleName}", schedule.Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calcola il prossimo tempo di esecuzione per una schedulazione
|
||||
/// </summary>
|
||||
private DateTime? CalculateNextExecutionTime(ProfileSchedule schedule, DateTime lastExecution)
|
||||
{
|
||||
if (!schedule.IsEnabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return schedule.ScheduleType.ToLower() switch
|
||||
{
|
||||
"once" => null, // Schedulazione singola non ha prossima esecuzione
|
||||
"daily" => CalculateNextDailyExecution(schedule, lastExecution),
|
||||
"weekly" => CalculateNextWeeklyExecution(schedule, lastExecution),
|
||||
"monthly" => CalculateNextMonthlyExecution(schedule, lastExecution),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private DateTime? CalculateNextDailyExecution(ProfileSchedule schedule, DateTime lastExecution)
|
||||
{
|
||||
if (string.IsNullOrEmpty(schedule.DailyTime) || !DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nextDate = lastExecution.Date.AddDays(1);
|
||||
return nextDate.Add(scheduledTime);
|
||||
}
|
||||
|
||||
private DateTime? CalculateNextWeeklyExecution(ProfileSchedule schedule, DateTime lastExecution)
|
||||
{
|
||||
if (!schedule.DayOfWeek.HasValue || string.IsNullOrEmpty(schedule.DailyTime) ||
|
||||
!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nextDate = lastExecution.Date.AddDays(7);
|
||||
return nextDate.Add(scheduledTime);
|
||||
}
|
||||
|
||||
private DateTime? CalculateNextMonthlyExecution(ProfileSchedule schedule, DateTime lastExecution)
|
||||
{
|
||||
if (!schedule.DayOfMonth.HasValue || string.IsNullOrEmpty(schedule.DailyTime) ||
|
||||
!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nextMonth = lastExecution.AddMonths(1);
|
||||
var daysInNextMonth = DateTime.DaysInMonth(nextMonth.Year, nextMonth.Month);
|
||||
var effectiveDay = Math.Min(schedule.DayOfMonth.Value, daysInNextMonth);
|
||||
|
||||
var nextDate = new DateTime(nextMonth.Year, nextMonth.Month, effectiveDay);
|
||||
return nextDate.Add(scheduledTime);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user