Skip to content
Merged
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
39 changes: 22 additions & 17 deletions src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public ResourceNotificationService(ILogger<ResourceNotificationService> logger,
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_serviceProvider = new NullServiceProvider();
_resourceLoggerService = new ResourceLoggerService();
DefaultWaitBehavior = WaitBehavior.StopOnDependencyFailure;
DefaultWaitBehavior = WaitBehavior.StopOnResourceUnavailable;
}

/// <summary>
Expand All @@ -69,7 +69,7 @@ public ResourceNotificationService(
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_serviceProvider = serviceProvider;
_resourceLoggerService = resourceLoggerService ?? throw new ArgumentNullException(nameof(resourceLoggerService));
DefaultWaitBehavior = serviceProvider.GetService<IOptions<ResourceNotificationServiceOptions>>()?.Value.DefaultWaitBehavior ?? WaitBehavior.StopOnDependencyFailure;
DefaultWaitBehavior = serviceProvider.GetService<IOptions<ResourceNotificationServiceOptions>>()?.Value.DefaultWaitBehavior ?? WaitBehavior.StopOnResourceUnavailable;

// The IHostApplicationLifetime parameter is not used anymore, but we keep it for backwards compatibility.
// Notification updates will be cancelled when the service is disposed.
Expand Down Expand Up @@ -161,7 +161,7 @@ async Task Core(string displayName, string resourceId)
var resourceEvent = await WaitForResourceCoreAsync(dependency.Name, re => re.ResourceId == resourceId && IsContinuableState(waitBehavior, re.Snapshot), cancellationToken: cancellationToken).ConfigureAwait(false);
var snapshot = resourceEvent.Snapshot;

if (waitBehavior == WaitBehavior.StopOnDependencyFailure)
if (waitBehavior == WaitBehavior.StopOnResourceUnavailable)
{
if (snapshot.State?.Text == KnownResourceStates.FailedToStart)
{
Expand Down Expand Up @@ -208,8 +208,8 @@ async Task Core(string displayName, string resourceId)
static bool IsContinuableState(WaitBehavior waitBehavior, CustomResourceSnapshot snapshot) =>
waitBehavior switch
{
WaitBehavior.WaitOnDependencyFailure => snapshot.State?.Text == KnownResourceStates.Running,
WaitBehavior.StopOnDependencyFailure => snapshot.State?.Text == KnownResourceStates.Running ||
WaitBehavior.WaitOnResourceUnavailable => snapshot.State?.Text == KnownResourceStates.Running,
WaitBehavior.StopOnResourceUnavailable => snapshot.State?.Text == KnownResourceStates.Running ||
snapshot.State?.Text == KnownResourceStates.Finished ||
snapshot.State?.Text == KnownResourceStates.Exited ||
snapshot.State?.Text == KnownResourceStates.FailedToStart ||
Expand All @@ -233,33 +233,25 @@ public async Task<ResourceEvent> WaitForResourceHealthyAsync(string resourceName
{
return await WaitForResourceHealthyAsync(
resourceName,
WaitBehavior.WaitOnDependencyFailure, // Retain default behavior.
WaitBehavior.WaitOnResourceUnavailable, // Retain default behavior.
cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Waits for a resource to become healthy.
/// </summary>
/// <param name="resourceName">The name of the resource.</param>
/// <param name="waitBehavior">The behavior to use when waiting for the resource to become healthy.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <param name="waitBehavior">The wait behavior.</param>
/// <returns>A task.</returns>
/// <remarks>
/// This method returns a task that will complete with the resource is healthy. A resource
/// without <see cref="HealthCheckAnnotation"/> annotations will be considered healthy. This overload
/// will throw a <see cref="Aspire.Hosting.DistributedApplicationException"/> if the resource fails to start.
/// without <see cref="HealthCheckAnnotation"/> annotations will be considered healthy.
/// </remarks>
public async Task<ResourceEvent> WaitForResourceHealthyAsync(string resourceName, WaitBehavior waitBehavior, CancellationToken cancellationToken = default)
{
var waitCondition = waitBehavior switch
{
WaitBehavior.WaitOnDependencyFailure => (Func<ResourceEvent, bool>)(re => re.Snapshot.HealthStatus == HealthStatus.Healthy),
WaitBehavior.StopOnDependencyFailure => (Func<ResourceEvent, bool>)(re => re.Snapshot.HealthStatus == HealthStatus.Healthy || re.Snapshot.State?.Text == KnownResourceStates.FailedToStart),
_ => throw new DistributedApplicationException($"Unexpected wait behavior: {waitBehavior}")
};

_logger.LogDebug("Waiting for resource '{Name}' to enter the '{State}' state.", resourceName, HealthStatus.Healthy);
var resourceEvent = await WaitForResourceCoreAsync(resourceName, waitCondition, cancellationToken: cancellationToken).ConfigureAwait(false);
var resourceEvent = await WaitForResourceCoreAsync(resourceName, re => ShouldYield(waitBehavior, re.Snapshot), cancellationToken: cancellationToken).ConfigureAwait(false);

if (resourceEvent.Snapshot.HealthStatus != HealthStatus.Healthy)
{
Expand All @@ -270,6 +262,19 @@ public async Task<ResourceEvent> WaitForResourceHealthyAsync(string resourceName
_logger.LogDebug("Finished waiting for resource '{Name}'.", resourceName);

return resourceEvent;

// Determine if we should yield based on the wait behavior and the snapshot of the resource.
static bool ShouldYield(WaitBehavior waitBehavior, CustomResourceSnapshot snapshot) =>
waitBehavior switch
{
WaitBehavior.WaitOnResourceUnavailable => snapshot.HealthStatus == HealthStatus.Healthy,
WaitBehavior.StopOnResourceUnavailable => snapshot.HealthStatus == HealthStatus.Healthy ||
snapshot.State?.Text == KnownResourceStates.Finished ||
snapshot.State?.Text == KnownResourceStates.Exited ||
snapshot.State?.Text == KnownResourceStates.FailedToStart ||
snapshot.State?.Text == KnownResourceStates.RuntimeUnhealthy,
_ => throw new DistributedApplicationException($"Unexpected wait behavior: {waitBehavior}")
};
}

private async Task WaitUntilCompletionAsync(IResource resource, IResource dependency, int exitCode, CancellationToken cancellationToken)
Expand Down
8 changes: 4 additions & 4 deletions src/Aspire.Hosting/ApplicationModel/WaitAnnotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ public enum WaitType
public enum WaitBehavior
{
/// <summary>
/// If the dependency fails, ignore the failure and continue waiting.
/// If the resource is unavailable, continue waiting.
/// </summary>
WaitOnDependencyFailure,
WaitOnResourceUnavailable,

/// <summary>
/// If the dependency fails, stop waiting and fail the wait.
/// If the resource is unavailable, stop waiting.
/// </summary>
StopOnDependencyFailure
StopOnResourceUnavailable
}
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
{
// Default to stopping on dependency failure if the dashboard is disabled. As there's no way to see or easily recover
// from a failure in that case.
o.DefaultWaitBehavior = options.DisableDashboard ? WaitBehavior.StopOnDependencyFailure : WaitBehavior.WaitOnDependencyFailure;
o.DefaultWaitBehavior = options.DisableDashboard ? WaitBehavior.StopOnResourceUnavailable : WaitBehavior.WaitOnResourceUnavailable;
});

ConfigureHealthChecks();
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ public static IResourceBuilder<T> WaitFor<T>(this IResourceBuilder<T> builder, I
/// var messaging = builder.AddRabbitMQ("messaging");
/// builder.AddProject&lt;Projects.MyApp&gt;("myapp")
/// .WithReference(messaging)
/// .WaitFor(messaging, WaitBehavior.StopOnDependencyFailure);
/// .WaitFor(messaging, WaitBehavior.StopOnResourceUnavailable);
/// </code>
/// </example>
public static IResourceBuilder<T> WaitFor<T>(this IResourceBuilder<T> builder, IResourceBuilder<IResource> dependency, WaitBehavior waitBehavior) where T : IResourceWithWaitSupport
Expand Down
24 changes: 12 additions & 12 deletions tests/Aspire.Hosting.Tests/WaitForTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,14 +168,14 @@ await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s w
[InlineData(nameof(KnownResourceStates.RuntimeUnhealthy))]
[InlineData(nameof(KnownResourceStates.Finished))]
[RequiresDocker]
public async Task WaitForBehaviorStopOnDependencyFailure(string status)
public async Task WaitForBehaviorStopOnResourceUnavailable(string status)
{
using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);

var dependency = builder.AddResource(new CustomResource("test"));
var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
.WithReference(dependency)
.WaitFor(dependency, WaitBehavior.StopOnDependencyFailure);
.WaitFor(dependency, WaitBehavior.StopOnResourceUnavailable);

using var app = builder.Build();

Expand All @@ -197,45 +197,45 @@ await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s w
}

[Fact]
public async Task WhenWaitBehaviorIsStopOnDependencyFailureWaitForResourceHealthyAsyncShouldThrowWhenResourceFailsToStart()
public async Task WhenWaitBehaviorIsStopOnResourceUnavailableWaitForResourceHealthyAsyncShouldThrowWhenResourceFailsToStart()
{
using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);

var failToStart = builder.AddExecutable("failToStart", "does-not-exist", ".");
var dependency = builder.AddContainer("redis", "redis");

dependency.WaitFor(failToStart, WaitBehavior.StopOnDependencyFailure);
dependency.WaitFor(failToStart, WaitBehavior.StopOnResourceUnavailable);

using var app = builder.Build();
await app.StartAsync();

var ex = await Assert.ThrowsAsync<DistributedApplicationException>(async () => {
await app.ResourceNotifications.WaitForResourceHealthyAsync(
dependency.Resource.Name,
WaitBehavior.StopOnDependencyFailure
WaitBehavior.StopOnResourceUnavailable
).WaitAsync(TimeSpan.FromSeconds(15));
});

Assert.Equal("Stopped waiting for resource 'redis' to become healthy because it failed to start.", ex.Message);
}

[Fact]
public async Task WhenWaitBehaviorIsWaitOnDependencyFailureWaitForResourceHealthyAsyncShouldThrowWhenResourceFailsToStart()
public async Task WhenWaitBehaviorIsWaitOnResourceUnavailableWaitForResourceHealthyAsyncShouldThrowWhenResourceFailsToStart()
{
using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);

var failToStart = builder.AddExecutable("failToStart", "does-not-exist", ".");
var dependency = builder.AddContainer("redis", "redis");

dependency.WaitFor(failToStart, WaitBehavior.StopOnDependencyFailure);
dependency.WaitFor(failToStart, WaitBehavior.StopOnResourceUnavailable);

using var app = builder.Build();
await app.StartAsync();

var ex = await Assert.ThrowsAsync<TimeoutException>(async () => {
await app.ResourceNotifications.WaitForResourceHealthyAsync(
dependency.Resource.Name,
WaitBehavior.WaitOnDependencyFailure
WaitBehavior.WaitOnResourceUnavailable
).WaitAsync(TimeSpan.FromSeconds(15));
});

Expand Down Expand Up @@ -282,14 +282,14 @@ await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s w
[InlineData(nameof(KnownResourceStates.RuntimeUnhealthy))]
[InlineData(nameof(KnownResourceStates.Finished))]
[RequiresDocker]
public async Task WaitForBehaviorWaitOnDependencyFailure(string status)
public async Task WaitForBehaviorWaitOnResourceUnavailable(string status)
{
using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);

var dependency = builder.AddResource(new CustomResource("test"));
var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
.WithReference(dependency)
.WaitFor(dependency, WaitBehavior.WaitOnDependencyFailure);
.WaitFor(dependency, WaitBehavior.WaitOnResourceUnavailable);

using var app = builder.Build();

Expand Down Expand Up @@ -324,13 +324,13 @@ await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s w
[InlineData(nameof(KnownResourceStates.RuntimeUnhealthy))]
[InlineData(nameof(KnownResourceStates.Finished))]
[RequiresDocker]
public async Task WaitForBehaviorWaitOnDependencyFailureViaOptions(string status)
public async Task WaitForBehaviorWaitOnResourceUnavailableViaOptions(string status)
{
using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);

builder.Services.Configure<ResourceNotificationServiceOptions>(o =>
{
o.DefaultWaitBehavior = WaitBehavior.WaitOnDependencyFailure;
o.DefaultWaitBehavior = WaitBehavior.WaitOnResourceUnavailable;
});

var dependency = builder.AddResource(new CustomResource("test"));
Expand Down