|
|
|
|
@@ -2,70 +2,109 @@ using Mercados.Infrastructure.DataFetchers;
|
|
|
|
|
|
|
|
|
|
namespace Mercados.Worker
|
|
|
|
|
{
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Servicio de fondo que orquesta la obtención de datos de diversas fuentes
|
|
|
|
|
/// de forma programada y periódica.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public class DataFetchingService : BackgroundService
|
|
|
|
|
{
|
|
|
|
|
private readonly ILogger<DataFetchingService> _logger;
|
|
|
|
|
private readonly IServiceProvider _serviceProvider;
|
|
|
|
|
private readonly TimeZoneInfo _argentinaTimeZone;
|
|
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
private readonly Dictionary<string, DateTime> _lastDailyRun = new();
|
|
|
|
|
|
|
|
|
|
public DataFetchingService(ILogger<DataFetchingService> logger, IServiceProvider serviceProvider)
|
|
|
|
|
{
|
|
|
|
|
_logger = logger;
|
|
|
|
|
_serviceProvider = serviceProvider;
|
|
|
|
|
|
|
|
|
|
// Se define explícitamente la zona horaria de Argentina.
|
|
|
|
|
// Esto asegura que los cálculos de tiempo sean correctos, sin importar
|
|
|
|
|
// la configuración de zona horaria del servidor donde se ejecute el worker.
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
// El ID estándar para Linux y macOS
|
|
|
|
|
_argentinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("America/Argentina/Buenos_Aires");
|
|
|
|
|
}
|
|
|
|
|
catch (TimeZoneNotFoundException)
|
|
|
|
|
{
|
|
|
|
|
// El ID equivalente para Windows
|
|
|
|
|
_argentinaTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Argentina Standard Time");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Método principal del servicio. Se ejecuta una vez cuando el servicio arranca.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("🚀 Servicio de Fetching iniciado a las: {time}", DateTimeOffset.Now);
|
|
|
|
|
|
|
|
|
|
// Ejecutamos una vez al inicio para tener datos frescos inmediatamente.
|
|
|
|
|
await RunAllFetchersAsync();
|
|
|
|
|
// Se recomienda una ejecución inicial para poblar la base de datos inmediatamente
|
|
|
|
|
// al iniciar el servicio, en lugar de esperar al primer horario programado.
|
|
|
|
|
//await RunAllFetchersAsync(stoppingToken);
|
|
|
|
|
|
|
|
|
|
// Usamos un PeriodicTimer que "despierta" cada minuto para revisar si hay tareas pendientes.
|
|
|
|
|
// PeriodicTimer es una forma moderna y eficiente de crear un bucle de "tic-tac"
|
|
|
|
|
// sin bloquear un hilo con Task.Delay.
|
|
|
|
|
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
|
|
|
|
|
|
|
|
|
|
while (await timer.WaitForNextTickAsync(stoppingToken))
|
|
|
|
|
// El bucle se ejecuta cada minuto mientras el servicio no reciba una señal de detención.
|
|
|
|
|
while (!stoppingToken.IsCancellationRequested && await timer.WaitForNextTickAsync(stoppingToken))
|
|
|
|
|
{
|
|
|
|
|
await RunScheduledTasksAsync();
|
|
|
|
|
await RunScheduledTasksAsync(stoppingToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task RunScheduledTasksAsync()
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Revisa la hora actual y ejecuta las tareas que coincidan con su horario programado.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task RunScheduledTasksAsync(CancellationToken stoppingToken)
|
|
|
|
|
{
|
|
|
|
|
// --- Lógica de Planificación ---
|
|
|
|
|
var now = DateTime.Now;
|
|
|
|
|
// Se obtiene la hora actual convertida a la zona horaria de Argentina.
|
|
|
|
|
var nowInArgentina = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, _argentinaTimeZone);
|
|
|
|
|
|
|
|
|
|
// Tarea 1: Mercado Agroganadero (todos los días a las 11:00)
|
|
|
|
|
if (now.Hour == 11 && now.Minute == 0 && HasNotRunToday("MercadoAgroganadero"))
|
|
|
|
|
// --- Tarea 1: Mercado Agroganadero (L-V a las 11:00 AM) ---
|
|
|
|
|
if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 0 && HasNotRunToday("MercadoAgroganadero"))
|
|
|
|
|
{
|
|
|
|
|
await RunFetcherByNameAsync("MercadoAgroganadero");
|
|
|
|
|
_lastDailyRun["MercadoAgroganadero"] = now.Date;
|
|
|
|
|
await RunFetcherByNameAsync("MercadoAgroganadero", stoppingToken);
|
|
|
|
|
_lastDailyRun["MercadoAgroganadero"] = nowInArgentina.Date;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tarea 2: Granos BCR (todos los días a las 11:30)
|
|
|
|
|
if (now.Hour == 11 && now.Minute == 30 && HasNotRunToday("BCR"))
|
|
|
|
|
// --- Tarea 2: Granos BCR (L-V a las 11:30 AM) ---
|
|
|
|
|
if (IsWeekDay(nowInArgentina) && nowInArgentina.Hour == 11 && nowInArgentina.Minute == 30 && HasNotRunToday("BCR"))
|
|
|
|
|
{
|
|
|
|
|
await RunFetcherByNameAsync("BCR");
|
|
|
|
|
_lastDailyRun["BCR"] = now.Date;
|
|
|
|
|
await RunFetcherByNameAsync("BCR", stoppingToken);
|
|
|
|
|
_lastDailyRun["BCR"] = nowInArgentina.Date;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Tarea 3: Mercados de Bolsa (cada 10 minutos si el mercado está abierto)
|
|
|
|
|
if (IsMarketOpen(now) && now.Minute % 10 == 0)
|
|
|
|
|
// --- 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)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Mercados abiertos. Ejecutando fetchers de bolsa.");
|
|
|
|
|
await RunFetcherByNameAsync("Finnhub");
|
|
|
|
|
await RunFetcherByNameAsync("YahooFinance");
|
|
|
|
|
_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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Esta función crea un "scope" para ejecutar un fetcher específico.
|
|
|
|
|
// Esto es crucial para que la inyección de dependencias funcione correctamente.
|
|
|
|
|
private async Task RunFetcherByNameAsync(string sourceName)
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Ejecuta un fetcher específico por su nombre. Utiliza un scope de DI para gestionar
|
|
|
|
|
/// correctamente el ciclo de vida de los servicios (como las conexiones a la BD).
|
|
|
|
|
/// </summary>
|
|
|
|
|
private async Task RunFetcherByNameAsync(string sourceName, CancellationToken stoppingToken)
|
|
|
|
|
{
|
|
|
|
|
if (stoppingToken.IsCancellationRequested) return;
|
|
|
|
|
|
|
|
|
|
_logger.LogInformation("Intentando ejecutar fetcher: {sourceName}", sourceName);
|
|
|
|
|
|
|
|
|
|
// Crea un "scope" de servicios. Todos los servicios "scoped" (como los repositorios)
|
|
|
|
|
// se crearán de nuevo para esta ejecución y se desecharán al final, evitando problemas.
|
|
|
|
|
using var scope = _serviceProvider.CreateScope();
|
|
|
|
|
var fetchers = scope.ServiceProvider.GetRequiredService<IEnumerable<IDataFetcher>>();
|
|
|
|
|
var fetcher = fetchers.FirstOrDefault(f => f.SourceName.Equals(sourceName, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
@@ -84,32 +123,42 @@ namespace Mercados.Worker
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Función de ayuda para ejecutar todos los fetchers (usada al inicio).
|
|
|
|
|
private async Task RunAllFetchersAsync()
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Ejecuta todos los fetchers al iniciar el servicio. Esto es útil para poblar
|
|
|
|
|
/// la base de datos inmediatamente al arrancar el worker.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/*
|
|
|
|
|
private async Task RunAllFetchersAsync(CancellationToken stoppingToken)
|
|
|
|
|
{
|
|
|
|
|
_logger.LogInformation("Ejecutando todos los fetchers al iniciar...");
|
|
|
|
|
using var scope = _serviceProvider.CreateScope();
|
|
|
|
|
var fetchers = scope.ServiceProvider.GetRequiredService<IEnumerable<IDataFetcher>>();
|
|
|
|
|
foreach (var fetcher in fetchers)
|
|
|
|
|
{
|
|
|
|
|
await RunFetcherByNameAsync(fetcher.SourceName);
|
|
|
|
|
if (stoppingToken.IsCancellationRequested) break;
|
|
|
|
|
await RunFetcherByNameAsync(fetcher.SourceName, stoppingToken);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#region Funciones de Ayuda para la Planificación
|
|
|
|
|
|
|
|
|
|
private bool HasNotRunToday(string taskName)
|
|
|
|
|
{
|
|
|
|
|
return !_lastDailyRun.ContainsKey(taskName) || _lastDailyRun[taskName].Date < DateTime.Now.Date;
|
|
|
|
|
// 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 IsMarketOpen(DateTime now)
|
|
|
|
|
private bool IsWeekDay(DateTime now)
|
|
|
|
|
{
|
|
|
|
|
// Lunes a Viernes (1 a 5, Domingo es 0)
|
|
|
|
|
if (now.DayOfWeek == DayOfWeek.Saturday || now.DayOfWeek == DayOfWeek.Sunday)
|
|
|
|
|
return false;
|
|
|
|
|
return now.DayOfWeek >= DayOfWeek.Monday && now.DayOfWeek <= DayOfWeek.Friday;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsArgentineMarketOpen(DateTime now)
|
|
|
|
|
{
|
|
|
|
|
if (!IsWeekDay(now)) return false;
|
|
|
|
|
|
|
|
|
|
// Horario de mercado de 11:00 a 17:15 (hora de Argentina)
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
|