diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs index 13c1fc34090..5c7ec7a63c8 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs @@ -50,7 +50,7 @@ public ResourceNotificationService(ILogger logger, _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _serviceProvider = new NullServiceProvider(); _resourceLoggerService = new ResourceLoggerService(); - DefaultWaitBehavior = WaitBehavior.StopOnDependencyFailure; + DefaultWaitBehavior = WaitBehavior.StopOnResourceUnavailable; } /// @@ -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>()?.Value.DefaultWaitBehavior ?? WaitBehavior.StopOnDependencyFailure; + DefaultWaitBehavior = serviceProvider.GetService>()?.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. @@ -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) { @@ -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 || @@ -233,7 +233,7 @@ public async Task WaitForResourceHealthyAsync(string resourceName { return await WaitForResourceHealthyAsync( resourceName, - WaitBehavior.WaitOnDependencyFailure, // Retain default behavior. + WaitBehavior.WaitOnResourceUnavailable, // Retain default behavior. cancellationToken).ConfigureAwait(false); } @@ -241,25 +241,17 @@ public async Task WaitForResourceHealthyAsync(string resourceName /// 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. + /// The wait behavior. /// 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. + /// without annotations will be considered healthy. /// 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, 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) { @@ -270,6 +262,19 @@ public async Task 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) diff --git a/src/Aspire.Hosting/ApplicationModel/WaitAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/WaitAnnotation.cs index 74da4d5d8cc..708b5dc344f 100644 --- a/src/Aspire.Hosting/ApplicationModel/WaitAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/WaitAnnotation.cs @@ -60,12 +60,12 @@ public enum WaitType public enum WaitBehavior { /// - /// If the dependency fails, ignore the failure and continue waiting. + /// If the resource is unavailable, continue waiting. /// - WaitOnDependencyFailure, + WaitOnResourceUnavailable, /// - /// If the dependency fails, stop waiting and fail the wait. + /// If the resource is unavailable, stop waiting. /// - StopOnDependencyFailure + StopOnResourceUnavailable } diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index f3d340f34e9..3c0b26e1c65 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -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(); diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index caae0ea5cef..0eba7720036 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -738,7 +738,7 @@ public static IResourceBuilder WaitFor(this IResourceBuilder builder, I /// var messaging = builder.AddRabbitMQ("messaging"); /// builder.AddProject<Projects.MyApp>("myapp") /// .WithReference(messaging) - /// .WaitFor(messaging, WaitBehavior.StopOnDependencyFailure); + /// .WaitFor(messaging, WaitBehavior.StopOnResourceUnavailable); /// /// public static IResourceBuilder WaitFor(this IResourceBuilder builder, IResourceBuilder dependency, WaitBehavior waitBehavior) where T : IResourceWithWaitSupport diff --git a/tests/Aspire.Hosting.Tests/WaitForTests.cs b/tests/Aspire.Hosting.Tests/WaitForTests.cs index 52509398858..9a5a6ca72ca 100644 --- a/tests/Aspire.Hosting.Tests/WaitForTests.cs +++ b/tests/Aspire.Hosting.Tests/WaitForTests.cs @@ -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(); @@ -197,14 +197,14 @@ 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(); @@ -212,7 +212,7 @@ public async Task WhenWaitBehaviorIsStopOnDependencyFailureWaitForResourceHealth var ex = await Assert.ThrowsAsync(async () => { await app.ResourceNotifications.WaitForResourceHealthyAsync( dependency.Resource.Name, - WaitBehavior.StopOnDependencyFailure + WaitBehavior.StopOnResourceUnavailable ).WaitAsync(TimeSpan.FromSeconds(15)); }); @@ -220,14 +220,14 @@ await app.ResourceNotifications.WaitForResourceHealthyAsync( } [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(); @@ -235,7 +235,7 @@ public async Task WhenWaitBehaviorIsWaitOnDependencyFailureWaitForResourceHealth var ex = await Assert.ThrowsAsync(async () => { await app.ResourceNotifications.WaitForResourceHealthyAsync( dependency.Resource.Name, - WaitBehavior.WaitOnDependencyFailure + WaitBehavior.WaitOnResourceUnavailable ).WaitAsync(TimeSpan.FromSeconds(15)); }); @@ -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(); @@ -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(o => { - o.DefaultWaitBehavior = WaitBehavior.WaitOnDependencyFailure; + o.DefaultWaitBehavior = WaitBehavior.WaitOnResourceUnavailable; }); var dependency = builder.AddResource(new CustomResource("test"));