Skip to content

Commit 5c7f20d

Browse files
github-actions[bot]mitchdennyCopilot
authored
[release/9.1] Add WaitBehavior to WaitForResourceHealthyAsync (#7664)
* Add overload for WaitForResourceHealthyAsync with customizable wait behavior * Update src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Mitch Denny <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 3961bed commit 5c7f20d

File tree

2 files changed

+81
-1
lines changed

2 files changed

+81
-1
lines changed

src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,8 +231,42 @@ static bool IsContinuableState(WaitBehavior waitBehavior, CustomResourceSnapshot
231231
/// </remarks>
232232
public async Task<ResourceEvent> WaitForResourceHealthyAsync(string resourceName, CancellationToken cancellationToken = default)
233233
{
234+
return await WaitForResourceHealthyAsync(
235+
resourceName,
236+
WaitBehavior.WaitOnDependencyFailure, // Retain default behavior.
237+
cancellationToken).ConfigureAwait(false);
238+
}
239+
240+
/// <summary>
241+
/// Waits for a resource to become healthy.
242+
/// </summary>
243+
/// <param name="resourceName">The name of the resource.</param>
244+
/// <param name="waitBehavior">The behavior to use when waiting for the resource to become healthy.</param>
245+
/// <param name="cancellationToken">The cancellation token.</param>
246+
/// <returns>A task.</returns>
247+
/// <remarks>
248+
/// This method returns a task that will complete with the resource is healthy. A resource
249+
/// without <see cref="HealthCheckAnnotation"/> annotations will be considered healthy. This overload
250+
/// will throw a <see cref="Aspire.Hosting.DistributedApplicationException"/> if the resource fails to start.
251+
/// </remarks>
252+
public async Task<ResourceEvent> WaitForResourceHealthyAsync(string resourceName, WaitBehavior waitBehavior, CancellationToken cancellationToken = default)
253+
{
254+
var waitCondition = waitBehavior switch
255+
{
256+
WaitBehavior.WaitOnDependencyFailure => (Func<ResourceEvent, bool>)(re => re.Snapshot.HealthStatus == HealthStatus.Healthy),
257+
WaitBehavior.StopOnDependencyFailure => (Func<ResourceEvent, bool>)(re => re.Snapshot.HealthStatus == HealthStatus.Healthy || re.Snapshot.State?.Text == KnownResourceStates.FailedToStart),
258+
_ => throw new DistributedApplicationException($"Unexpected wait behavior: {waitBehavior}")
259+
};
260+
234261
_logger.LogDebug("Waiting for resource '{Name}' to enter the '{State}' state.", resourceName, HealthStatus.Healthy);
235-
var resourceEvent = await WaitForResourceCoreAsync(resourceName, re => re.Snapshot.HealthStatus == HealthStatus.Healthy, cancellationToken: cancellationToken).ConfigureAwait(false);
262+
var resourceEvent = await WaitForResourceCoreAsync(resourceName, waitCondition, cancellationToken: cancellationToken).ConfigureAwait(false);
263+
264+
if (resourceEvent.Snapshot.HealthStatus != HealthStatus.Healthy)
265+
{
266+
_logger.LogError("Stopped waiting for resource '{ResourceName}' to become healthy because it failed to start.", resourceName);
267+
throw new DistributedApplicationException($"Stopped waiting for resource '{resourceName}' to become healthy because it failed to start.");
268+
}
269+
236270
_logger.LogDebug("Finished waiting for resource '{Name}'.", resourceName);
237271

238272
return resourceEvent;

tests/Aspire.Hosting.Tests/WaitForTests.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,52 @@ await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s w
196196
await startTask;
197197
}
198198

199+
[Fact]
200+
public async Task WhenWaitBehaviorIsStopOnDependencyFailureWaitForResourceHealthyAsyncShouldThrowWhenResourceFailsToStart()
201+
{
202+
using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
203+
204+
var failToStart = builder.AddExecutable("failToStart", "does-not-exist", ".");
205+
var dependency = builder.AddContainer("redis", "redis");
206+
207+
dependency.WaitFor(failToStart, WaitBehavior.StopOnDependencyFailure);
208+
209+
using var app = builder.Build();
210+
await app.StartAsync();
211+
212+
var ex = await Assert.ThrowsAsync<DistributedApplicationException>(async () => {
213+
await app.ResourceNotifications.WaitForResourceHealthyAsync(
214+
dependency.Resource.Name,
215+
WaitBehavior.StopOnDependencyFailure
216+
).WaitAsync(TimeSpan.FromSeconds(15));
217+
});
218+
219+
Assert.Equal("Stopped waiting for resource 'redis' to become healthy because it failed to start.", ex.Message);
220+
}
221+
222+
[Fact]
223+
public async Task WhenWaitBehaviorIsWaitOnDependencyFailureWaitForResourceHealthyAsyncShouldThrowWhenResourceFailsToStart()
224+
{
225+
using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
226+
227+
var failToStart = builder.AddExecutable("failToStart", "does-not-exist", ".");
228+
var dependency = builder.AddContainer("redis", "redis");
229+
230+
dependency.WaitFor(failToStart, WaitBehavior.StopOnDependencyFailure);
231+
232+
using var app = builder.Build();
233+
await app.StartAsync();
234+
235+
var ex = await Assert.ThrowsAsync<TimeoutException>(async () => {
236+
await app.ResourceNotifications.WaitForResourceHealthyAsync(
237+
dependency.Resource.Name,
238+
WaitBehavior.WaitOnDependencyFailure
239+
).WaitAsync(TimeSpan.FromSeconds(15));
240+
});
241+
242+
Assert.Equal("The operation has timed out.", ex.Message);
243+
}
244+
199245
[Theory]
200246
[InlineData(nameof(KnownResourceStates.Exited))]
201247
[InlineData(nameof(KnownResourceStates.FailedToStart))]

0 commit comments

Comments
 (0)