Skip to content

Commit 4e112fd

Browse files
Wait for dashboard to be healthy before returning URL via RPC. (#9027)
* Wait for dashboard to be healthy before returning URL via RPC. * Add health check to dashboard. * More files for health check. * Update src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs --------- Co-authored-by: David Fowler <[email protected]>
1 parent 5a83f39 commit 4e112fd

File tree

5 files changed

+52
-4
lines changed

5 files changed

+52
-4
lines changed

src/Aspire.Hosting/Aspire.Hosting.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<Compile Include="$(SharedDir)Model\KnownRelationshipTypes.cs" Link="Dashboard\KnownRelationshipTypes.cs" />
2222
<Compile Include="$(SharedDir)IConfigurationExtensions.cs" Link="Utils\IConfigurationExtensions.cs" />
2323
<Compile Include="$(SharedDir)KnownFormats.cs" Link="Utils\KnownFormats.cs" />
24+
<Compile Include="$(SharedDir)KnownHealthCheckNames.cs" Link="Utils\KnownHealthCheckNames.cs" />
2425
<Compile Include="$(SharedDir)KnownResourceNames.cs" Link="Utils\KnownResourceNames.cs" />
2526
<Compile Include="$(SharedDir)KnownConfigNames.cs" Link="Utils\KnownConfigNames.cs" />
2627
<Compile Include="$(SharedDir)PathNormalizer.cs" Link="Utils\PathNormalizer.cs" />

src/Aspire.Hosting/Backchannel/AppHostRpcTarget.cs

+23-3
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ internal class AppHostRpcTarget(
2121
IServiceProvider serviceProvider,
2222
IDistributedApplicationEventing eventing,
2323
PublishingActivityProgressReporter activityReporter,
24-
IHostApplicationLifetime lifetime
24+
IHostApplicationLifetime lifetime,
25+
DistributedApplicationOptions options
2526
)
2627
{
2728
public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken)
@@ -101,6 +102,25 @@ public Task<long> PingAsync(long timestamp, CancellationToken cancellationToken)
101102

102103
public Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync()
103104
{
105+
return GetDashboardUrlsAsync(CancellationToken.None);
106+
}
107+
108+
public async Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync(CancellationToken cancellationToken)
109+
{
110+
if (!options.DashboardEnabled)
111+
{
112+
logger.LogError("Dashboard URL requested but dashboard is disabled.");
113+
throw new InvalidOperationException("Dashboard URL requested but dashboard is disabled.");
114+
}
115+
116+
// Wait for the dashboard to be healthy before we return the URL. This is to avoid
117+
// a race condition when using Codespaces or devcontainers where the dashboard URL
118+
// is displayed before the dashboard port forwarding is actually configured. It is
119+
// also a point of friction to show the URL before the dashboard is ready to be used
120+
// when using Devcontainers/Codespaces because people think that something isn't working
121+
// when in fact they just need to refresh the page.
122+
await resourceNotificationService.WaitForResourceHealthyAsync(KnownResourceNames.AspireDashboard, cancellationToken).ConfigureAwait(false);
123+
104124
var dashboardOptions = serviceProvider.GetService<IOptions<DashboardOptions>>();
105125

106126
if (dashboardOptions is null)
@@ -122,11 +142,11 @@ public Task<long> PingAsync(long timestamp, CancellationToken cancellationToken)
122142

123143
if (baseUrlWithLoginToken == codespacesUrlWithLoginToken)
124144
{
125-
return Task.FromResult<(string, string?)>((baseUrlWithLoginToken, null));
145+
return (baseUrlWithLoginToken, null);
126146
}
127147
else
128148
{
129-
return Task.FromResult((baseUrlWithLoginToken, codespacesUrlWithLoginToken));
149+
return (baseUrlWithLoginToken, codespacesUrlWithLoginToken);
130150
}
131151
}
132152

src/Aspire.Hosting/Dashboard/DashboardLifecycleHook.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@ private void AddDashboardResource(DistributedApplicationModel model)
127127
nameGenerator.EnsureDcpInstancesPopulated(dashboardResource);
128128

129129
ConfigureAspireDashboardResource(dashboardResource);
130-
131130
// Make the dashboard first in the list so it starts as fast as possible.
132131
model.Resources.Insert(0, dashboardResource);
133132
}
@@ -179,6 +178,7 @@ private void ConfigureAspireDashboardResource(IResource dashboardResource)
179178
dashboardResource.Annotations.Add(new ResourceSnapshotAnnotation(snapshot));
180179

181180
dashboardResource.Annotations.Add(new EnvironmentCallbackAnnotation(ConfigureEnvironmentVariables));
181+
dashboardResource.Annotations.Add(new HealthCheckAnnotation(KnownHealthCheckNames.DasboardHealthCheck));
182182
}
183183

184184
internal async Task ConfigureEnvironmentVariables(EnvironmentCallbackContext context)

src/Aspire.Hosting/DistributedApplicationBuilder.cs

+15
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using Aspire.Hosting.Lifecycle;
2020
using Aspire.Hosting.Orchestrator;
2121
using Aspire.Hosting.Publishing;
22+
using Aspire.Hosting.Utils;
2223
using Microsoft.Extensions.Configuration;
2324
using Microsoft.Extensions.DependencyInjection;
2425
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -331,6 +332,20 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
331332
_innerBuilder.Services.AddLifecycleHook<DashboardLifecycleHook>();
332333
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<DashboardOptions>, ConfigureDefaultDashboardOptions>());
333334
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<DashboardOptions>, ValidateDashboardOptions>());
335+
336+
// Dashboard health check.
337+
_innerBuilder.Services.AddHealthChecks().AddUrlGroup(sp => {
338+
339+
var dashboardOptions = sp.GetRequiredService<IOptions<DashboardOptions>>().Value;
340+
if (StringUtils.TryGetUriFromDelimitedString(dashboardOptions.DashboardUrl, ";", out var firstDashboardUrl))
341+
{
342+
return firstDashboardUrl;
343+
}
344+
else
345+
{
346+
throw new DistributedApplicationException($"The dashboard resource '{KnownResourceNames.AspireDashboard}' does not have endpoints.");
347+
}
348+
}, KnownHealthCheckNames.DasboardHealthCheck);
334349
}
335350

336351
if (options.EnableResourceLogging)

src/Shared/KnownHealthCheckNames.cs

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Aspire;
5+
6+
internal static class KnownHealthCheckNames
7+
{
8+
/// <summary>
9+
/// Common name for dashboard health check.
10+
/// </summary>
11+
public const string DasboardHealthCheck = "aspire_dashboard_check";
12+
}

0 commit comments

Comments
 (0)