using Microsoft.AspNetCore.Mvc; using StellaOps.Auth.ServerIntegration.Tenancy; using StellaOps.JobEngine.Core.Scheduling; using StellaOps.JobEngine.Infrastructure.Repositories; using StellaOps.JobEngine.WebService.Contracts; using StellaOps.JobEngine.WebService.Services; using static StellaOps.Localization.T; namespace StellaOps.JobEngine.WebService.Endpoints; /// /// REST API endpoints for job DAG (dependency graph). /// public static class DagEndpoints { /// /// Maps DAG endpoints to the route builder. /// public static RouteGroupBuilder MapDagEndpoints(this IEndpointRouteBuilder app) { var group = app.MapGroup("/api/v1/jobengine/dag") .WithTags("Orchestrator DAG") .RequireAuthorization(JobEnginePolicies.Read) .RequireTenant(); group.MapGet("run/{runId:guid}", GetRunDag) .WithName("Orchestrator_GetRunDag") .WithDescription(_t("orchestrator.dag.get_run_description")); group.MapGet("run/{runId:guid}/edges", GetRunEdges) .WithName("Orchestrator_GetRunEdges") .WithDescription(_t("orchestrator.dag.get_run_edges_description")); group.MapGet("run/{runId:guid}/ready-jobs", GetReadyJobs) .WithName("Orchestrator_GetReadyJobs") .WithDescription(_t("orchestrator.dag.get_ready_jobs_description")); group.MapGet("run/{runId:guid}/blocked/{jobId:guid}", GetBlockedJobs) .WithName("Orchestrator_GetBlockedJobs") .WithDescription(_t("orchestrator.dag.get_blocked_jobs_description")); group.MapGet("job/{jobId:guid}/parents", GetJobParents) .WithName("Orchestrator_GetJobParents") .WithDescription(_t("orchestrator.dag.get_job_parents_description")); group.MapGet("job/{jobId:guid}/children", GetJobChildren) .WithName("Orchestrator_GetJobChildren") .WithDescription(_t("orchestrator.dag.get_job_children_description")); return group; } private static async Task GetRunDag( HttpContext context, [FromRoute] Guid runId, [FromServices] TenantResolver tenantResolver, [FromServices] IRunRepository runRepository, [FromServices] IJobRepository jobRepository, [FromServices] IDagEdgeRepository dagEdgeRepository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); // Verify run exists var run = await runRepository.GetByIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); if (run is null) { return Results.NotFound(); } // Get all edges var edges = await dagEdgeRepository.GetByRunIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); var edgeResponses = edges.Select(DagEdgeResponse.FromDomain).ToList(); // Get all jobs for topological sort and critical path var jobs = await jobRepository.GetByRunIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); // Compute topological order IReadOnlyList topologicalOrder; try { topologicalOrder = DagPlanner.TopologicalSort(jobs.Select(j => j.JobId), edges); } catch (InvalidOperationException) { // Cycle detected - return empty order topologicalOrder = []; } // Compute critical path (using a fixed estimate for simplicity) var criticalPath = DagPlanner.CalculateCriticalPath(jobs, edges, _ => TimeSpan.FromMinutes(5)); return Results.Ok(new DagResponse( runId, edgeResponses, topologicalOrder, criticalPath.CriticalPathJobIds, criticalPath.TotalDuration)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetRunEdges( HttpContext context, [FromRoute] Guid runId, [FromServices] TenantResolver tenantResolver, [FromServices] IRunRepository runRepository, [FromServices] IDagEdgeRepository dagEdgeRepository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); // Verify run exists var run = await runRepository.GetByIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); if (run is null) { return Results.NotFound(); } var edges = await dagEdgeRepository.GetByRunIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); var responses = edges.Select(DagEdgeResponse.FromDomain).ToList(); return Results.Ok(new DagEdgeListResponse(responses)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetReadyJobs( HttpContext context, [FromRoute] Guid runId, [FromServices] TenantResolver tenantResolver, [FromServices] IRunRepository runRepository, [FromServices] IJobRepository jobRepository, [FromServices] IDagEdgeRepository dagEdgeRepository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); // Verify run exists var run = await runRepository.GetByIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); if (run is null) { return Results.NotFound(); } var jobs = await jobRepository.GetByRunIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); var edges = await dagEdgeRepository.GetByRunIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); var readyJobs = DagPlanner.GetReadyJobs(jobs, edges); var responses = readyJobs.Select(JobResponse.FromDomain).ToList(); return Results.Ok(new JobListResponse(responses, null)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetBlockedJobs( HttpContext context, [FromRoute] Guid runId, [FromRoute] Guid jobId, [FromServices] TenantResolver tenantResolver, [FromServices] IRunRepository runRepository, [FromServices] IDagEdgeRepository dagEdgeRepository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); // Verify run exists var run = await runRepository.GetByIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); if (run is null) { return Results.NotFound(); } var edges = await dagEdgeRepository.GetByRunIdAsync(tenantId, runId, cancellationToken).ConfigureAwait(false); var blockedJobs = DagPlanner.GetBlockedJobs(jobId, edges); return Results.Ok(new BlockedJobsResponse(jobId, blockedJobs.ToList())); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetJobParents( HttpContext context, [FromRoute] Guid jobId, [FromServices] TenantResolver tenantResolver, [FromServices] IDagEdgeRepository dagEdgeRepository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); var edges = await dagEdgeRepository.GetParentEdgesAsync(tenantId, jobId, cancellationToken).ConfigureAwait(false); var responses = edges.Select(DagEdgeResponse.FromDomain).ToList(); return Results.Ok(new DagEdgeListResponse(responses)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } private static async Task GetJobChildren( HttpContext context, [FromRoute] Guid jobId, [FromServices] TenantResolver tenantResolver, [FromServices] IDagEdgeRepository dagEdgeRepository, CancellationToken cancellationToken = default) { try { var tenantId = tenantResolver.Resolve(context); var edges = await dagEdgeRepository.GetChildEdgesAsync(tenantId, jobId, cancellationToken).ConfigureAwait(false); var responses = edges.Select(DagEdgeResponse.FromDomain).ToList(); return Results.Ok(new DagEdgeListResponse(responses)); } catch (InvalidOperationException ex) { return Results.BadRequest(new { error = ex.Message }); } } }