feat(Worker): Refactoriza planificador para usar expresiones Cron desde configuración
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user