File: Runtime\WorkloadExecutionPlan.cs
Web Access
Project: src\src\Aspire.Hosting.Native.Core\Aspire.Hosting.Native.Core.csproj (Aspire.Hosting.Native.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Hosting.Native.Model;
 
namespace Aspire.Hosting.Native.Runtime;
 
internal enum ExecutionMilestone { Allocated, Configured, Start, Ready }
 
/// <summary>Reports a prerequisite cycle using validated resource names and execution milestones only.</summary>
internal sealed class ExecutionPlanException(string cycle) : InvalidOperationException($"Execution prerequisite cycle: {cycle}");
 
/// <summary>Validates operation prerequisites, allowing reference cycles that do not form execution cycles.</summary>
internal static class WorkloadExecutionPlan
{
    public static void Validate(IEnumerable<ResourceSnapshot> declarations, bool allocationBeforeStart)
    {
        var resources = declarations.ToDictionary(resource => resource.Handle.ResourceId);
        var graph = new Dictionary<Step, HashSet<Step>>();
        foreach (var resource in resources.Values)
        {
            var id = resource.Handle.ResourceId;
            var allocation = new Step(id, ExecutionMilestone.Allocated);
            var configuration = new Step(id, ExecutionMilestone.Configured);
            var start = new Step(id, ExecutionMilestone.Start);
            var ready = new Step(id, ExecutionMilestone.Ready);
            Add(allocation);
            Add(ready);
            if (resource.Workload is null)
            {
                // Externally owned resources publish these milestones through
                // their execution authority; the native scheduler does not start them.
                continue;
            }
            Add(configuration);
            Add(start, configuration);
            Add(ready, start);
            if (allocationBeforeStart)
            {
                Add(configuration, allocation);
            }
            else
            {
                Add(allocation, start);
            }
            foreach (var reference in resource.SymbolicConfiguration.SelectMany(entry => entry.Value.References).Distinct())
            {
                Add(configuration, new Step(reference.ResourceId, ExecutionMilestone.Allocated));
            }
            foreach (var dependency in resource.Dependencies)
            {
                Add(start, new Step(dependency.ResourceId, ExecutionMilestone.Ready));
            }
        }
 
        // Iterative DFS avoids stack growth with a long dependency chain. A gray
        // node is on the current path; encountering it gives the actual cycle.
        var colors = new Dictionary<Step, int>();
        var path = new List<Step>();
        foreach (var root in graph.Keys)
        {
            var pending = new Stack<(Step Step, bool Exit)>();
            pending.Push((root, false));
            while (pending.TryPop(out var visit))
            {
                if (visit.Exit)
                {
                    colors[visit.Step] = 2;
                    path.RemoveAt(path.Count - 1);
                    continue;
                }
                var color = colors.GetValueOrDefault(visit.Step);
                if (color == 2)
                {
                    continue;
                }
                if (color == 1)
                {
                    var cycle = path.Skip(path.IndexOf(visit.Step)).Append(visit.Step);
                    throw new ExecutionPlanException(string.Join(" -> ", cycle.Select(step =>
                        $"{resources[step.ResourceId].Name}.{step.Milestone}")));
                }
                colors[visit.Step] = 1;
                path.Add(visit.Step);
                pending.Push((visit.Step, true));
                foreach (var dependency in graph[visit.Step])
                {
                    pending.Push((dependency, false));
                }
            }
        }
 
        void Add(Step step, params Step[] prerequisites)
        {
            if (!graph.TryGetValue(step, out var dependencies))
            {
                graph.Add(step, dependencies = []);
            }
            dependencies.UnionWith(prerequisites);
        }
    }
 
    private readonly record struct Step(Guid ResourceId, ExecutionMilestone Milestone);
}