diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index 9b8aab5051c..020dd030364 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -2006,31 +2006,42 @@ internal async Task StartResourceAsync(string resourceName, CancellationToken ca async Task StartExecutableOrContainerAsync(T resource) where T : CustomResource { + var resourceNotFound = false; try { - // Note that DeleteAsync returns before the resource is completely deleted. await kubernetesService.DeleteAsync(resource.Metadata.Name, cancellationToken: cancellationToken).ConfigureAwait(false); } catch (HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound) { // No-op if the resource wasn't found. // This could happen in a race condition, e.g. double clicking start button. + resourceNotFound = true; } - // Ensure resource is deleted. - // The resource must be properly deleted before it is started again because they share the same name. Poll to check. - for (var i = 0; i < 5; i++) + // Ensure resource is deleted. DeleteAsync returns before the resource is completely deleted so we must poll + // to discover when it is safe to recreate the resource. This is required because the resources share the same name. + if (!resourceNotFound) { - // Pause to give DCP a chance to finish deleting the resource. - await Task.Delay(100 * (i + 1), cancellationToken).ConfigureAwait(false); - - try + // Limit polling to 5 attempts to avoid hanging with an infinite loop. + for (var i = 0; i < 5; i++) { - await kubernetesService.GetAsync(resource.Metadata.Name, cancellationToken: cancellationToken).ConfigureAwait(false); + // Pause to give DCP a chance to finish deleting the resource. + await Task.Delay(100 * (i + 1), cancellationToken).ConfigureAwait(false); + + try + { + await kubernetesService.GetAsync(resource.Metadata.Name, cancellationToken: cancellationToken).ConfigureAwait(false); + } + catch (HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound) + { + resourceNotFound = true; + break; + } } - catch (HttpOperationException ex) when (ex.Response.StatusCode == System.Net.HttpStatusCode.NotFound) + + if (!resourceNotFound) { - break; + throw new InvalidOperationException($"Failed to successfully delete '{resource.Metadata.Name}' before restart."); } } diff --git a/src/Aspire.Hosting/Dcp/Model/ExecutableReplicaSet.cs b/src/Aspire.Hosting/Dcp/Model/ExecutableReplicaSet.cs index 9fc00325676..3f480ef9f5b 100644 --- a/src/Aspire.Hosting/Dcp/Model/ExecutableReplicaSet.cs +++ b/src/Aspire.Hosting/Dcp/Model/ExecutableReplicaSet.cs @@ -53,10 +53,6 @@ internal sealed class ExecutableReplicaSetSpec [JsonPropertyName("replicas")] public int Replicas { get; set; } = 1; - // Should the replica be soft deleted on scale down instead of deleted? - [JsonPropertyName("stopOnScaleDown")] - public bool? StopOnScaleDown { get; set; } - // Template describing the configuration of child Executable objects created by the replica set [JsonPropertyName("template")] public ExecutableTemplate Template { get; set; } = new ExecutableTemplate(); @@ -104,8 +100,7 @@ public static ExecutableReplicaSet Create(string name, int replicas, string exec var ers = new ExecutableReplicaSet(new ExecutableReplicaSetSpec { - Replicas = replicas, - StopOnScaleDown = true + Replicas = replicas }); ers.Kind = Dcp.ExecutableReplicaSetKind; ers.ApiVersion = Dcp.GroupVersion.ToString(); diff --git a/src/Aspire.Hosting/Health/ResourceNotificationHealthCheckPublisher.cs b/src/Aspire.Hosting/Health/ResourceNotificationHealthCheckPublisher.cs index b7b0c1f5414..7721e171e61 100644 --- a/src/Aspire.Hosting/Health/ResourceNotificationHealthCheckPublisher.cs +++ b/src/Aspire.Hosting/Health/ResourceNotificationHealthCheckPublisher.cs @@ -17,12 +17,10 @@ public async Task PublishAsync(HealthReport report, CancellationToken cancellati // Make sure every annotation is represented as health in the report, and if an entry is missing that means it is unhealthy. var status = annotations.All(a => report.Entries.TryGetValue(a.Key, out var entry) && entry.Status == HealthStatus.Healthy) ? HealthStatus.Healthy : HealthStatus.Unhealthy; - _ = resourceNotificationService; - await Task.Yield(); - //await resourceNotificationService.PublishUpdateAsync(resource, s => s with - //{ - // HealthStatus = status - //}).ConfigureAwait(false); + await resourceNotificationService.PublishUpdateAsync(resource, s => s with + { + HealthStatus = status + }).ConfigureAwait(false); } } } diff --git a/src/Aspire.Hosting/PublicAPI.Unshipped.txt b/src/Aspire.Hosting/PublicAPI.Unshipped.txt index f9e0b1ff34c..8d85da726fd 100644 --- a/src/Aspire.Hosting/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting/PublicAPI.Unshipped.txt @@ -142,6 +142,7 @@ Aspire.Hosting.DistributedApplicationExecutionContextOptions.ServiceProvider.get Aspire.Hosting.DistributedApplicationExecutionContextOptions.ServiceProvider.set -> void static Aspire.Hosting.ResourceBuilderExtensions.WaitFor(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder! dependency) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ResourceBuilderExtensions.WaitForCompletion(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, Aspire.Hosting.ApplicationModel.IResourceBuilder! dependency, int exitCode = 0) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static Aspire.Hosting.ResourceBuilderExtensions.WithCommand(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! type, string! displayName, System.Func! updateState, System.Func!>! executeCommand, string? iconName = null, bool isHighlighted = false) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ResourceBuilderExtensions.WithHealthCheck(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! key) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Exited -> string! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.FailedToStart -> string! diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 74698b7a680..3886d496507 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -771,16 +771,37 @@ public static IResourceBuilder WithHealthCheck(this IResourceBuilder bu return builder; } -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member -#pragma warning disable RS0016 // Add public types and members to the declared API + /// + /// Adds a to the resource annotations to add a resource command. + /// + /// The type of the resource. + /// The resource builder. + /// The type of command. The type uniquely identifies the command. + /// The display name visible in UI. + /// + /// A callback that is used to update the command state. + /// The callback is executed when the command's resource snapshot is updated. + /// + /// + /// A callback that is executed when the command is executed. The callback is run inside the .NET Aspire host. + /// The callback result is used to indicate success or failure in the UI. + /// + /// The icon name for the command. The name should be a valid FluentUI icon name. https://aka.ms/fluentui-system-icons + /// A flag indicating whether the command is highlighted in the UI. + /// The resource builder. + /// + /// The WithCommand method is used to add commands to the resource. Commands are displayed in the dashboard + /// and can be executed by a user using the dashboard UI. + /// When a command is executed, the callback is called and is run inside the .NET Aspire host. + /// public static IResourceBuilder WithCommand( this IResourceBuilder builder, string type, string displayName, Func updateState, Func> executeCommand, - string? iconName, - bool isHighlighted) where T : IResource + string? iconName = null, + bool isHighlighted = false) where T : IResource { // Replace existing annotation with the same name. var existingAnnotation = builder.Resource.Annotations.OfType().SingleOrDefault(a => a.Type == type); @@ -791,6 +812,4 @@ public static IResourceBuilder WithCommand( return builder.WithAnnotation(new ResourceCommandAnnotation(type, displayName, updateState, executeCommand, iconName, isHighlighted)); } -#pragma warning restore RS0016 // Add public types and members to the declared API -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member }