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 Cronos;
namespace Mercados.Worker
{
@@ -11,15 +12,17 @@ namespace Mercados.Worker
private readonly ILogger<DataFetchingService> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly TimeZoneInfo _argentinaTimeZone;
private readonly IConfiguration _configuration;
// 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.
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;
_serviceProvider = serviceProvider;
_configuration = configuration;
// Se define explícitamente la zona horaria de Argentina.
// Esto asegura que los cálculos de tiempo sean correctos, sin importar
@@ -63,33 +66,78 @@ namespace Mercados.Worker
/// </summary>
private async Task RunScheduledTasksAsync(CancellationToken stoppingToken)
{
// Se obtiene la hora actual convertida a la zona horaria de Argentina.
var nowInArgentina = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone);
var utcNow = DateTime.UtcNow;
// --- Tarea 1: Mercado Agroganadero (L-V a las 11:00 AM) ---
if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 0 && HasNotRunToday("MercadoAgroganadero"))
// Obtenemos las expresiones Cron desde la configuración
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);
_lastDailyRun["MercadoAgroganadero"] = nowInArgentina.Date;
await TryRunDailyTaskAsync("MercadoAgroganadero", agroSchedule, utcNow, stoppingToken);
}
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 (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 30 && HasNotRunToday("BCR"))
if (!string.IsNullOrEmpty(bcrSchedule))
{
await RunFetcherByNameAsync("BCR", stoppingToken);
_lastDailyRun["BCR"] = nowInArgentina.Date;
await TryRunDailyTaskAsync("BCR", bcrSchedule, utcNow, stoppingToken);
}
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) ---
// 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)
if (!string.IsNullOrEmpty(bolsasSchedule))
{
_logger.LogInformation("Hora de actualización de mercados de bolsa. Ejecutando fetchers...");
await RunFetcherByNameAsync("YahooFinance", stoppingToken);
// Si Finnhub está desactivado en Program.cs, este simplemente no se encontrará y se omitirá.
await RunFetcherByNameAsync("Finnhub", stoppingToken);
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 ^ ---
}
/// <summary>
/// Comprueba y ejecuta una tarea que debe correr solo una vez al día.
/// </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)
{
// 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;
}
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
}
}

View File

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

View File

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