Skip to content

Commit d3bd0e9

Browse files
authored
Wait for dashboard to be healthy before returning URL via RPC (revert-revert) (#9044)
* Revert "Revert "Wait for dashboard to be healthy before returning URL via RPC…" This reverts commit 935f06b. * Workaround. * Add comments.
1 parent fc618bc commit d3bd0e9

File tree

6 files changed

+61
-5
lines changed

6 files changed

+61
-5
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

+28-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Aspire.Hosting.Publishing;
1010
using Aspire.Hosting.Utils;
1111
using Microsoft.Extensions.DependencyInjection;
12+
using Microsoft.Extensions.Diagnostics.HealthChecks;
1213
using Microsoft.Extensions.Hosting;
1314
using Microsoft.Extensions.Logging;
1415
using Microsoft.Extensions.Options;
@@ -21,7 +22,8 @@ internal class AppHostRpcTarget(
2122
IServiceProvider serviceProvider,
2223
IDistributedApplicationEventing eventing,
2324
PublishingActivityProgressReporter activityReporter,
24-
IHostApplicationLifetime lifetime
25+
IHostApplicationLifetime lifetime,
26+
DistributedApplicationOptions options
2527
)
2628
{
2729
public async IAsyncEnumerable<(string Id, string StatusText, bool IsComplete, bool IsError)> GetPublishingActivitiesAsync([EnumeratorCancellation]CancellationToken cancellationToken)
@@ -101,6 +103,29 @@ public Task<long> PingAsync(long timestamp, CancellationToken cancellationToken)
101103

102104
public Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync()
103105
{
106+
return GetDashboardUrlsAsync(CancellationToken.None);
107+
}
108+
109+
public async Task<(string BaseUrlWithLoginToken, string? CodespacesUrlWithLoginToken)> GetDashboardUrlsAsync(CancellationToken cancellationToken)
110+
{
111+
if (!options.DashboardEnabled)
112+
{
113+
logger.LogError("Dashboard URL requested but dashboard is disabled.");
114+
throw new InvalidOperationException("Dashboard URL requested but dashboard is disabled.");
115+
}
116+
117+
// Wait for the dashboard to be healthy before returning the URL. This next statement has several
118+
// layers of hacks. Some to work around devcontainer/codespaces port forwarding behavior, and one to
119+
// temporarily work around the fact that resource events abuse the state to mark the resource as
120+
// hidden instead of having another field. There is a corresponding modification in the ResourceHealthService
121+
// which allows the dashboard resource to trigger health reports even though it never enters
122+
// the Running state. This is a hack. The reason we can't just check HealthStatus is because
123+
// the current implementation of HealthStatus depends on the state of the resource as well.
124+
await resourceNotificationService.WaitForResourceAsync(
125+
KnownResourceNames.AspireDashboard,
126+
re => re.Snapshot.HealthReports.All(h => h.Status == HealthStatus.Healthy),
127+
cancellationToken).ConfigureAwait(false);
128+
104129
var dashboardOptions = serviceProvider.GetService<IOptions<DashboardOptions>>();
105130

106131
if (dashboardOptions is null)
@@ -122,11 +147,11 @@ public Task<long> PingAsync(long timestamp, CancellationToken cancellationToken)
122147

123148
if (baseUrlWithLoginToken == codespacesUrlWithLoginToken)
124149
{
125-
return Task.FromResult<(string, string?)>((baseUrlWithLoginToken, null));
150+
return (baseUrlWithLoginToken, null);
126151
}
127152
else
128153
{
129-
return Task.FromResult((baseUrlWithLoginToken, codespacesUrlWithLoginToken));
154+
return (baseUrlWithLoginToken, codespacesUrlWithLoginToken);
130155
}
131156
}
132157

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/Aspire.Hosting/Health/ResourceHealthCheckService.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
3939
}
4040
}
4141

42-
if (resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running)
42+
// HACK: We are special casing the Aspire dashboard here until we address the issue of the Hidden state
43+
// making it impossible to determine whether a hidden resource is running or not. When that change
44+
// is made we can remove the special case logic here for the dashboard.
45+
if (resourceEvent.Snapshot.State?.Text == KnownResourceStates.Running || resourceEvent.Resource.Name == KnownResourceNames.AspireDashboard)
4346
{
4447
if (state == null)
4548
{

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)