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 });
}
}
}