feat(Worker): Refactoriza planificador para usar expresiones Cron desde configuración

This commit is contained in:
2025-07-03 11:57:11 -03:00
parent 93b2887bd5
commit 1730c66d6a
3 changed files with 74 additions and 37 deletions

View File

@@ -1,4 +1,5 @@
using Mercados.Infrastructure.DataFetchers; using Mercados.Infrastructure.DataFetchers;
using Cronos;
namespace Mercados.Worker namespace Mercados.Worker
{ {
@@ -11,15 +12,17 @@ namespace Mercados.Worker
private readonly ILogger<DataFetchingService> _logger; private readonly ILogger<DataFetchingService> _logger;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly TimeZoneInfo _argentinaTimeZone; private readonly TimeZoneInfo _argentinaTimeZone;
private readonly IConfiguration _configuration;
// Diccionario para rastrear la última vez que se ejecutó una tarea diaria // Diccionario para rastrear la última vez que se ejecutó una tarea diaria
// y evitar que se ejecute múltiples veces si el servicio se reinicia. // y evitar que se ejecute múltiples veces si el servicio se reinicia.
private readonly Dictionary<string, DateTime> _lastDailyRun = new(); private readonly Dictionary<string, DateTime> _lastDailyRun = new();
public DataFetchingService(ILogger<DataFetchingService> logger, IServiceProvider serviceProvider) public DataFetchingService(ILogger<DataFetchingService> logger, IServiceProvider serviceProvider,IConfiguration configuration)
{ {
_logger = logger; _logger = logger;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_configuration = configuration;
// Se define explícitamente la zona horaria de Argentina. // Se define explícitamente la zona horaria de Argentina.
// Esto asegura que los cálculos de tiempo sean correctos, sin importar // Esto asegura que los cálculos de tiempo sean correctos, sin importar
@@ -63,33 +66,78 @@ namespace Mercados.Worker
/// </summary> /// </summary>
private async Task RunScheduledTasksAsync(CancellationToken stoppingToken) private async Task RunScheduledTasksAsync(CancellationToken stoppingToken)
{ {
// Se obtiene la hora actual convertida a la zona horaria de Argentina. var utcNow = DateTime.UtcNow;
var nowInArgentina = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone);
// --- Tarea 1: Mercado Agroganadero (L-V a las 11:00 AM) --- // Obtenemos las expresiones Cron desde la configuración
if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 0 && HasNotRunToday("MercadoAgroganadero")) string? agroSchedule = _configuration["Schedules:MercadoAgroganadero"];
string? bcrSchedule = _configuration["Schedules:BCR"];
string? bolsasSchedule = _configuration["Schedules:Bolsas"];
// Comprobamos cada una antes de usarla
if (!string.IsNullOrEmpty(agroSchedule))
{ {
await RunFetcherByNameAsync("MercadoAgroganadero", stoppingToken); await TryRunDailyTaskAsync("MercadoAgroganadero", agroSchedule, utcNow, stoppingToken);
_lastDailyRun["MercadoAgroganadero"] = nowInArgentina.Date; }
else
{
_logger.LogWarning("No se encontró la configuración de horario para 'MercadoAgroganadero' en appsettings.json.");
} }
// --- Tarea 2: Granos BCR (L-V a las 11:30 AM) --- if (!string.IsNullOrEmpty(bcrSchedule))
if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 30 && HasNotRunToday("BCR"))
{ {
await RunFetcherByNameAsync("BCR", stoppingToken); await TryRunDailyTaskAsync("BCR", bcrSchedule, utcNow, stoppingToken);
_lastDailyRun["BCR"] = nowInArgentina.Date; }
else
{
_logger.LogWarning("No se encontró la configuración de horario para 'BCR' en appsettings.json.");
} }
// --- Tarea 3 y 4: Mercados de Bolsa (L-V, durante horario de mercado, una vez por hora) --- if (!string.IsNullOrEmpty(bolsasSchedule))
// Se ejecutan si el mercado está abierto y si el minuto actual es exactamente 10.
// Esto replica la lógica de "cada hora a las y 10".
if (IsArgentineMarketOpen(nowInArgentina) && nowInArgentina.Minute == 10)
{ {
_logger.LogInformation("Hora de actualización de mercados de bolsa. Ejecutando fetchers..."); await TryRunRecurringTaskAsync(new[] { "YahooFinance", "Finnhub" }, bolsasSchedule, utcNow, stoppingToken);
}
else
{
_logger.LogWarning("No se encontró la configuración de horario para 'Bolsas' en appsettings.json.");
}
// --- ^ FIN DE LA CORRECCIÓN DE NULABILIDAD ^ ---
}
await RunFetcherByNameAsync("YahooFinance", stoppingToken); /// <summary>
// Si Finnhub está desactivado en Program.cs, este simplemente no se encontrará y se omitirá. /// Comprueba y ejecuta una tarea que debe correr solo una vez al día.
await RunFetcherByNameAsync("Finnhub", stoppingToken); /// </summary>
private async Task TryRunDailyTaskAsync(string taskName, string cronExpression, DateTime utcNow, CancellationToken stoppingToken)
{
var cron = CronExpression.Parse(cronExpression);
var nextOccurrence = cron.GetNextOccurrence(utcNow.AddMinutes(-1));
if (nextOccurrence.HasValue && nextOccurrence.Value <= utcNow)
{
if (HasNotRunToday(taskName))
{
await RunFetcherByNameAsync(taskName, stoppingToken);
_lastDailyRun[taskName] = TimeZoneInfo.ConvertTimeFromUtc(utcNow, _argentinaTimeZone).Date;
}
}
}
/// <summary>
/// Comprueba y ejecuta una tarea que puede correr múltiples veces al día.
/// </summary>
private async Task TryRunRecurringTaskAsync(string[] taskNames, string cronExpression, DateTime utcNow, CancellationToken stoppingToken)
{
// Añadimos 'IncludeSeconds' para que la comparación sea precisa y no se ejecute dos veces en el mismo minuto.
var cron = CronExpression.Parse(cronExpression, CronFormat.IncludeSeconds);
// Comprobamos si hubo una ocurrencia en el último minuto.
var nextOccurrence = cron.GetNextOccurrence(utcNow.AddMinutes(-1));
if (nextOccurrence.HasValue && nextOccurrence.Value <= utcNow)
{
_logger.LogInformation("Ventana de ejecución recurrente detectada para: {Tasks}", string.Join(", ", taskNames));
foreach (var taskName in taskNames)
{
await RunFetcherByNameAsync(taskName, stoppingToken);
}
} }
} }
@@ -145,26 +193,9 @@ namespace Mercados.Worker
private bool HasNotRunToday(string taskName) private bool HasNotRunToday(string taskName)
{ {
// Comprueba si la tarea ya se ejecutó en la fecha actual (en zona horaria de Argentina).
return !_lastDailyRun.ContainsKey(taskName) || _lastDailyRun[taskName].Date < TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone).Date; return !_lastDailyRun.ContainsKey(taskName) || _lastDailyRun[taskName].Date < TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone).Date;
} }
private bool IsWeekDay(DateTime now)
{
return now.DayOfWeek >= DayOfWeek.Monday && now.DayOfWeek <= DayOfWeek.Friday;
}
private bool IsArgentineMarketOpen(DateTime now)
{
if (!IsWeekDay(now)) return false;
// Rango de 11:00 a 17:15, para asegurar la captura del cierre a las 17:10.
var marketOpen = new TimeSpan(11, 0, 0);
var marketClose = new TimeSpan(17, 15, 0);
return now.TimeOfDay >= marketOpen && now.TimeOfDay <= marketClose;
}
#endregion #endregion
} }
} }

View File

@@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Cronos" Version="0.11.0" />
<PackageReference Include="DotNetEnv" Version="3.1.1" /> <PackageReference Include="DotNetEnv" Version="3.1.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.5" />
</ItemGroup> </ItemGroup>

View File

@@ -9,6 +9,11 @@
"ConnectionStrings": { "ConnectionStrings": {
"DefaultConnection": "" "DefaultConnection": ""
}, },
"Schedules": {
"MercadoAgroganadero": "0 11 * * 1-5",
"BCR": "30 11 * * 1-5",
"Bolsas": "10 11-17 * * 1-5"
},
"ApiKeys": { "ApiKeys": {
"Finnhub": "", "Finnhub": "",
"Bcr": { "Bcr": {