diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs index 98d75176d6e..13c1fc34090 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs @@ -231,8 +231,42 @@ static bool IsContinuableState(WaitBehavior waitBehavior, CustomResourceSnapshot /// public async Task WaitForResourceHealthyAsync(string resourceName, CancellationToken cancellationToken = default) { + return await WaitForResourceHealthyAsync( + resourceName, + WaitBehavior.WaitOnDependencyFailure, // Retain default behavior. + cancellationToken).ConfigureAwait(false); + } + + /// + /// Waits for a resource to become healthy. + /// + /// The name of the resource. + /// The behavior to use when waiting for the resource to become healthy. + /// The cancellation token. + /// A task. + /// + /// This method returns a task that will complete with the resource is healthy. A resource + /// without annotations will be considered healthy. This overload + /// will throw a if the resource fails to start. + /// + public async Task WaitForResourceHealthyAsync(string resourceName, WaitBehavior waitBehavior, CancellationToken cancellationToken = default) + { + var waitCondition = waitBehavior switch + { + WaitBehavior.WaitOnDependencyFailure => (Func)(re => re.Snapshot.HealthStatus == HealthStatus.Healthy), + WaitBehavior.StopOnDependencyFailure => (Func)(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, re => re.Snapshot.HealthStatus == HealthStatus.Healthy, cancellationToken: cancellationToken).ConfigureAwait(false); + var resourceEvent = await WaitForResourceCoreAsync(resourceName, waitCondition, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (resourceEvent.Snapshot.HealthStatus != HealthStatus.Healthy) + { + _logger.LogError("Stopped waiting for resource '{ResourceName}' to become healthy because it failed to start.", resourceName); + throw new DistributedApplicationException($"Stopped waiting for resource '{resourceName}' to become healthy because it failed to start."); + } + _logger.LogDebug("Finished waiting for resource '{Name}'.", resourceName); return resourceEvent; diff --git a/tests/Aspire.Hosting.Tests/WaitForTests.cs b/tests/Aspire.Hosting.Tests/WaitForTests.cs index 3bf0eca6f2d..52509398858 100644 --- a/tests/Aspire.Hosting.Tests/WaitForTests.cs +++ b/tests/Aspire.Hosting.Tests/WaitForTests.cs @@ -196,6 +196,52 @@ await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s w await startTask; } + [Fact] + public async Task WhenWaitBehaviorIsStopOnDependencyFailureWaitForResourceHealthyAsyncShouldThrowWhenResourceFailsToStart() + { + 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); + + using var app = builder.Build(); + await app.StartAsync(); + + var ex = await Assert.ThrowsAsync(async () => { + await app.ResourceNotifications.WaitForResourceHealthyAsync( + dependency.Resource.Name, + WaitBehavior.StopOnDependencyFailure + ).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() + { + 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); + + using var app = builder.Build(); + await app.StartAsync(); + + var ex = await Assert.ThrowsAsync(async () => { + await app.ResourceNotifications.WaitForResourceHealthyAsync( + dependency.Resource.Name, + WaitBehavior.WaitOnDependencyFailure + ).WaitAsync(TimeSpan.FromSeconds(15)); + }); + + Assert.Equal("The operation has timed out.", ex.Message); + } + [Theory] [InlineData(nameof(KnownResourceStates.Exited))] [InlineData(nameof(KnownResourceStates.FailedToStart))]