Skip to content

Commit

Permalink
Fix flaky ResourceHealthCheckService test. (#6730)
Browse files Browse the repository at this point in the history
Introduce time provider to the resource health check service and use channels for sampling loop time.
  • Loading branch information
mitchdenny authored Nov 22, 2024
1 parent db291de commit 3bd8a75
Show file tree
Hide file tree
Showing 4 changed files with 24 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
LogBuilderConstructing(options, innerBuilderOptions);
_innerBuilder = new HostApplicationBuilder(innerBuilderOptions);

_innerBuilder.Services.AddSingleton(TimeProvider.System);

_innerBuilder.Logging.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Warning);
_innerBuilder.Logging.AddFilter("Microsoft.AspNetCore.Server.Kestrel", LogLevel.Error);
_innerBuilder.Logging.AddFilter("Aspire.Hosting.Dashboard", LogLevel.Error);
Expand Down
8 changes: 4 additions & 4 deletions src/Aspire.Hosting/Health/ResourceHealthCheckService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

namespace Aspire.Hosting.Health;

internal class ResourceHealthCheckService(ILogger<ResourceHealthCheckService> logger, ResourceNotificationService resourceNotificationService, HealthCheckService healthCheckService, IServiceProvider services, IDistributedApplicationEventing eventing) : BackgroundService
internal class ResourceHealthCheckService(ILogger<ResourceHealthCheckService> logger, ResourceNotificationService resourceNotificationService, HealthCheckService healthCheckService, IServiceProvider services, IDistributedApplicationEventing eventing, TimeProvider timeProvider) : BackgroundService
{
private readonly Dictionary<string, ResourceEvent> _latestEvents = new();

Expand Down Expand Up @@ -62,7 +62,7 @@ await eventing.PublishAsync(

var registrationKeysToCheck = annotations.DistinctBy(a => a.Key).Select(a => a.Key).ToFrozenSet();

using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5));
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(5), timeProvider);

do
{
Expand Down Expand Up @@ -144,11 +144,11 @@ await resourceNotificationService.PublishUpdateAsync(resource, s =>

async Task SlowDownMonitoringAsync(ResourceEvent lastEvent, CancellationToken cancellationToken)
{
var releaseAfter = DateTime.Now.AddSeconds(30);
var releaseAfter = timeProvider.GetUtcNow().AddSeconds(30);

// If we've waited for 30 seconds, or we received an updated event, or the health status is no longer
// healthy then we stop slowing down the monitoring loop.
while (DateTime.Now < releaseAfter && _latestEvents[lastEvent.Resource.Name] == lastEvent && lastEvent.Snapshot.HealthStatus == HealthStatus.Healthy)
while (timeProvider.GetUtcNow() < releaseAfter && _latestEvents[lastEvent.Resource.Name] == lastEvent && lastEvent.Snapshot.HealthStatus == HealthStatus.Healthy)
{
await Task.Delay(1000, cancellationToken).ConfigureAwait(false);
}
Expand Down
3 changes: 2 additions & 1 deletion tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>$(DefaultTargetFramework)</TargetFramework>
Expand All @@ -19,6 +19,7 @@

<PackageReference Include="Microsoft.Extensions.Http.Resilience" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.Testing" />
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />

<Compile Include="$(TestsSharedDir)Logging\*.cs" LinkBase="shared/Logging" />
<Compile Include="$(TestsSharedDir)ConsoleLogging\*.cs" LinkBase="shared" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Threading.Channels;
using Aspire.Hosting.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Time.Testing;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -129,12 +131,14 @@ public async Task HealthCheckIntervalDoesNotSlowBeforeSteadyHealthyState()
{
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);

AutoResetEvent? are = null;
var channel = Channel.CreateUnbounded<DateTimeOffset>();

builder.Services.AddHealthChecks().AddCheck("resource_check", () =>
{
are?.Set();
var timeProvider = new FakeTimeProvider(DateTimeOffset.Now);

builder.Services.AddSingleton<TimeProvider>(timeProvider);
builder.Services.AddHealthChecks().AddAsyncCheck("resource_check", async () =>
{
await channel.Writer.WriteAsync(timeProvider.GetUtcNow());
return HealthCheckResult.Unhealthy();
});

Expand All @@ -154,21 +158,18 @@ await rns.PublishUpdateAsync(resource.Resource, s => s with
}).DefaultTimeout();
await rns.WaitForResourceAsync(resource.Resource.Name, KnownResourceStates.Running, abortTokenSource.Token).DefaultTimeout();

are = new AutoResetEvent(false);
var firstCheck = await channel.Reader.ReadAsync(abortTokenSource.Token).DefaultTimeout();
timeProvider.Advance(TimeSpan.FromSeconds(5));

// Allow one event to through since it could be half way through.
are.WaitOne();

var stopwatch = Stopwatch.StartNew();
are.WaitOne();
stopwatch.Stop();
var secondCheck = await channel.Reader.ReadAsync(abortTokenSource.Token).DefaultTimeout();
timeProvider.Advance(TimeSpan.FromSeconds(5));

// When not in a healthy state the delay should be ~3 seconds but
// we'll check for 10 seconds to make sure we haven't got down
// the 30 second slow path.
Assert.True(stopwatch.ElapsedMilliseconds < 10000);
var thirdCheck = await channel.Reader.ReadAsync(abortTokenSource.Token).DefaultTimeout();

await app.StopAsync(abortTokenSource.Token).DefaultTimeout();

var duration = thirdCheck - firstCheck;
Assert.Equal(TimeSpan.FromSeconds(10), duration);
}

[Fact]
Expand Down

0 comments on commit 3bd8a75

Please sign in to comment.