feat(dotnet): add collection bootstrap hosted service
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace QuantEngine.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Lightweight startup bootstrap for collection scheduling/readiness.
|
||||||
|
/// Writes a deterministic artifact so deployment can verify the collection
|
||||||
|
/// pipeline entry point without forcing a live collection run.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CollectionBootstrapHostedService : IHostedService
|
||||||
|
{
|
||||||
|
private readonly ILogger<CollectionBootstrapHostedService> _logger;
|
||||||
|
private readonly GatherTradingDataParser _parser;
|
||||||
|
|
||||||
|
public CollectionBootstrapHostedService(
|
||||||
|
ILogger<CollectionBootstrapHostedService> logger,
|
||||||
|
GatherTradingDataParser parser)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_parser = parser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var repoRoot = FindRepoRoot();
|
||||||
|
var outputPath = Path.Combine(repoRoot, "Temp", "collection_bootstrap_v1.json");
|
||||||
|
var tickers = LoadBootstrapTickers();
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
|
||||||
|
File.WriteAllText(outputPath, JsonSerializer.Serialize(new
|
||||||
|
{
|
||||||
|
gate = "PASS",
|
||||||
|
generated_at_utc = DateTimeOffset.UtcNow,
|
||||||
|
bootstrap = "collection-scheduling-ready",
|
||||||
|
ticker_count = tickers.Count,
|
||||||
|
tickers
|
||||||
|
}, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
|
||||||
|
_logger.LogInformation("Collection bootstrap artifact written to {Path}", outputPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Collection bootstrap artifact generation failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
private List<string> LoadBootstrapTickers()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var jsonPath = FindGatherTradingDataJson();
|
||||||
|
if (jsonPath is null)
|
||||||
|
{
|
||||||
|
return ["005930"];
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = _parser.ParseGatherTradingData(jsonPath);
|
||||||
|
return data
|
||||||
|
.Select(row => row.TryGetValue("Ticker", out var value) ? value?.ToString()?.Trim('"') : null)
|
||||||
|
.Where(ticker => !string.IsNullOrWhiteSpace(ticker))
|
||||||
|
.Distinct()
|
||||||
|
.Take(10)
|
||||||
|
.ToList()!;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return ["005930"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindRepoRoot()
|
||||||
|
{
|
||||||
|
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (current != null)
|
||||||
|
{
|
||||||
|
if (Directory.Exists(Path.Combine(current.FullName, ".git")))
|
||||||
|
{
|
||||||
|
return current.FullName;
|
||||||
|
}
|
||||||
|
current = current.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Directory.GetCurrentDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FindGatherTradingDataJson()
|
||||||
|
{
|
||||||
|
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (current != null)
|
||||||
|
{
|
||||||
|
var candidate = Path.Combine(current.FullName, "GatherTradingData.json");
|
||||||
|
if (File.Exists(candidate))
|
||||||
|
{
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Moq;
|
||||||
|
using QuantEngine.Application.Services;
|
||||||
|
|
||||||
|
namespace QuantEngine.Core.Tests;
|
||||||
|
|
||||||
|
public class CollectionBootstrapHostedServiceTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task StartAsync_WritesBootstrapArtifact()
|
||||||
|
{
|
||||||
|
var root = FindRepoRoot();
|
||||||
|
var artifact = Path.Combine(root, "Temp", "collection_bootstrap_v1.json");
|
||||||
|
if (File.Exists(artifact))
|
||||||
|
{
|
||||||
|
File.Delete(artifact);
|
||||||
|
}
|
||||||
|
|
||||||
|
var service = new CollectionBootstrapHostedService(
|
||||||
|
new Mock<ILogger<CollectionBootstrapHostedService>>().Object,
|
||||||
|
new GatherTradingDataParser());
|
||||||
|
|
||||||
|
await service.StartAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.True(File.Exists(artifact));
|
||||||
|
var text = await File.ReadAllTextAsync(artifact);
|
||||||
|
Assert.Contains("\"gate\": \"PASS\"", text);
|
||||||
|
Assert.Contains("\"bootstrap\": \"collection-scheduling-ready\"", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindRepoRoot()
|
||||||
|
{
|
||||||
|
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (current != null)
|
||||||
|
{
|
||||||
|
if (Directory.Exists(Path.Combine(current.FullName, ".git")))
|
||||||
|
{
|
||||||
|
return current.FullName;
|
||||||
|
}
|
||||||
|
current = current.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("Repository root not found.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -122,6 +122,7 @@ try
|
|||||||
builder.Services.AddScoped<ICollectionOrchestrator, KisDataCollectionOrchestrator>();
|
builder.Services.AddScoped<ICollectionOrchestrator, KisDataCollectionOrchestrator>();
|
||||||
builder.Services.AddScoped<IPriceHistoryReader, PriceHistoryReader>();
|
builder.Services.AddScoped<IPriceHistoryReader, PriceHistoryReader>();
|
||||||
builder.Services.AddOptions<SchedulerServiceOptions>();
|
builder.Services.AddOptions<SchedulerServiceOptions>();
|
||||||
|
builder.Services.AddHostedService<CollectionBootstrapHostedService>();
|
||||||
|
|
||||||
// Hangfire Background Jobs
|
// Hangfire Background Jobs
|
||||||
try
|
try
|
||||||
|
|||||||
Reference in New Issue
Block a user