247 lines
9.1 KiB
C#
247 lines
9.1 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// REST API endpoints for job DAG (dependency graph).
|
|
/// </summary>
|
|
public static class DagEndpoints
|
|
{
|
|
/// <summary>
|
|
/// Maps DAG endpoints to the route builder.
|
|
/// </summary>
|
|
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<IResult> 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<Guid> 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<IResult> 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<IResult> 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<IResult> 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<IResult> 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<IResult> 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 });
|
|
}
|
|
}
|
|
}
|