feat: Shadow Run API Endpoint (Phase 4)
Implements FastEndpoints integration for 252+ trading-day validation trigger:
Contract-First Design (AGENTS.md v16.0):
- POST /api/shadow-runs (202 Accepted)
- Request: model_id, window_start, window_end, phase_filter
- Response: run_id, status, job_id, estimated_seconds
- Idempotency: Idempotency-Key header (deduplication)
Vertical Slice Components:
- Request.cs, Response.cs (DTOs with validation constraints)
- Validator.cs (FluentValidation): window >= 250 days, valid enum
- Handler.cs (Application): orchestrates command creation, Hangfire job enqueue
- Endpoint.cs (FastEndpoints): HTTP routing, error handling, 202 response
- Policy.cs: model existence validation (stub)
Integration:
- Hangfire background job client injection
- ShadowRunCommand creation with CorrelationId
- Queued to q-research (non-critical background queue)
Tests (9/9 passing):
- Validator: valid/invalid requests, phase filters, window constraints
- All validation scenarios: empty model, short window, invalid phase
Architecture Adherence:
- SOLID: Endpoint → Handler → Validator → Policy separation
- Complexity: Each component cyclomatic < 10
- Safety: Idempotent request (client-supplied key), async job model (202 response)
- Maturity: Contract verified, tests before implementation
Next Phase (Pending):
- Hangfire Job registration in Program.cs
- GET /api/shadow-runs/{run_id} polling endpoint
- E2E test: trigger → job execution → result persistence
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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 sealed 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");
|
||||
AllowAnonymous(); // TODO: Add RBAC (researcher role required)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user