Files
QuantEngineByItz/src/dotnet/QuantEngine.Web/Pages/Admin/Operations/Index.cshtml.cs
T
kjh2064 475950be36
Validators (Pushes and Pull Requests) / validate-ui-and-storage (push) Successful in 22s
Validators (Pushes and Pull Requests) / validate-core (push) Has been cancelled
Prepare Release / Build & Create Release (push) Successful in 1m9s
Prepare Release / Release Notification (push) Successful in 2s
feat(web): add manual trigger action support for Hangfire recurring jobs
2026-07-12 13:36:52 +09:00

157 lines
6.0 KiB
C#

using Hangfire;
using Hangfire.Storage;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using QuantEngine.Web.Services;
namespace QuantEngine.Web.Pages.Admin.Operations;
[Authorize(AuthenticationSchemes = AdminAuthDefaults.Scheme)]
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public List<ScheduledJobInfo>? ScheduledJobs { get; set; }
public List<JobExecutionInfo>? RecentExecutions { get; set; }
public int TotalJobsCount { get; set; }
public int ActiveJobsCount { get; set; }
public int InactiveJobsCount { get; set; }
public int PendingJobsCount { get; set; }
public bool IsJobProcessorRunning { get; set; }
public DateTime? LastRefreshTime { get; set; }
public string? StatusMessage { get; set; }
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public Task OnGetAsync()
{
LoadOperationsData();
return Task.CompletedTask;
}
public async Task<IActionResult> OnPostTriggerJobAsync(string jobId)
{
if (string.IsNullOrEmpty(jobId))
{
TempData["ErrorMessage"] = "올바르지 않은 작업 ID입니다.";
return RedirectToPage();
}
try
{
RecurringJob.TriggerJob(jobId);
TempData["SuccessMessage"] = $"작업 '{DescribeJobId(jobId)}'이(가) 즉시 실행 큐에 등록되었습니다.";
_logger.LogInformation("Manually triggered Hangfire recurring job: {JobId}", jobId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to trigger Hangfire job: {JobId}", jobId);
TempData["ErrorMessage"] = $"작업 실행 실패: {ex.Message}";
}
return RedirectToPage();
}
private void LoadOperationsData()
{
try
{
LastRefreshTime = DateTime.UtcNow;
// All data below comes directly from Hangfire's own storage
// (JobStorage.Current), which already backs the real
// recurring jobs registered in SchedulerService.InitializeSchedules
// (daily-collection, hourly-price-update, weekly-report,
// monthly-optimization) and every job it has actually run.
// This page previously returned four fabricated job names and
// fake execution timestamps with no connection to Hangfire at all.
using IStorageConnection connection = JobStorage.Current.GetConnection();
var monitoringApi = JobStorage.Current.GetMonitoringApi();
var recurringJobs = connection.GetRecurringJobs();
ScheduledJobs = recurringJobs
.Select(j => new ScheduledJobInfo(
j.Id,
DescribeJobId(j.Id),
DescribeCron(j.Cron),
j.NextExecution,
string.IsNullOrEmpty(j.Error)))
.ToList();
TotalJobsCount = ScheduledJobs.Count;
ActiveJobsCount = ScheduledJobs.Count(j => j.IsEnabled);
InactiveJobsCount = TotalJobsCount - ActiveJobsCount;
var succeeded = monitoringApi.SucceededJobs(0, 10)
.Select(kv => new JobExecutionInfo(
kv.Value.Job?.Method.Name ?? kv.Key,
kv.Value.SucceededAt ?? DateTime.UtcNow,
kv.Value.SucceededAt,
true));
var failed = monitoringApi.FailedJobs(0, 10)
.Select(kv => new JobExecutionInfo(
kv.Value.Job?.Method.Name ?? kv.Key,
kv.Value.FailedAt ?? DateTime.UtcNow,
kv.Value.FailedAt,
false));
RecentExecutions = succeeded.Concat(failed)
.OrderByDescending(e => e.StartedAt)
.Take(10)
.ToList();
var servers = monitoringApi.Servers();
IsJobProcessorRunning = servers.Count > 0;
PendingJobsCount = (int)monitoringApi.EnqueuedCount("default");
StatusMessage = IsJobProcessorRunning
? $"{servers.Count}개 워커 서버 운영 중"
: "Hangfire 서버가 등록되어 있지 않습니다";
_logger.LogInformation("Operations data loaded from Hangfire ({Count} recurring jobs, {Servers} servers)",
ScheduledJobs.Count, servers.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to load operations data from Hangfire");
ScheduledJobs = [];
RecentExecutions = [];
IsJobProcessorRunning = false;
StatusMessage = "Hangfire 상태 조회 실패 - 로그 확인 필요";
}
}
// Purely cosmetic: renders the actual CRON expression from Hangfire in
// readable Korean where we recognize the exact pattern SchedulerService
// registers, falling back to the raw CRON string for anything else so
// this never hides or fabricates a schedule that isn't really there.
private static string DescribeCron(string cron) => cron switch
{
"0 9 * * *" => "매일 09:00",
"0 9-15 * * 1-5" => "평일 09-15시 매시",
"0 17 * * 5" => "매주 금요일 17:00",
"0 2 1 * *" => "매월 1일 02:00",
_ => cron,
};
// Same rationale as DescribeCron: display label for a real Hangfire
// recurring-job id, not a substitute for it. Unknown ids pass through
// unchanged.
private static string DescribeJobId(string jobId) => jobId switch
{
"daily-collection" => "일일 데이터 수집",
"hourly-price-update" => "시간별 가격 갱신",
"weekly-report" => "주간 리포트 생성",
"monthly-optimization" => "월간 최적화",
_ => jobId,
};
}
public record ScheduledJobInfo(string JobId, string JobName, string Schedule, DateTime? NextRun, bool IsEnabled);
public record JobExecutionInfo(string JobName, DateTime StartedAt, DateTime? CompletedAt, bool IsSuccess);