Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion src/Aspire.Hosting/Pipelines/DistributedApplicationPipeline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ private static (List<PipelineStep> StepsToExecute, Dictionary<string, PipelineSt
var stepsToExecute = ComputeTransitiveDependencies(targetStep, allStepsByName);
stepsToExecute.Add(targetStep);
var filteredStepsByName = stepsToExecute.ToDictionary(s => s.Name, StringComparer.Ordinal);

// Mark steps that were filtered out as Skipped
foreach (var step in allSteps)
{
if (!filteredStepsByName.ContainsKey(step.Name))
{
step.TryTransitionStatus(PipelineStepStatus.Skipped);
}
}

return (stepsToExecute, filteredStepsByName);
}

Expand Down Expand Up @@ -354,13 +364,16 @@ async Task ExecuteStepWithDependencies(PipelineStep step)

// Wrap the dependency failure with context about this step
var wrappedException = new InvalidOperationException(message, ex);
step.TryTransitionStatus(PipelineStepStatus.Failed);
stepTcs.TrySetException(wrappedException);
return;
}
}

try
{
step.TryTransitionStatus(PipelineStepStatus.Running);

var activityReporter = context.Services.GetRequiredService<IPipelineActivityReporter>();
var publishingStep = await activityReporter.CreateStepAsync(step.Name, context.CancellationToken).ConfigureAwait(false);

Expand All @@ -377,9 +390,17 @@ async Task ExecuteStepWithDependencies(PipelineStep step)
PipelineLoggerProvider.CurrentLogger = stepContext.Logger;

await ExecuteStepAsync(step, stepContext).ConfigureAwait(false);

step.TryTransitionStatus(PipelineStepStatus.Succeeded);
}
catch (OperationCanceledException)
{
step.TryTransitionStatus(PipelineStepStatus.Canceled);
throw;
}
catch (Exception ex)
{
step.TryTransitionStatus(PipelineStepStatus.Failed);
// Report the failure to the activity reporter before disposing
await publishingStep.FailAsync(ex.Message, CancellationToken.None).ConfigureAwait(false);
throw;
Expand All @@ -394,7 +415,7 @@ async Task ExecuteStepWithDependencies(PipelineStep step)
}
catch (Exception ex)
{
// Execution failure - mark as failed, cancel all other work, and re-throw
// Execution failure - set exception and cancel all other work
stepTcs.TrySetException(ex);

// Cancel all remaining work
Expand Down Expand Up @@ -571,6 +592,11 @@ private static async Task ExecuteStepAsync(PipelineStep step, PipelineStepContex
{
await step.Action(stepContext).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Re-throw cancellation exceptions without wrapping
throw;
}
catch (Exception ex)
{
var exceptionInfo = ExceptionDispatchInfo.Capture(ex);
Expand Down
67 changes: 67 additions & 0 deletions src/Aspire.Hosting/Pipelines/PipelineStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ namespace Aspire.Hosting.Pipelines;
[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public class PipelineStep
{
private PipelineStepStatus _status = PipelineStepStatus.Pending;
private readonly object _statusLock = new object();

/// <summary>
/// Gets or initializes the unique name of the step.
/// </summary>
Expand All @@ -33,11 +36,75 @@ public class PipelineStep
/// </summary>
public List<string> RequiredBySteps { get; init; } = [];

/// <summary>
/// Gets the current execution status of the step.
/// </summary>
public PipelineStepStatus Status
{
get
{
lock (_statusLock)
{
return _status;
}
}
}

/// <summary>
/// Gets or initializes the list of tags that categorize this step.
/// </summary>
public List<string> Tags { get; init; } = [];

/// <summary>
/// Transitions the step to a new status, validating that the transition is valid.
/// </summary>
/// <param name="newStatus">The new status to transition to.</param>
/// <returns>True if the transition was successful, false if the transition was invalid.</returns>
/// <exception cref="InvalidOperationException">Thrown when an invalid state transition is attempted.</exception>
internal bool TryTransitionStatus(PipelineStepStatus newStatus)
{
lock (_statusLock)
{
// Validate the state transition
var isValid = IsValidTransition(_status, newStatus);
if (!isValid)
{
return false;
}

_status = newStatus;
return true;
}
}

/// <summary>
/// Determines if a transition from one status to another is valid.
/// </summary>
private static bool IsValidTransition(PipelineStepStatus current, PipelineStepStatus next)
{
// Allow any transition if we're already in a terminal state and trying to stay there
if (current == next)
{
return true;
}

return (current, next) switch
{
// From Pending, can go to Running, Failed (if dependency fails), or Skipped (if filtered out)
(PipelineStepStatus.Pending, PipelineStepStatus.Running) => true,
(PipelineStepStatus.Pending, PipelineStepStatus.Failed) => true,
(PipelineStepStatus.Pending, PipelineStepStatus.Skipped) => true,

// From Running, can go to any terminal state
(PipelineStepStatus.Running, PipelineStepStatus.Succeeded) => true,
(PipelineStepStatus.Running, PipelineStepStatus.Failed) => true,
(PipelineStepStatus.Running, PipelineStepStatus.Canceled) => true,

// Terminal states (Succeeded, Failed, Canceled, Skipped) cannot transition to other states
_ => false
};
}

/// <summary>
/// Adds a dependency on another step.
/// </summary>
Expand Down
43 changes: 43 additions & 0 deletions src/Aspire.Hosting/Pipelines/PipelineStepStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics.CodeAnalysis;

namespace Aspire.Hosting.Pipelines;

/// <summary>
/// Represents the execution status of a pipeline step.
/// </summary>
[Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public enum PipelineStepStatus
{
/// <summary>
/// The step is waiting to start.
/// </summary>
Pending,

/// <summary>
/// The step is currently executing.
/// </summary>
Running,

/// <summary>
/// The step completed successfully.
/// </summary>
Succeeded,

/// <summary>
/// The step failed during execution.
/// </summary>
Failed,

/// <summary>
/// The step was canceled before completion.
/// </summary>
Canceled,

/// <summary>
/// The step was skipped and not executed.
/// </summary>
Skipped
}
Loading
Loading