Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d9937c590 | |||
| ef6f9c74f6 | |||
| 24ec410f3d | |||
| 024122d310 | |||
| c6f269e30a | |||
| 97447a551f | |||
| fc0d341d15 | |||
| 688ee3350d |
@@ -2,7 +2,7 @@ name: Deploy to Production
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [Prepare Release]
|
||||
workflows: ["Prepare Release"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
name: Prepare Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_run:
|
||||
workflows: ["Validators (Pushes and Pull Requests)"]
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
@@ -16,6 +17,7 @@ env:
|
||||
jobs:
|
||||
build-and-release:
|
||||
name: Build & Create Release
|
||||
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
outputs:
|
||||
@@ -94,6 +96,10 @@ jobs:
|
||||
--no-restore \
|
||||
--no-build
|
||||
|
||||
- name: Write Version Text
|
||||
run: |
|
||||
echo "${{ steps.metadata.outputs.version }}" > ./publish/version.txt
|
||||
|
||||
- name: Write Production Config
|
||||
run: |
|
||||
mkdir -p ./publish
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="Hangfire.Core" Version="1.8.23" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -22,6 +24,7 @@
|
||||
<ProjectReference Include="..\QuantEngine.Core\QuantEngine.Core.csproj" />
|
||||
<ProjectReference Include="..\QuantEngine.Application\QuantEngine.Application.csproj" />
|
||||
<ProjectReference Include="..\QuantEngine.Infrastructure\QuantEngine.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\QuantEngine.Web\QuantEngine.Web.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="Fixtures\operational_report.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
using Xunit;
|
||||
using Moq;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Hangfire;
|
||||
using Hangfire.Common;
|
||||
using QuantEngine.Web.Services;
|
||||
|
||||
namespace QuantEngine.Core.Tests;
|
||||
|
||||
public class SchedulerServiceTests
|
||||
{
|
||||
[Fact]
|
||||
public void InitializeSchedules_RegistersAllFourRequiredJobs()
|
||||
{
|
||||
// Arrange
|
||||
var loggerMock = new Mock<ILogger<SchedulerService>>();
|
||||
var jobClientMock = new Mock<IBackgroundJobClient>();
|
||||
var recurringJobManagerMock = new Mock<IRecurringJobManager>();
|
||||
var scopeFactoryMock = new Mock<IServiceScopeFactory>();
|
||||
|
||||
var configMock = new Mock<IConfiguration>();
|
||||
configMock.Setup(c => c["Kis:AccountMode"]).Returns("mock");
|
||||
|
||||
var service = new SchedulerService(
|
||||
loggerMock.Object,
|
||||
jobClientMock.Object,
|
||||
recurringJobManagerMock.Object,
|
||||
scopeFactoryMock.Object,
|
||||
configMock.Object
|
||||
);
|
||||
|
||||
// Act
|
||||
service.InitializeSchedules();
|
||||
|
||||
// Assert
|
||||
// Verify daily-collection was added or updated
|
||||
recurringJobManagerMock.Verify(m => m.AddOrUpdate(
|
||||
"daily-collection",
|
||||
It.IsAny<Job>(),
|
||||
"0 9 * * *",
|
||||
It.IsAny<RecurringJobOptions>()
|
||||
), Times.Once);
|
||||
|
||||
// Verify hourly-price-update was added or updated
|
||||
recurringJobManagerMock.Verify(m => m.AddOrUpdate(
|
||||
"hourly-price-update",
|
||||
It.IsAny<Job>(),
|
||||
"0 9,11,13,15 * * 1-5",
|
||||
It.IsAny<RecurringJobOptions>()
|
||||
), Times.Once);
|
||||
|
||||
// Verify weekly-report was added or updated
|
||||
recurringJobManagerMock.Verify(m => m.AddOrUpdate(
|
||||
"weekly-report",
|
||||
It.IsAny<Job>(),
|
||||
"0 17 * * 5",
|
||||
It.IsAny<RecurringJobOptions>()
|
||||
), Times.Once);
|
||||
|
||||
// Verify monthly-optimization was added or updated
|
||||
recurringJobManagerMock.Verify(m => m.AddOrUpdate(
|
||||
"monthly-optimization",
|
||||
It.IsAny<Job>(),
|
||||
"0 2 1 * *",
|
||||
It.IsAny<RecurringJobOptions>()
|
||||
), Times.Once);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,20 @@ public class LoginModel : PageModel
|
||||
{
|
||||
get
|
||||
{
|
||||
// 1. Try reading version.txt from application base directory
|
||||
try
|
||||
{
|
||||
var txtPath = Path.Combine(AppContext.BaseDirectory, "version.txt");
|
||||
if (System.IO.File.Exists(txtPath))
|
||||
{
|
||||
var txtVersion = System.IO.File.ReadAllText(txtPath).Trim();
|
||||
if (!string.IsNullOrEmpty(txtVersion))
|
||||
return txtVersion;
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
|
||||
// 2. Try reading raw assembly version
|
||||
var rawVersion = Assembly.GetEntryAssembly()
|
||||
?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
?.InformationalVersion;
|
||||
@@ -31,8 +45,9 @@ public class LoginModel : PageModel
|
||||
return rawVersion;
|
||||
}
|
||||
|
||||
// 3. Fallback to dynamic local Git parsing if available
|
||||
var dateStr = DateTime.UtcNow.AddHours(9).ToString("yyyyMMdd");
|
||||
string gitHash = "a9986fb";
|
||||
string gitHash = "024122d";
|
||||
try
|
||||
{
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
@@ -55,7 +70,7 @@ public class LoginModel : PageModel
|
||||
var headContent = System.IO.File.ReadAllText(headPath).Trim();
|
||||
if (headContent.StartsWith("ref:"))
|
||||
{
|
||||
var refPath = Path.Combine(repoRoot, ".git", headContent.Substring(4).Trim());
|
||||
var refPath = Path.Combine(repoRoot, ".git", headContent.Substring(4).Trim().Replace('/', Path.DirectorySeparatorChar));
|
||||
if (System.IO.File.Exists(refPath))
|
||||
{
|
||||
var fullHash = System.IO.File.ReadAllText(refPath).Trim();
|
||||
@@ -72,7 +87,7 @@ public class LoginModel : PageModel
|
||||
}
|
||||
catch {}
|
||||
|
||||
int runCount = 14;
|
||||
int runCount = 11;
|
||||
return $"quant_{dateStr}.{runCount}.{gitHash}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,20 @@ public class IndexModel : PageModel
|
||||
{
|
||||
get
|
||||
{
|
||||
// 1. Try reading version.txt from application base directory
|
||||
try
|
||||
{
|
||||
var txtPath = Path.Combine(AppContext.BaseDirectory, "version.txt");
|
||||
if (System.IO.File.Exists(txtPath))
|
||||
{
|
||||
var txtVersion = System.IO.File.ReadAllText(txtPath).Trim();
|
||||
if (!string.IsNullOrEmpty(txtVersion))
|
||||
return txtVersion;
|
||||
}
|
||||
}
|
||||
catch {}
|
||||
|
||||
// 2. Try reading raw assembly version
|
||||
var rawVersion = Assembly.GetEntryAssembly()
|
||||
?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
?.InformationalVersion;
|
||||
@@ -32,8 +46,9 @@ public class IndexModel : PageModel
|
||||
return rawVersion;
|
||||
}
|
||||
|
||||
// 3. Fallback to dynamic local Git parsing if available
|
||||
var dateStr = DateTime.UtcNow.AddHours(9).ToString("yyyyMMdd");
|
||||
string gitHash = "a9986fb";
|
||||
string gitHash = "024122d";
|
||||
try
|
||||
{
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
@@ -56,7 +71,7 @@ public class IndexModel : PageModel
|
||||
var headContent = System.IO.File.ReadAllText(headPath).Trim();
|
||||
if (headContent.StartsWith("ref:"))
|
||||
{
|
||||
var refPath = Path.Combine(repoRoot, ".git", headContent.Substring(4).Trim());
|
||||
var refPath = Path.Combine(repoRoot, ".git", headContent.Substring(4).Trim().Replace('/', Path.DirectorySeparatorChar));
|
||||
if (System.IO.File.Exists(refPath))
|
||||
{
|
||||
var fullHash = System.IO.File.ReadAllText(refPath).Trim();
|
||||
@@ -73,7 +88,7 @@ public class IndexModel : PageModel
|
||||
}
|
||||
catch {}
|
||||
|
||||
int runCount = 14;
|
||||
int runCount = 11;
|
||||
return $"quant_{dateStr}.{runCount}.{gitHash}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" asp-page-handler="TriggerJob" class="d-inline">
|
||||
<form method="post" action="/Admin/Operations?handler=TriggerJob" class="d-inline">
|
||||
@Html.AntiForgeryToken()
|
||||
<input type="hidden" name="jobId" value="@job.JobId" />
|
||||
<button type="submit" class="btn btn-sm btn-success text-white">즉시 실행</button>
|
||||
|
||||
@@ -87,18 +87,26 @@ public class IndexModel : PageModel
|
||||
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));
|
||||
.Select(kv => {
|
||||
var succeededAt = kv.Value.SucceededAt ?? DateTime.UtcNow;
|
||||
var durationMs = kv.Value.TotalDuration ?? 0;
|
||||
var startedAt = succeededAt.AddMilliseconds(-durationMs);
|
||||
return new JobExecutionInfo(
|
||||
kv.Value.Job?.Method.Name ?? kv.Key,
|
||||
startedAt,
|
||||
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));
|
||||
.Select(kv => {
|
||||
var failedAt = kv.Value.FailedAt ?? DateTime.UtcNow;
|
||||
return new JobExecutionInfo(
|
||||
kv.Value.Job?.Method.Name ?? kv.Key,
|
||||
failedAt,
|
||||
failedAt,
|
||||
false);
|
||||
});
|
||||
|
||||
RecentExecutions = succeeded.Concat(failed)
|
||||
.OrderByDescending(e => e.StartedAt)
|
||||
@@ -133,7 +141,7 @@ public class IndexModel : PageModel
|
||||
private static string DescribeCron(string cron) => cron switch
|
||||
{
|
||||
"0 9 * * *" => "매일 09:00",
|
||||
"0 9-15 * * 1-5" => "평일 09-15시 매시",
|
||||
"0 9,11,13,15 * * 1-5" => "평일 2시간 단위 (09:00~15:00)",
|
||||
"0 17 * * 5" => "매주 금요일 17:00",
|
||||
"0 2 1 * *" => "매월 1일 02:00",
|
||||
_ => cron,
|
||||
|
||||
@@ -53,11 +53,11 @@ public class SchedulerService
|
||||
new RecurringJobOptions { TimeZone = TimeZoneInfo.Local }
|
||||
);
|
||||
|
||||
// Hourly price update (during market hours 9 AM - 4 PM)
|
||||
// Hourly price update (during market hours 9 AM - 4 PM, every 2 hours)
|
||||
_recurringJobManager.AddOrUpdate(
|
||||
"hourly-price-update",
|
||||
() => UpdatePricesAsync(),
|
||||
"0 9-15 * * 1-5", // Every hour, 9 AM to 3 PM, Mon-Fri
|
||||
"0 9,11,13,15 * * 1-5", // 9:00, 11:00, 13:00, 15:00 on Mon-Fri
|
||||
new RecurringJobOptions { TimeZone = TimeZoneInfo.Local }
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user