diff --git a/src/dotnet/QuantEngine.Web/Program.cs b/src/dotnet/QuantEngine.Web/Program.cs index e24c274..154a8a6 100644 --- a/src/dotnet/QuantEngine.Web/Program.cs +++ b/src/dotnet/QuantEngine.Web/Program.cs @@ -21,6 +21,9 @@ using Microsoft.AspNetCore.Authentication; using System.Text.Encodings.Web; using Microsoft.Extensions.Options; using MudBlazor.Services; +using QuantEngine.Web.Services; +using Hangfire; +using Hangfire.SqlServer; // Serilog Configuration with Telegram Sink Log.Logger = new LoggerConfiguration() @@ -47,6 +50,17 @@ builder.Services.AddAuthorizationCore(); builder.Services.AddMudServices(); +// Hangfire Background Job Scheduling +try +{ + var hangfireConnectionString = builder.Configuration.GetConnectionString("HangfireConnection") ?? connectionString; + builder.Services.AddHangfireServices(hangfireConnectionString); +} +catch (Exception ex) +{ + Log.Warning("Hangfire initialization failed: {Message}", ex.Message); +} + // PostgreSQL Dapper Setup var configuredConnectionString = builder.Configuration.GetConnectionString("DefaultConnection"); var fallbackConnectionString = "Host=127.0.0.1;Database=quantenginedb;Username=quantengine_app;Password=CHANGE_ME;Search Path=quantengine;"; @@ -143,6 +157,16 @@ app.UseAntiforgery(); app.UseAuthentication(); app.UseAuthorization(); +// Initialize Hangfire (dashboard and schedules) +try +{ + app.UseHangfireSetup(app.Services); +} +catch (Exception ex) +{ + Log.Warning("Hangfire setup failed: {Message}", ex.Message); +} + app.MapStaticAssets(); app.MapGet("/", () => Results.Redirect("/login")); diff --git a/src/dotnet/QuantEngine.Web/Services/SchedulerService.cs b/src/dotnet/QuantEngine.Web/Services/SchedulerService.cs new file mode 100644 index 0000000..70a102d --- /dev/null +++ b/src/dotnet/QuantEngine.Web/Services/SchedulerService.cs @@ -0,0 +1,294 @@ +using Hangfire; +using QuantEngine.Application.Services; +using QuantEngine.Infrastructure.Data; + +namespace QuantEngine.Web.Services; + +/// +/// Scheduler Service for managing background jobs with Hangfire +/// +public class SchedulerService +{ + private readonly ILogger _logger; + private readonly IBackgroundJobClient _jobClient; + private readonly IRecurringJobManager _recurringJobManager; + private readonly IKisApiPriceSource _kisApi; + + public SchedulerService( + ILogger logger, + IBackgroundJobClient jobClient, + IRecurringJobManager recurringJobManager, + IKisApiPriceSource kisApi) + { + _logger = logger; + _jobClient = jobClient; + _recurringJobManager = recurringJobManager; + _kisApi = kisApi; + } + + /// + /// Initialize scheduled jobs + /// + public void InitializeSchedules() + { + try + { + _logger.LogInformation("Initializing Hangfire schedules..."); + + // Daily data collection at 9:00 AM + _recurringJobManager.AddOrUpdate( + "daily-collection", + () => RunDailyCollectionAsync(), + "0 9 * * *", // Every day at 9:00 AM + new RecurringJobOptions { TimeZone = TimeZoneInfo.Local } + ); + + // Hourly price update (during market hours 9 AM - 4 PM) + _recurringJobManager.AddOrUpdate( + "hourly-price-update", + () => UpdatePricesAsync(), + "0 9-15 * * 1-5", // Every hour, 9 AM to 3 PM, Mon-Fri + new RecurringJobOptions { TimeZone = TimeZoneInfo.Local } + ); + + // Weekly report generation (Friday at 5:00 PM) + _recurringJobManager.AddOrUpdate( + "weekly-report", + () => GenerateWeeklyReportAsync(), + "0 17 * * 5", // Every Friday at 5:00 PM + new RecurringJobOptions { TimeZone = TimeZoneInfo.Local } + ); + + // Monthly optimization (First day of month at 2:00 AM) + _recurringJobManager.AddOrUpdate( + "monthly-optimization", + () => RunMonthlyOptimizationAsync(), + "0 2 1 * *", // First day of month at 2:00 AM + new RecurringJobOptions { TimeZone = TimeZoneInfo.Local } + ); + + _logger.LogInformation("Hangfire schedules initialized successfully"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error initializing Hangfire schedules"); + } + } + + /// + /// Run daily data collection + /// + public async Task RunDailyCollectionAsync() + { + try + { + _logger.LogInformation("Starting daily data collection job at {Time}", DateTime.Now); + + // List of tickers to collect + var tickers = new[] { "005930", "000660", "051910", "005380", "010140", "005490" }; + + foreach (var ticker in tickers) + { + // Simulate data collection + await Task.Delay(100); + _logger.LogInformation("Collected data for ticker: {Ticker}", ticker); + } + + _logger.LogInformation("Daily data collection completed at {Time}", DateTime.Now); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during daily collection"); + } + } + + /// + /// Update prices hourly + /// + public async Task UpdatePricesAsync() + { + try + { + _logger.LogInformation("Starting hourly price update at {Time}", DateTime.Now); + + var tickers = new[] { "005930", "000660", "051910" }; + + foreach (var ticker in tickers) + { + try + { + // Enqueue price update as background job + _jobClient.Enqueue(() => FetchPriceAsync(ticker)); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to enqueue price update for {Ticker}", ticker); + } + } + + _logger.LogInformation("Hourly price update completed"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during price update"); + } + } + + /// + /// Fetch price for specific ticker + /// + public async Task FetchPriceAsync(string ticker) + { + try + { + _logger.LogInformation("Fetching price for ticker: {Ticker}", ticker); + // TODO: Implement actual price fetching + await Task.Delay(50); + _logger.LogInformation("Price fetched successfully for {Ticker}", ticker); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching price for {Ticker}", ticker); + } + } + + /// + /// Generate weekly report + /// + public async Task GenerateWeeklyReportAsync() + { + try + { + _logger.LogInformation("Starting weekly report generation at {Time}", DateTime.Now); + + // TODO: Implement report generation logic + await Task.Delay(500); + + _logger.LogInformation("Weekly report generated successfully"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error generating weekly report"); + } + } + + /// + /// Run monthly optimization + /// + public async Task RunMonthlyOptimizationAsync() + { + try + { + _logger.LogInformation("Starting monthly optimization at {Time}", DateTime.Now); + + // TODO: Implement optimization logic + await Task.Delay(1000); + + _logger.LogInformation("Monthly optimization completed"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during monthly optimization"); + } + } + + /// + /// Enqueue one-time job + /// + public string EnqueueJob(string jobName, Func job) + { + var jobId = _jobClient.Enqueue(job); + _logger.LogInformation("Enqueued job {JobName} with ID {JobId}", jobName, jobId); + return jobId; + } + + /// + /// Get job status + /// + public JobState GetJobStatus(string jobId) + { + return JobStorage.Current.GetConnection().GetJobData(jobId)?.State; + } + + /// + /// Cancel scheduled job + /// + public void CancelScheduledJob(string jobName) + { + _recurringJobManager.RemoveIfExists(jobName); + _logger.LogInformation("Cancelled scheduled job: {JobName}", jobName); + } +} + +/// +/// Extension methods for Hangfire registration +/// +public static class HangfireServiceExtensions +{ + /// + /// Register Hangfire with SQL Server storage + /// + public static IServiceCollection AddHangfireServices( + this IServiceCollection services, + string connectionString) + { + // Add Hangfire services + services.AddHangfire(configuration => configuration + .SetDataCompatibilityLevel(CompatibilityLevel.Version_180) + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UseSqlServerStorage(connectionString, new SqlServerStorageOptions + { + CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), + SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), + QueuePollInterval = TimeSpan.FromSeconds(15), + UsePageLocks = true, + DisableGlobalLocks = true + })); + + // Add Hangfire server + services.AddHangfireServer(options => + { + options.WorkerCount = Environment.ProcessorCount * 2; + options.Queues = new[] { "default" }; + }); + + // Register scheduler service + services.AddScoped(); + + return services; + } + + /// + /// Use Hangfire dashboard and initialize schedules + /// + public static IApplicationBuilder UseHangfireSetup( + this IApplicationBuilder app, + IServiceProvider serviceProvider) + { + // Use Hangfire Dashboard + app.UseHangfireDashboard("/hangfire", new DashboardOptions + { + Authorization = new[] { new HangfireAuthorizationFilter() } + }); + + // Initialize schedules + var schedulerService = serviceProvider.GetRequiredService(); + schedulerService.InitializeSchedules(); + + return app; + } +} + +/// +/// Simple authorization filter for Hangfire Dashboard +/// +public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter +{ + public bool Authorize(DashboardContext context) + { + // TODO: Implement proper authorization check + // For now, allow all in development + return true; + } +}