cd54c84cc2
Implements all Phase 2-3 infrastructure tasks per AGENTS.md v16.0: Task #3: OpenDart Daily Batch API (225 LOC) - OpenDartService: 3-month caching + idempotent batch processing - OpenDartDailyBatchJob: Recurring job 09:00 KST daily - Quota tracking (1000/day limit with audit trail) Task #4: KIS Connection Pool (250 LOC) - Manages 3-5 concurrent connections with OAuth2 token refresh - Priority queue: BUY > SELL > CANCEL - 55-min token refresh interval, no connection leaks Task #5: Central Rate Limiter (220 LOC) - Token bucket pattern for KRX/OpenDart/KIS - Per-API quotas: KRX 100/min, OpenDart 1000/day, KIS 50/sec - Atomic token consumption, HTTP 429 with Retry-After Task #6: Circuit Breaker Pattern (190 LOC) - Polly integration with 3-strike failure rule - 5-minute auto-recovery window - Failure classification: transient/permanent/dq Task #7: Gate 5 Observability Dashboard (300 LOC) - GET /api/observability/metrics endpoint - 5 KPI metrics: Batch SLA, DQ Quarantine, Duplicates, Reconciliation, Model Drift - PIT queries with published_at <= cutoff pattern Code Quality (AGENTS.md compliance): ✅ No SELECT *, schema-qualified queries with explicit columns ✅ Idempotent operations (token refresh, batch jobs, rate limit resets) ✅ Atomic state transitions (no partial success) ✅ Structured logging with correlation IDs ✅ Build: 0 errors, 0 warnings, 1185 LOC total Gate 3 Shadow Run endpoint 404 tracked separately pending root cause analysis. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
64 lines
2.1 KiB
C#
64 lines
2.1 KiB
C#
using FastEndpoints;
|
|
using KArtSell.BuildingBlocks.Time;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace KArtSell.Host.Features.ShadowRun;
|
|
|
|
/// <summary>
|
|
/// POST /api/shadow-runs
|
|
/// Initiate a 252+ trading-day model validation run.
|
|
/// Returns 202 Accepted with job tracking info.
|
|
/// </summary>
|
|
public class InitiateShadowRunEndpoint : Endpoint<InitiateShadowRunRequest, InitiateShadowRunResponse>
|
|
{
|
|
private InitiateShadowRunHandler? _handler;
|
|
private ILogger<InitiateShadowRunEndpoint>? _logger;
|
|
|
|
private static readonly Action<ILogger, Guid, Exception?> LogRequestReceived =
|
|
LoggerMessage.Define<Guid>(
|
|
LogLevel.Information,
|
|
new EventId(1, nameof(LogRequestReceived)),
|
|
"Shadow run request received: {ModelId}");
|
|
|
|
private static readonly Action<ILogger, Exception?> LogRequestFailed =
|
|
LoggerMessage.Define(
|
|
LogLevel.Error,
|
|
new EventId(2, nameof(LogRequestFailed)),
|
|
"Shadow run initiation failed");
|
|
|
|
public override void Configure()
|
|
{
|
|
Post("/api/shadow-runs");
|
|
Roles("Admin", "Researcher"); // RBAC: Only Admin or Researcher can initiate
|
|
Validator<InitiateShadowRunValidator>();
|
|
}
|
|
|
|
public override async Task HandleAsync(InitiateShadowRunRequest req, CancellationToken ct)
|
|
{
|
|
_handler = Resolve<InitiateShadowRunHandler>();
|
|
_logger = Resolve<ILogger<InitiateShadowRunEndpoint>>();
|
|
|
|
var correlationId = HttpContext.Items["CorrelationId"] as Guid? ?? Guid.NewGuid();
|
|
|
|
try
|
|
{
|
|
LogRequestReceived(_logger, req.ModelId, null);
|
|
|
|
var response = await _handler.HandleAsync(req, correlationId, ct);
|
|
|
|
// 202 Accepted: Job queued, results available later via polling
|
|
HttpContext.Response.StatusCode = 202;
|
|
await HttpContext.Response.WriteAsJsonAsync(response, ct);
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
ThrowError(ex.Message, 400);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogRequestFailed(_logger, ex);
|
|
ThrowError("Shadow run initiation failed. Please retry.", 500);
|
|
}
|
|
}
|
|
}
|