Skip to content

Instantly share code, notes, and snippets.

@afscrome
Last active May 16, 2024 11:28
Show Gist options
  • Save afscrome/828bac82e30f2643478325e3355a9ce0 to your computer and use it in GitHub Desktop.
Save afscrome/828bac82e30f2643478325e3355a9ce0 to your computer and use it in GitHub Desktop.
Aspire Readiness checks
public class DelayStartAnnotation(IResource waitForResource) : IResourceAnnotation
{
public IResource WaitForResource { get; } = waitForResource;
}
using Aspire.Hosting.Lifecycle;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using System.Collections.Immutable;
public class DependencyDelayStartLifecycleHook(ResourceNotificationService resourceNotificationService) : IDistributedApplicationLifecycleHook
{
private readonly ResourceNotificationService _resourceNotificationService1 = resourceNotificationService;
public Task BeforeStartAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken)
{
foreach (var resource in appModel.Resources)
{
var waitOn = resource.Annotations
.OfType<DelayStartAnnotation>()
.Select(x => x.WaitForResource)
.ToArray();
if (waitOn.Any())
{
resource.Annotations.Add(new EnvironmentCallbackAnnotation(async context =>
{
if (context.ExecutionContext.IsPublishMode)
{
return;
}
await _resourceNotificationService1.PublishUpdateAsync(resource, x =>
{
return x with
{
State = new(
"Waiting",
KnownResourceStateStyles.Warn.ToString()
)
};
});
await WaitForDependencies(waitOn, context.Logger, context.CancellationToken);
}));
}
}
return Task.CompletedTask;
}
async Task WaitForDependencies(IResource[] dependsOn, ILogger? logger, CancellationToken cancellationToken)
{
var remainingReadinessChecks = dependsOn
.SelectMany(x => x.Annotations.OfType<ReadinessCheckAnnotation>())
.ToImmutableList();
int remainingAttempts = 30;
var delay = TimeSpan.FromSeconds(1);
logger?.LogInformation("Waiting for dependencies to be ready");
while (remainingReadinessChecks.Count > 0 && remainingAttempts > 0)
{
await Task.Delay(delay, cancellationToken);
foreach (var check in remainingReadinessChecks)
{
var result = await check.HealthCheck.CheckHealthAsync(new HealthCheckContext(), cancellationToken);
if (result.Status == HealthStatus.Healthy)
{
var healthCheckName = "TODO - ???pull from HealthCheckRegistration???";
logger?.LogDebug("{Dependency} is ready", healthCheckName);
remainingReadinessChecks = remainingReadinessChecks.Remove(check);
}
}
remainingAttempts--;
}
if (remainingReadinessChecks.Count > 0)
{
//TODO: describe what checks failed
throw new Exception("Readiness checks failed");
}
}
}
using Microsoft.Extensions.Diagnostics.HealthChecks;
public class ReadinessCheckAnnotation(IHealthCheck healthCheck) : IResourceAnnotation
{
public IHealthCheck HealthCheck { get; } = healthCheck;
}
using Microsoft.Extensions.Diagnostics.HealthChecks;
public static class IResourceBuilderExtensions
{
public static IResourceBuilder<T> WithReadinessCheck<T>(this IResourceBuilder<T> builder, IHealthCheck healthCheck)
where T : IResource
=> builder.WithAnnotation(new ReadinessCheckAnnotation(healthCheck));
// TODO: This is POC code that needs refactoring
// - Refactor to a more central service so if two or more things depend on the same
// dependency, they don't need to run their own copy of health checks
// - Improve logging
public static IResourceBuilder<T> WithDependencyReadinessWait<T>(this IResourceBuilder<T> builder, IResource resource)
where T : IResourceWithEnvironment
=> builder.WithAnnotation(new DelayStartAnnotation(resource));
}
@afscrome
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment