using Hangfire;
using Hangfire.States;
using Hangfire.Dashboard;
using Hangfire.PostgreSql;
using Hangfire.MemoryStorage;
using System.Linq.Expressions;
using QuantEngine.Application.Services;
using QuantEngine.Application.Interfaces;
using QuantEngine.Infrastructure.Data;
using Microsoft.Extensions.Configuration;
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 IServiceScopeFactory _scopeFactory;
private readonly IConfiguration _configuration;
public SchedulerService(
ILogger logger,
IBackgroundJobClient jobClient,
IRecurringJobManager recurringJobManager,
IServiceScopeFactory scopeFactory,
IConfiguration configuration)
{
_logger = logger;
_jobClient = jobClient;
_recurringJobManager = recurringJobManager;
_scopeFactory = scopeFactory;
_configuration = configuration;
}
///
/// 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, every 2 hours)
_recurringJobManager.AddOrUpdate(
"hourly-price-update",
() => UpdatePricesAsync(),
"0 9,11,13,15 * * 1-5", // 9:00, 11:00, 13:00, 15:00 on 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" };
// Create scope for scoped services
using var scope = _scopeFactory.CreateScope();
var orchestrator = scope.ServiceProvider.GetRequiredService();
// Build runId with timestamp
var runId = $"daily-{DateTime.Now:yyyyMMdd-HHmmss}";
// Read account mode from configuration (default to "mock")
var accountMode = _configuration["Kis:AccountMode"] ?? "mock";
// Execute collection
var result = await orchestrator.RunCollectionAsync(runId, accountMode, tickers.ToList());
// Log completion
_logger.LogInformation("Collection run {RunId} completed: {Snapshots} snapshots, {Errors} errors",
runId, result.SuccessCount, result.ErrorCount);
}
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, Expression> job)
{
var jobId = _jobClient.Enqueue(job);
_logger.LogInformation("Enqueued job {JobName} with ID {JobId}", jobName, jobId);
return jobId;
}
///
/// Get job status
///
public string? 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();
try
{
using (var conn = new Npgsql.NpgsqlConnection(connectionString))
{
conn.Open();
}
configuration.UsePostgreSqlStorage(options => options.UseNpgsqlConnection(connectionString), new PostgreSqlStorageOptions
{
QueuePollInterval = TimeSpan.FromSeconds(15),
PrepareSchemaIfNecessary = true
});
Console.WriteLine("[Hangfire] Configured PostgreSQL storage successfully.");
}
catch (Exception ex)
{
Console.WriteLine($"[Hangfire] PostgreSQL connection failed ({ex.Message}). Falling back to MemoryStorage.");
configuration.UseMemoryStorage();
}
});
// 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. SchedulerService is registered as Scoped
// (AddScoped above), so it cannot be resolved directly from the
// root provider passed in here (app.Services) -- doing so silently
// failed every startup with "Cannot resolve scoped service
// 'SchedulerService' from root provider", meaning the recurring
// jobs were never being freshly registered/updated on boot (they
// only appeared to work because Hangfire persists them in
// PostgreSQL from whichever startup last managed to run this).
using var scope = serviceProvider.CreateScope();
var schedulerService = scope.ServiceProvider.GetRequiredService();
schedulerService.InitializeSchedules();
return app;
}
}
///
/// Simple authorization filter for Hangfire Dashboard
///
public class HangfireAuthorizationFilter : IDashboardAuthorizationFilter
{
public bool Authorize(DashboardContext context)
{
var httpContext = context.GetHttpContext();
return httpContext.User.Identity?.IsAuthenticated == true;
}
}