From 9ae6a64fc170133887b17668e47d9dfa20aae725 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 23 Aug 2024 12:58:46 +1000 Subject: [PATCH 01/11] WaitFor/WaitForCompletion implementation. --- .../TestShop/TestShop.AppHost/Program.cs | 12 +++-- .../StateColumnDisplay.razor | 2 +- .../Extensions/ResourceViewModelExtensions.cs | 6 +-- .../Model/KnownResourceState.cs | 5 +- .../CustomResourceSnapshot.cs | 5 ++ src/Aspire.Hosting/PublicAPI.Unshipped.txt | 3 ++ .../ResourceBuilderExtensions.cs | 48 +++++++++++++++++++ 7 files changed, 70 insertions(+), 11 deletions(-) diff --git a/playground/TestShop/TestShop.AppHost/Program.cs b/playground/TestShop/TestShop.AppHost/Program.cs index 8e3f5d256f2..e595a2e5bb1 100644 --- a/playground/TestShop/TestShop.AppHost/Program.cs +++ b/playground/TestShop/TestShop.AppHost/Program.cs @@ -15,9 +15,13 @@ }); #endif +var catalogDbApp = builder.AddProject("catalogdbapp") + .WithReference(catalogDb); + var catalogService = builder.AddProject("catalogservice") .WithReference(catalogDb) - .WithReplicas(2); + .WithReplicas(2) + .WaitForCompletion(catalogDbApp); var messaging = builder.AddRabbitMQ("messaging") .WithDataVolume() @@ -34,15 +38,13 @@ .WithReference(catalogService); builder.AddProject("orderprocessor", launchProfileName: "OrderProcessor") - .WithReference(messaging); + .WithReference(messaging) + .WaitFor(messaging); builder.AddProject("apigateway") .WithReference(basketService) .WithReference(catalogService); -builder.AddProject("catalogdbapp") - .WithReference(catalogDb); - #if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code diff --git a/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor b/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor index 8e4564af903..14b7672f88b 100644 --- a/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor +++ b/src/Aspire.Dashboard/Components/ResourcesGridColumns/StateColumnDisplay.razor @@ -32,7 +32,7 @@ Class="severity-icon" /> } } -else if (Resource.IsStartingOrBuilding()) +else if (Resource.IsStartingOrBuildingOrWaiting()) { string.IsNullOrEmpty(resource.State); diff --git a/src/Aspire.Dashboard/Model/KnownResourceState.cs b/src/Aspire.Dashboard/Model/KnownResourceState.cs index 3903364e0ab..cb62a66a89b 100644 --- a/src/Aspire.Dashboard/Model/KnownResourceState.cs +++ b/src/Aspire.Dashboard/Model/KnownResourceState.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. namespace Aspire.Dashboard.Model; @@ -11,5 +11,6 @@ public enum KnownResourceState Starting, Running, Building, - Hidden + Hidden, + Waiting } diff --git a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs index 6cde3ae94ee..c4d52224ae4 100644 --- a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs +++ b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs @@ -149,4 +149,9 @@ public static class KnownResourceStates /// The finished state. Useful for showing the resource has finished. /// public static readonly string Finished = nameof(Finished); + + /// + /// The waiting state. Useful for showing the resource is waiting for a dependency. + /// + public static readonly string Waiting = nameof(Waiting); } diff --git a/src/Aspire.Hosting/PublicAPI.Unshipped.txt b/src/Aspire.Hosting/PublicAPI.Unshipped.txt index 85e0d9002cd..47eb786c596 100644 --- a/src/Aspire.Hosting/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting/PublicAPI.Unshipped.txt @@ -49,6 +49,8 @@ Aspire.Hosting.DistributedApplicationExecutionContextOptions.DistributedApplicat Aspire.Hosting.DistributedApplicationExecutionContextOptions.Operation.get -> Aspire.Hosting.DistributedApplicationOperation Aspire.Hosting.DistributedApplicationExecutionContextOptions.ServiceProvider.get -> System.IServiceProvider? 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) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Exited -> string! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.FailedToStart -> string! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Finished -> string! @@ -72,3 +74,4 @@ static Aspire.Hosting.ContainerResourceBuilderExtensions.WithBuildArg(this As static Aspire.Hosting.ContainerResourceBuilderExtensions.WithBuildSecret(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.IResourceBuilder! value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! projectPath, System.Action! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Action! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Waiting -> string! diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 1dd8905f077..3e9309ba27b 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -4,6 +4,8 @@ using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Publishing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Aspire.Hosting; @@ -549,4 +551,50 @@ public static IResourceBuilder ExcludeFromManifest(this IResourceBuilder + /// Waits for the dependency resource to enter the Running state before starting the resource. + /// + /// The type of the resource. + /// The resource builder for the resource that will be waiting. + /// The resource builder for the dependency resource. + /// + public static IResourceBuilder WaitFor(this IResourceBuilder builder, IResourceBuilder dependency) where T : IResource + { + builder.ApplicationBuilder.Eventing.Subscribe(builder.Resource, async (e, ct) => + { + var rls = e.Services.GetRequiredService(); + var resourceLogger = rls.GetLogger(builder.Resource); + resourceLogger.LogInformation($"Waiting for resource '{dependency.Resource.Name}' to enter the '{KnownResourceStates.Running}' state."); + + var rns = e.Services.GetRequiredService(); + await rns.PublishUpdateAsync(builder.Resource, s => s with { State = KnownResourceStates.Waiting }).ConfigureAwait(false); + await rns.WaitForResourceAsync(dependency.Resource.Name, cancellationToken: ct).ConfigureAwait(false); + }); + + return builder; + } + + /// + /// Waits for the dependency resource to enter the Exited or Finished state before starting the resource. + /// + /// The type of the resource. + /// The resource builder for the resource that will be waiting. + /// The resource builder for the dependency resource. + /// + public static IResourceBuilder WaitForCompletion(this IResourceBuilder builder, IResourceBuilder dependency) where T : IResource + { + builder.ApplicationBuilder.Eventing.Subscribe(builder.Resource, async (e, ct) => + { + var rls = e.Services.GetRequiredService(); + var resourceLogger = rls.GetLogger(builder.Resource); + resourceLogger.LogInformation($"Waiting for resource '{dependency.Resource.Name}' to enter the 'Finished' state."); + + var rns = e.Services.GetRequiredService(); + await rns.PublishUpdateAsync(builder.Resource, s => s with { State = KnownResourceStates.Waiting }).ConfigureAwait(false); + await rns.WaitForResourceAsync(dependency.Resource.Name, targetState: "Finished", cancellationToken: ct).ConfigureAwait(false); + }); + + return builder; + } } From a42780b5cd52571f17cdc574bb9c7aff4a4a010f Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 23 Aug 2024 13:14:17 +1000 Subject: [PATCH 02/11] Better XML doc comments. --- .../ResourceBuilderExtensions.cs | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 3e9309ba27b..ad1de85c6f6 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -558,7 +558,21 @@ public static IResourceBuilder ExcludeFromManifest(this IResourceBuilderThe type of the resource. /// The resource builder for the resource that will be waiting. /// The resource builder for the dependency resource. - /// + /// The resource builder. + /// + /// This method is useful when a resource should wait until another has started running. This can help + /// reduce errors in logs during local development where dependency resources. + /// + /// + /// Start message queue before starting the worker service. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// var messaging = builder.AddRabbitMQ("messaging"); + /// builder.AddProject<Projects.MyApp>("myapp") + /// .WithReference(messaging) + /// .WaitFor(messaging); + /// + /// public static IResourceBuilder WaitFor(this IResourceBuilder builder, IResourceBuilder dependency) where T : IResource { builder.ApplicationBuilder.Eventing.Subscribe(builder.Resource, async (e, ct) => @@ -581,7 +595,25 @@ public static IResourceBuilder WaitFor(this IResourceBuilder builder, I /// The type of the resource. /// The resource builder for the resource that will be waiting. /// The resource builder for the dependency resource. - /// + /// The resource builder. + /// + /// This method is useful when a resource should wait until another has completed. A common usage pattern + /// would be to include a console application that initializes the database schema or performs other one off + /// initialization tasks. + /// Note that this method has no impact at deployment time and only works for local development. + /// + /// + /// Wait for database initialization app to complete running. + /// + /// var builder = DistributedApplication.CreateBuilder(args); + /// var pgsql = builder.AddPostgres("postgres"); + /// var dbprep = builder.AddProject<Projects.DbPrepApp>("dbprep") + /// .WithReference(pgsql); + /// builder.AddProject<Projects.DatabasePrepTool>("dbprep") + /// .WithReference(pgsql) + /// .WaitForCompletion(dbprep); + /// + /// public static IResourceBuilder WaitForCompletion(this IResourceBuilder builder, IResourceBuilder dependency) where T : IResource { builder.ApplicationBuilder.Eventing.Subscribe(builder.Resource, async (e, ct) => From cb52d52f2d2c4e52134788cf29e65bafd47b90c3 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 23 Aug 2024 16:04:13 +1000 Subject: [PATCH 03/11] Update src/Aspire.Hosting/ResourceBuilderExtensions.cs Co-authored-by: David Fowler --- src/Aspire.Hosting/ResourceBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index ad1de85c6f6..da7a894714a 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -579,7 +579,7 @@ public static IResourceBuilder WaitFor(this IResourceBuilder builder, I { var rls = e.Services.GetRequiredService(); var resourceLogger = rls.GetLogger(builder.Resource); - resourceLogger.LogInformation($"Waiting for resource '{dependency.Resource.Name}' to enter the '{KnownResourceStates.Running}' state."); + resourceLogger.LogInformation("Waiting for resource '{Name}' to enter the '{State}' state.", dependency.Resource.Name, KnownResourceStates.Running); var rns = e.Services.GetRequiredService(); await rns.PublishUpdateAsync(builder.Resource, s => s with { State = KnownResourceStates.Waiting }).ConfigureAwait(false); From a88cd1fe749f153b1c297afd0121e72e1af54a14 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 23 Aug 2024 16:55:34 +1000 Subject: [PATCH 04/11] Add test cases. --- tests/Aspire.Hosting.Tests/WaitForTests.cs | 97 ++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests/Aspire.Hosting.Tests/WaitForTests.cs diff --git a/tests/Aspire.Hosting.Tests/WaitForTests.cs b/tests/Aspire.Hosting.Tests/WaitForTests.cs new file mode 100644 index 00000000000..88fdeeddfae --- /dev/null +++ b/tests/Aspire.Hosting.Tests/WaitForTests.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Components.Common.Tests; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Hosting.Tests; + +public class WaitForTests +{ + [Fact] + [RequiresDocker] + public async Task EnsureDependentResourceMovesIntoWaitingState() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dependency = builder.AddResource(new CustomResource("test")); + var redis = builder.AddRedis("redis") + .WithReference(dependency) + .WaitFor(dependency); + + using var app = builder.Build(); + + // StartAsync will currently block until the dependency resource moves + // into a Running state, so rather than awaiting it we'll hold onto the + // task so we can inspect the state of the Redis resource which should + // be in a waiting state if everything is working correctly. + var startTask = app.StartAsync(); + + // We don't want to wait forever for Redis to move into a waiting state, + // it should be super quick, but we'll allow 10 seconds just in case the + // CI machine is chugging. + var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceAsync(redis.Resource.Name, "Waiting", waitingStateCts.Token); + + // Now that we know we successfully entered the Waiting state, we can swap + // the dependency into a running state which will unblock startup and + // we can continue executing. + await rns.PublishUpdateAsync(dependency.Resource, s => s with { State = KnownResourceStates.Running }); + + await startTask; + + await app.StopAsync(); + } + + [Fact] + [RequiresDocker] + public async Task EnsureDependentResourceMovesIntoWaitingStateUntilDependencyMovesToFinishedState() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dependency = builder.AddResource(new CustomResource("test")); + var redis = builder.AddRedis("redis") + .WithReference(dependency) + .WaitForCompletion(dependency); + + using var app = builder.Build(); + + // StartAsync will currently block until the dependency resource moves + // into a Finished state, so rather than awaiting it we'll hold onto the + // task so we can inspect the state of the Redis resource which should + // be in a waiting state if everything is working correctly. + var startTask = app.StartAsync(); + + // We don't want to wait forever for Redis to move into a waiting state, + // it should be super quick, but we'll allow 10 seconds just in case the + // CI machine is chugging. + var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceAsync(redis.Resource.Name, "Waiting", waitingStateCts.Token); + + // Now that we know we successfully entered the Waiting state, we can swap + // the dependency into a running state which will unblock startup and + // we can continue executing. + await rns.PublishUpdateAsync(dependency.Resource, s => s with { State = KnownResourceStates.Finished }); + + // This time we want to wait for Redis to move into a Running state to verify that + // it successfully started after we moved the dependency resource into the Finished, but + // we need to give it more time since we have to download the image in CI. + var runningStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + await rns.WaitForResourceAsync(redis.Resource.Name, KnownResourceStates.Running, runningStateCts.Token); + + await startTask; + + await app.StopAsync(); + } + + private sealed class CustomResource(string name) : Resource(name), IResourceWithConnectionString + { + public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"foo"); + } +} From bf58bb8bbea5119ea81f6f4d40022ebd5ddb546d Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Tue, 27 Aug 2024 22:01:59 +1000 Subject: [PATCH 05/11] WaitForCompletion --- playground/TestShop/CatalogDb/Program.cs | 1 - .../CustomResourceSnapshot.cs | 5 +++ .../ResourceNotificationService.cs | 32 ++++++++++++++++ src/Aspire.Hosting/PublicAPI.Unshipped.txt | 4 +- .../ResourceBuilderExtensions.cs | 37 +++++++++++++++++-- 5 files changed, 74 insertions(+), 5 deletions(-) diff --git a/playground/TestShop/CatalogDb/Program.cs b/playground/TestShop/CatalogDb/Program.cs index f2cd200790e..50cc70cc12c 100644 --- a/playground/TestShop/CatalogDb/Program.cs +++ b/playground/TestShop/CatalogDb/Program.cs @@ -18,4 +18,3 @@ app.MapDefaultEndpoints(); await app.RunAsync(); - diff --git a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs index c4d52224ae4..84d0e54d834 100644 --- a/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs +++ b/src/Aspire.Hosting/ApplicationModel/CustomResourceSnapshot.cs @@ -154,4 +154,9 @@ public static class KnownResourceStates /// The waiting state. Useful for showing the resource is waiting for a dependency. /// public static readonly string Waiting = nameof(Waiting); + + /// + /// List of terminal states. + /// + public static readonly string[] TerminalStates = [Finished, FailedToStart, Exited]; } diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs index 7091369cbaa..f0456b64279 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs @@ -103,6 +103,38 @@ public async Task WaitForResourceAsync(string resourceName, IEnumerable< throw new OperationCanceledException($"The operation was cancelled before the resource reached one of the target states: [{string.Join(", ", targetStates)}]"); } + /// + /// Waits for a resource to reach one of the specified states. See for common states. + /// + /// + /// This method returns a task that will complete when the resource reaches one of the specified target states. If the resource + /// is already in the target state, the method will return immediately.
+ /// If the resource doesn't reach one of the target states before is signaled, this method + /// will throw . + ///
+ /// The name of the resource. + /// A predicate which is evaluated for each for the selected resource. + /// A cancellation token that cancels the wait operation when signaled. + /// A representing the wait operation and which of the target states the resource reached. + [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", + Justification = "targetState(s) parameters are mutually exclusive.")] + public async Task WaitForResourceAsync(string resourceName, Func predicate, CancellationToken cancellationToken = default) + { + using var watchCts = CancellationTokenSource.CreateLinkedTokenSource(_applicationStopping, cancellationToken); + var watchToken = watchCts.Token; + await foreach (var resourceEvent in WatchAsync(watchToken).ConfigureAwait(false)) + { + if (string.Equals(resourceName, resourceEvent.Resource.Name, StringComparisons.ResourceName) + && resourceEvent.Snapshot.State?.Text is { Length: > 0 } statusText + && predicate(resourceEvent)) + { + return resourceEvent; + } + } + + throw new OperationCanceledException($"The operation was cancelled before the resource met the predicate condition."); + } + /// /// Watch for changes to the state for all resources. /// diff --git a/src/Aspire.Hosting/PublicAPI.Unshipped.txt b/src/Aspire.Hosting/PublicAPI.Unshipped.txt index 47eb786c596..782d494db05 100644 --- a/src/Aspire.Hosting/PublicAPI.Unshipped.txt +++ b/src/Aspire.Hosting/PublicAPI.Unshipped.txt @@ -16,6 +16,7 @@ Aspire.Hosting.ApplicationModel.BeforeStartEvent.BeforeStartEvent(System.IServic Aspire.Hosting.ApplicationModel.BeforeStartEvent.Model.get -> Aspire.Hosting.ApplicationModel.DistributedApplicationModel! Aspire.Hosting.ApplicationModel.BeforeStartEvent.Services.get -> System.IServiceProvider! Aspire.Hosting.ApplicationModel.ResourceNotificationService.ResourceNotificationService(Microsoft.Extensions.Logging.ILogger! logger, Microsoft.Extensions.Hosting.IHostApplicationLifetime! hostApplicationLifetime) -> void +Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForResourceAsync(string! resourceName, System.Func! predicate, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task! Aspire.Hosting.DistributedApplicationBuilder.Eventing.get -> Aspire.Hosting.Eventing.IDistributedApplicationEventing! Aspire.Hosting.Eventing.DistributedApplicationEventing Aspire.Hosting.Eventing.DistributedApplicationEventing.DistributedApplicationEventing() -> void @@ -50,7 +51,7 @@ Aspire.Hosting.DistributedApplicationExecutionContextOptions.Operation.get -> As Aspire.Hosting.DistributedApplicationExecutionContextOptions.ServiceProvider.get -> System.IServiceProvider? 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) -> 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 readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Exited -> string! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.FailedToStart -> string! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Finished -> string! @@ -74,4 +75,5 @@ static Aspire.Hosting.ContainerResourceBuilderExtensions.WithBuildArg(this As static Aspire.Hosting.ContainerResourceBuilderExtensions.WithBuildSecret(this Aspire.Hosting.ApplicationModel.IResourceBuilder! builder, string! name, Aspire.Hosting.ApplicationModel.IResourceBuilder! value) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, string! projectPath, System.Action! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! static Aspire.Hosting.ProjectResourceBuilderExtensions.AddProject(this Aspire.Hosting.IDistributedApplicationBuilder! builder, string! name, System.Action! configure) -> Aspire.Hosting.ApplicationModel.IResourceBuilder! +static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.TerminalStates -> string![]! static readonly Aspire.Hosting.ApplicationModel.KnownResourceStates.Waiting -> string! diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index da7a894714a..92087c5403e 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -595,6 +595,7 @@ public static IResourceBuilder WaitFor(this IResourceBuilder builder, I /// The type of the resource. /// The resource builder for the resource that will be waiting. /// The resource builder for the dependency resource. + /// The exit code which is interpretted as successful. /// The resource builder. /// /// This method is useful when a resource should wait until another has completed. A common usage pattern @@ -614,19 +615,49 @@ public static IResourceBuilder WaitFor(this IResourceBuilder builder, I /// .WaitForCompletion(dbprep); /// /// - public static IResourceBuilder WaitForCompletion(this IResourceBuilder builder, IResourceBuilder dependency) where T : IResource + public static IResourceBuilder WaitForCompletion(this IResourceBuilder builder, IResourceBuilder dependency, int exitCode = 0) where T : IResource { builder.ApplicationBuilder.Eventing.Subscribe(builder.Resource, async (e, ct) => { + // TODO: Decide how we want to interpret inconsistent results from replicas of projects. For now + // if we detect that the project is configured for replicas we will throw an exception. + if (dependency.Resource.Annotations.Any(a => a is ReplicaAnnotation ra && ra.Replicas > 1)) + { + throw new DistributedApplicationException("WaitForCompletion cannot be used with resources that have replicas."); + } + var rls = e.Services.GetRequiredService(); var resourceLogger = rls.GetLogger(builder.Resource); - resourceLogger.LogInformation($"Waiting for resource '{dependency.Resource.Name}' to enter the 'Finished' state."); + resourceLogger.LogInformation($"Waiting for resource '{dependency.Resource.Name}' to complete."); var rns = e.Services.GetRequiredService(); await rns.PublishUpdateAsync(builder.Resource, s => s with { State = KnownResourceStates.Waiting }).ConfigureAwait(false); - await rns.WaitForResourceAsync(dependency.Resource.Name, targetState: "Finished", cancellationToken: ct).ConfigureAwait(false); + var resourceEvent = await rns.WaitForResourceAsync(dependency.Resource.Name, re => IsKnownTerminalState(re.Snapshot), cancellationToken: ct).ConfigureAwait(false); + var snapshot = resourceEvent.Snapshot; + + if (snapshot.State == KnownResourceStates.FailedToStart) + { + throw new DistributedApplicationException("Dependency resource failed to start."); + } + else if ((snapshot.State!.Text == KnownResourceStates.Finished || snapshot.State!.Text == KnownResourceStates.Exited) && snapshot.ExitCode != exitCode) + { + resourceLogger.LogInformation( + "Resource '{ResourceName}' has entered the '{State}' state with exit code '{ExitCode}'", + dependency.Resource.Name, + snapshot.State.Text, + snapshot.ExitCode + ); + + throw new DistributedApplicationException( + $"Resource '{dependency.Resource.Name}' has entered the '{snapshot.State.Text}' state with exit code '{snapshot.ExitCode}'" + ); + } }); return builder; + + static bool IsKnownTerminalState(CustomResourceSnapshot snapshot) => + KnownResourceStates.TerminalStates.Contains(snapshot.State?.Text) && + snapshot.ExitCode is not null; } } From a80d501cf9c33e38f26357d37ad767fae97c33e4 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 28 Aug 2024 12:10:42 +1000 Subject: [PATCH 06/11] Add more tests. --- tests/Aspire.Hosting.Tests/WaitForTests.cs | 102 +++++++++++++++++++-- 1 file changed, 94 insertions(+), 8 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/WaitForTests.cs b/tests/Aspire.Hosting.Tests/WaitForTests.cs index 88fdeeddfae..8f82edff09a 100644 --- a/tests/Aspire.Hosting.Tests/WaitForTests.cs +++ b/tests/Aspire.Hosting.Tests/WaitForTests.cs @@ -30,9 +30,9 @@ public async Task EnsureDependentResourceMovesIntoWaitingState() var startTask = app.StartAsync(); // We don't want to wait forever for Redis to move into a waiting state, - // it should be super quick, but we'll allow 10 seconds just in case the - // CI machine is chugging. - var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + // it should be super quick, but we'll allow 60 seconds just in case the + // CI machine is chugging (also useful when collecting code coverage). + var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var rns = app.Services.GetRequiredService(); await rns.WaitForResourceAsync(redis.Resource.Name, "Waiting", waitingStateCts.Token); @@ -40,7 +40,9 @@ public async Task EnsureDependentResourceMovesIntoWaitingState() // Now that we know we successfully entered the Waiting state, we can swap // the dependency into a running state which will unblock startup and // we can continue executing. - await rns.PublishUpdateAsync(dependency.Resource, s => s with { State = KnownResourceStates.Running }); + await rns.PublishUpdateAsync(dependency.Resource, s => s with { + State = KnownResourceStates.Running + }); await startTask; @@ -67,9 +69,9 @@ public async Task EnsureDependentResourceMovesIntoWaitingStateUntilDependencyMov var startTask = app.StartAsync(); // We don't want to wait forever for Redis to move into a waiting state, - // it should be super quick, but we'll allow 10 seconds just in case the - // CI machine is chugging. - var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + // it should be super quick, but we'll allow 60 seconds just in case the + // CI machine is chugging (also useful when collecting code coverage). + var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var rns = app.Services.GetRequiredService(); await rns.WaitForResourceAsync(redis.Resource.Name, "Waiting", waitingStateCts.Token); @@ -77,7 +79,11 @@ public async Task EnsureDependentResourceMovesIntoWaitingStateUntilDependencyMov // Now that we know we successfully entered the Waiting state, we can swap // the dependency into a running state which will unblock startup and // we can continue executing. - await rns.PublishUpdateAsync(dependency.Resource, s => s with { State = KnownResourceStates.Finished }); + await rns.PublishUpdateAsync(dependency.Resource, s => s with + { + State = KnownResourceStates.Finished, + ExitCode = 0 + }); // This time we want to wait for Redis to move into a Running state to verify that // it successfully started after we moved the dependency resource into the Finished, but @@ -90,6 +96,86 @@ public async Task EnsureDependentResourceMovesIntoWaitingStateUntilDependencyMov await app.StopAsync(); } + [Fact] + [RequiresDocker] + public async Task EnsureDependencyResourceThatReturnsNonMatchingExitCodeResultsInDependentResourceFailingToStart() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dependency = builder.AddResource(new CustomResource("test")); + var redis = builder.AddRedis("redis") + .WithReference(dependency) + .WaitForCompletion(dependency, exitCode: 2); + + using var app = builder.Build(); + + // StartAsync will currently block until the dependency resource moves + // into a Finished state, so rather than awaiting it we'll hold onto the + // task so we can inspect the state of the Redis resource which should + // be in a waiting state if everything is working correctly. + var startTask = app.StartAsync(); + + // We don't want to wait forever for Redis to move into a waiting state, + // it should be super quick, but we'll allow 60 seconds just in case the + // CI machine is chugging (also useful when collecting code coverage). + var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceAsync(redis.Resource.Name, "Waiting", waitingStateCts.Token); + + // Now that we know we successfully entered the Waiting state, we can swap + // the dependency into a finished state which will unblock startup and + // we can continue executing. + await rns.PublishUpdateAsync(dependency.Resource, s => s with + { + State = KnownResourceStates.Finished, + ExitCode = 3 // Exit code does not match expected exit code above intentionally. + }); + + // This time we want to wait for Redis to move into a FailedToStart state to verify that + // it didn't start if the dependency resource didn't finish with the correct exit code. + var runningStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + await rns.WaitForResourceAsync(redis.Resource.Name, KnownResourceStates.FailedToStart, runningStateCts.Token); + + await startTask; + + await app.StopAsync(); + } + + [Fact] + [RequiresDocker] + public async Task DependencyWithGreaterThan1ReplicaAnnotationCausesDependentResourceToFailToStart() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dependency = builder.AddResource(new CustomResource("test")) + .WithAnnotation(new ReplicaAnnotation(2)); + + var redis = builder.AddRedis("redis") + .WithReference(dependency) + .WaitForCompletion(dependency); + + using var app = builder.Build(); + + // StartAsync will currently block until the dependency resource moves + // into a Finished state, so rather than awaiting it we'll hold onto the + // task so we can inspect the state of the Redis resource which should + // be in a waiting state if everything is working correctly. + var startTask = app.StartAsync(); + + // We don't want to wait forever for Redis to move into a waiting state, + // it should be super quick, but we'll allow 60 seconds just in case the + // CI machine is chugging (also useful when collecting code coverage). + var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceAsync(redis.Resource.Name, "FailedToStart", waitingStateCts.Token); + + await startTask; + + await app.StopAsync(); + } + private sealed class CustomResource(string name) : Resource(name), IResourceWithConnectionString { public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"foo"); From c96909914be230c7400d9ef7c779ea257814bfe3 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 28 Aug 2024 14:59:28 +1000 Subject: [PATCH 07/11] Test tweaks and PR feedback. --- src/Aspire.Hosting/ResourceBuilderExtensions.cs | 4 +--- tests/Aspire.Hosting.Tests/WaitForTests.cs | 12 ++++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 92087c5403e..dba8209d281 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -619,9 +619,7 @@ public static IResourceBuilder WaitForCompletion(this IResourceBuilder { builder.ApplicationBuilder.Eventing.Subscribe(builder.Resource, async (e, ct) => { - // TODO: Decide how we want to interpret inconsistent results from replicas of projects. For now - // if we detect that the project is configured for replicas we will throw an exception. - if (dependency.Resource.Annotations.Any(a => a is ReplicaAnnotation ra && ra.Replicas > 1)) + if (dependency.Resource.TryGetLastAnnotation(out var replicaAnnotation) && replicaAnnotation.Replicas > 1) { throw new DistributedApplicationException("WaitForCompletion cannot be used with resources that have replicas."); } diff --git a/tests/Aspire.Hosting.Tests/WaitForTests.cs b/tests/Aspire.Hosting.Tests/WaitForTests.cs index 8f82edff09a..cad3c728686 100644 --- a/tests/Aspire.Hosting.Tests/WaitForTests.cs +++ b/tests/Aspire.Hosting.Tests/WaitForTests.cs @@ -27,7 +27,8 @@ public async Task EnsureDependentResourceMovesIntoWaitingState() // into a Running state, so rather than awaiting it we'll hold onto the // task so we can inspect the state of the Redis resource which should // be in a waiting state if everything is working correctly. - var startTask = app.StartAsync(); + var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var startTask = app.StartAsync(startupCts.Token); // We don't want to wait forever for Redis to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the @@ -66,7 +67,8 @@ public async Task EnsureDependentResourceMovesIntoWaitingStateUntilDependencyMov // into a Finished state, so rather than awaiting it we'll hold onto the // task so we can inspect the state of the Redis resource which should // be in a waiting state if everything is working correctly. - var startTask = app.StartAsync(); + var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var startTask = app.StartAsync(startupCts.Token); // We don't want to wait forever for Redis to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the @@ -113,7 +115,8 @@ public async Task EnsureDependencyResourceThatReturnsNonMatchingExitCodeResultsI // into a Finished state, so rather than awaiting it we'll hold onto the // task so we can inspect the state of the Redis resource which should // be in a waiting state if everything is working correctly. - var startTask = app.StartAsync(); + var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var startTask = app.StartAsync(startupCts.Token); // We don't want to wait forever for Redis to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the @@ -161,7 +164,8 @@ public async Task DependencyWithGreaterThan1ReplicaAnnotationCausesDependentReso // into a Finished state, so rather than awaiting it we'll hold onto the // task so we can inspect the state of the Redis resource which should // be in a waiting state if everything is working correctly. - var startTask = app.StartAsync(); + var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var startTask = app.StartAsync(startupCts.Token); // We don't want to wait forever for Redis to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the From ab6668f975062b29b90e19728e2b38848025be82 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Wed, 28 Aug 2024 18:24:29 +1000 Subject: [PATCH 08/11] PR feedback. Make WaitFor cause a failure if the process exists or fails before going into a running state. --- .../ResourceBuilderExtensions.cs | 26 ++++++++- tests/Aspire.Hosting.Tests/WaitForTests.cs | 54 +++++++++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index dba8209d281..c2c52a863de 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -583,10 +583,34 @@ public static IResourceBuilder WaitFor(this IResourceBuilder builder, I var rns = e.Services.GetRequiredService(); await rns.PublishUpdateAsync(builder.Resource, s => s with { State = KnownResourceStates.Waiting }).ConfigureAwait(false); - await rns.WaitForResourceAsync(dependency.Resource.Name, cancellationToken: ct).ConfigureAwait(false); + var resourceEvent = await rns.WaitForResourceAsync(dependency.Resource.Name, re => IsContinuableState(re.Snapshot), cancellationToken: ct).ConfigureAwait(false); + var snapshot = resourceEvent.Snapshot; + + if (snapshot.State == KnownResourceStates.FailedToStart) + { + throw new DistributedApplicationException("Dependency resource failed to start."); + } + else if (snapshot.State!.Text == KnownResourceStates.Finished || snapshot.State!.Text == KnownResourceStates.Exited) + { + resourceLogger.LogInformation( + "Resource '{ResourceName}' has entered the '{State}' state prematurely.", + dependency.Resource.Name, + snapshot.State.Text + ); + + throw new DistributedApplicationException( + $"Resource '{dependency.Resource.Name}' has entered the '{snapshot.State.Text}' state prematurely." + ); + } }); return builder; + + static bool IsContinuableState(CustomResourceSnapshot snapshot) => + snapshot.State?.Text == KnownResourceStates.Running || + snapshot.State?.Text == KnownResourceStates.Finished || + snapshot.State?.Text == KnownResourceStates.Exited || + snapshot.State?.Text == KnownResourceStates.FailedToStart; } /// diff --git a/tests/Aspire.Hosting.Tests/WaitForTests.cs b/tests/Aspire.Hosting.Tests/WaitForTests.cs index cad3c728686..e1a0422410d 100644 --- a/tests/Aspire.Hosting.Tests/WaitForTests.cs +++ b/tests/Aspire.Hosting.Tests/WaitForTests.cs @@ -52,7 +52,7 @@ await rns.PublishUpdateAsync(dependency.Resource, s => s with { [Fact] [RequiresDocker] - public async Task EnsureDependentResourceMovesIntoWaitingStateUntilDependencyMovesToFinishedState() + public async Task WaitForCompletionWaitsForTerminalStateOfDependencyResource() { using var builder = TestDistributedApplicationBuilder.Create(); @@ -67,13 +67,13 @@ public async Task EnsureDependentResourceMovesIntoWaitingStateUntilDependencyMov // into a Finished state, so rather than awaiting it we'll hold onto the // task so we can inspect the state of the Redis resource which should // be in a waiting state if everything is working correctly. - var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); var startTask = app.StartAsync(startupCts.Token); // We don't want to wait forever for Redis to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the // CI machine is chugging (also useful when collecting code coverage). - var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); var rns = app.Services.GetRequiredService(); await rns.WaitForResourceAsync(redis.Resource.Name, "Waiting", waitingStateCts.Token); @@ -98,6 +98,54 @@ await rns.PublishUpdateAsync(dependency.Resource, s => s with await app.StopAsync(); } + [Fact] + [RequiresDocker] + public async Task WaitForThrowsIfResourceMovesToTerminalStateBeforeRunning() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var dependency = builder.AddResource(new CustomResource("test")); + var redis = builder.AddRedis("redis") + .WithReference(dependency) + .WaitFor(dependency); + + using var app = builder.Build(); + + // StartAsync will currently block until the dependency resource moves + // into a Finished state, so rather than awaiting it we'll hold onto the + // task so we can inspect the state of the Redis resource which should + // be in a waiting state if everything is working correctly. + var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); + var startTask = app.StartAsync(startupCts.Token); + + // We don't want to wait forever for Redis to move into a waiting state, + // it should be super quick, but we'll allow 60 seconds just in case the + // CI machine is chugging (also useful when collecting code coverage). + var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); + + var rns = app.Services.GetRequiredService(); + await rns.WaitForResourceAsync(redis.Resource.Name, "Waiting", waitingStateCts.Token); + + // Now that we know we successfully entered the Waiting state, we can swap + // the dependency into a running state which will unblock startup and + // we can continue executing. + await rns.PublishUpdateAsync(dependency.Resource, s => s with + { + State = KnownResourceStates.Finished, + ExitCode = 0 + }); + + // This time we want to wait for Redis to move into a Running state to verify that + // it successfully started after we moved the dependency resource into the Finished, but + // we need to give it more time since we have to download the image in CI. + var runningStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); + await rns.WaitForResourceAsync(redis.Resource.Name, KnownResourceStates.FailedToStart, runningStateCts.Token); + + await startTask; + + await app.StopAsync(); + } + [Fact] [RequiresDocker] public async Task EnsureDependencyResourceThatReturnsNonMatchingExitCodeResultsInDependentResourceFailingToStart() From d1d57adb468021fbdd661b69d4982c6befc7ff95 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Thu, 29 Aug 2024 20:47:23 +1000 Subject: [PATCH 09/11] Tweaks to logging and using nginx container image instead. --- .../ResourceBuilderExtensions.cs | 18 +++++-- tests/Aspire.Hosting.Tests/WaitForTests.cs | 52 +++++++++---------- 2 files changed, 40 insertions(+), 30 deletions(-) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index c2c52a863de..b4f85ee9a5a 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -588,11 +588,16 @@ public static IResourceBuilder WaitFor(this IResourceBuilder builder, I if (snapshot.State == KnownResourceStates.FailedToStart) { - throw new DistributedApplicationException("Dependency resource failed to start."); + resourceLogger.LogError( + "Dependency resource '{ResourceName}' failed to start.", + dependency.Resource.Name + ); + + throw new DistributedApplicationException($"Dependency resource '{dependency.Resource.Name}' failed to start."); } else if (snapshot.State!.Text == KnownResourceStates.Finished || snapshot.State!.Text == KnownResourceStates.Exited) { - resourceLogger.LogInformation( + resourceLogger.LogError( "Resource '{ResourceName}' has entered the '{State}' state prematurely.", dependency.Resource.Name, snapshot.State.Text @@ -659,11 +664,16 @@ public static IResourceBuilder WaitForCompletion(this IResourceBuilder if (snapshot.State == KnownResourceStates.FailedToStart) { - throw new DistributedApplicationException("Dependency resource failed to start."); + resourceLogger.LogError( + "Dependency resource '{ResourceName}' failed to start.", + dependency.Resource.Name + ); + + throw new DistributedApplicationException($"Dependency resource '{dependency.Resource.Name}' failed to start."); } else if ((snapshot.State!.Text == KnownResourceStates.Finished || snapshot.State!.Text == KnownResourceStates.Exited) && snapshot.ExitCode != exitCode) { - resourceLogger.LogInformation( + resourceLogger.LogError( "Resource '{ResourceName}' has entered the '{State}' state with exit code '{ExitCode}'", dependency.Resource.Name, snapshot.State.Text, diff --git a/tests/Aspire.Hosting.Tests/WaitForTests.cs b/tests/Aspire.Hosting.Tests/WaitForTests.cs index e1a0422410d..278764e2317 100644 --- a/tests/Aspire.Hosting.Tests/WaitForTests.cs +++ b/tests/Aspire.Hosting.Tests/WaitForTests.cs @@ -17,7 +17,7 @@ public async Task EnsureDependentResourceMovesIntoWaitingState() using var builder = TestDistributedApplicationBuilder.Create(); var dependency = builder.AddResource(new CustomResource("test")); - var redis = builder.AddRedis("redis") + var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22") .WithReference(dependency) .WaitFor(dependency); @@ -25,18 +25,18 @@ public async Task EnsureDependentResourceMovesIntoWaitingState() // StartAsync will currently block until the dependency resource moves // into a Running state, so rather than awaiting it we'll hold onto the - // task so we can inspect the state of the Redis resource which should + // task so we can inspect the state of the Nginx resource which should // be in a waiting state if everything is working correctly. var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var startTask = app.StartAsync(startupCts.Token); - // We don't want to wait forever for Redis to move into a waiting state, + // We don't want to wait forever for Nginx to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the // CI machine is chugging (also useful when collecting code coverage). var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceAsync(redis.Resource.Name, "Waiting", waitingStateCts.Token); + await rns.WaitForResourceAsync(nginx.Resource.Name, "Waiting", waitingStateCts.Token); // Now that we know we successfully entered the Waiting state, we can swap // the dependency into a running state which will unblock startup and @@ -57,7 +57,7 @@ public async Task WaitForCompletionWaitsForTerminalStateOfDependencyResource() using var builder = TestDistributedApplicationBuilder.Create(); var dependency = builder.AddResource(new CustomResource("test")); - var redis = builder.AddRedis("redis") + var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22") .WithReference(dependency) .WaitForCompletion(dependency); @@ -65,18 +65,18 @@ public async Task WaitForCompletionWaitsForTerminalStateOfDependencyResource() // StartAsync will currently block until the dependency resource moves // into a Finished state, so rather than awaiting it we'll hold onto the - // task so we can inspect the state of the Redis resource which should + // task so we can inspect the state of the Nginx resource which should // be in a waiting state if everything is working correctly. var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); var startTask = app.StartAsync(startupCts.Token); - // We don't want to wait forever for Redis to move into a waiting state, + // We don't want to wait forever for Nginx to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the // CI machine is chugging (also useful when collecting code coverage). var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceAsync(redis.Resource.Name, "Waiting", waitingStateCts.Token); + await rns.WaitForResourceAsync(nginx.Resource.Name, "Waiting", waitingStateCts.Token); // Now that we know we successfully entered the Waiting state, we can swap // the dependency into a running state which will unblock startup and @@ -87,11 +87,11 @@ await rns.PublishUpdateAsync(dependency.Resource, s => s with ExitCode = 0 }); - // This time we want to wait for Redis to move into a Running state to verify that + // This time we want to wait for Nginx to move into a Running state to verify that // it successfully started after we moved the dependency resource into the Finished, but // we need to give it more time since we have to download the image in CI. var runningStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - await rns.WaitForResourceAsync(redis.Resource.Name, KnownResourceStates.Running, runningStateCts.Token); + await rns.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Running, runningStateCts.Token); await startTask; @@ -105,7 +105,7 @@ public async Task WaitForThrowsIfResourceMovesToTerminalStateBeforeRunning() using var builder = TestDistributedApplicationBuilder.Create(); var dependency = builder.AddResource(new CustomResource("test")); - var redis = builder.AddRedis("redis") + var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22") .WithReference(dependency) .WaitFor(dependency); @@ -113,18 +113,18 @@ public async Task WaitForThrowsIfResourceMovesToTerminalStateBeforeRunning() // StartAsync will currently block until the dependency resource moves // into a Finished state, so rather than awaiting it we'll hold onto the - // task so we can inspect the state of the Redis resource which should + // task so we can inspect the state of the Nginx resource which should // be in a waiting state if everything is working correctly. var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); var startTask = app.StartAsync(startupCts.Token); - // We don't want to wait forever for Redis to move into a waiting state, + // We don't want to wait forever for Nginx to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the // CI machine is chugging (also useful when collecting code coverage). var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(120)); var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceAsync(redis.Resource.Name, "Waiting", waitingStateCts.Token); + await rns.WaitForResourceAsync(nginx.Resource.Name, "Waiting", waitingStateCts.Token); // Now that we know we successfully entered the Waiting state, we can swap // the dependency into a running state which will unblock startup and @@ -135,11 +135,11 @@ await rns.PublishUpdateAsync(dependency.Resource, s => s with ExitCode = 0 }); - // This time we want to wait for Redis to move into a Running state to verify that + // This time we want to wait for Nginx to move into a Running state to verify that // it successfully started after we moved the dependency resource into the Finished, but // we need to give it more time since we have to download the image in CI. var runningStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - await rns.WaitForResourceAsync(redis.Resource.Name, KnownResourceStates.FailedToStart, runningStateCts.Token); + await rns.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.FailedToStart, runningStateCts.Token); await startTask; @@ -153,7 +153,7 @@ public async Task EnsureDependencyResourceThatReturnsNonMatchingExitCodeResultsI using var builder = TestDistributedApplicationBuilder.Create(); var dependency = builder.AddResource(new CustomResource("test")); - var redis = builder.AddRedis("redis") + var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22") .WithReference(dependency) .WaitForCompletion(dependency, exitCode: 2); @@ -161,18 +161,18 @@ public async Task EnsureDependencyResourceThatReturnsNonMatchingExitCodeResultsI // StartAsync will currently block until the dependency resource moves // into a Finished state, so rather than awaiting it we'll hold onto the - // task so we can inspect the state of the Redis resource which should + // task so we can inspect the state of the Nginx resource which should // be in a waiting state if everything is working correctly. var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var startTask = app.StartAsync(startupCts.Token); - // We don't want to wait forever for Redis to move into a waiting state, + // We don't want to wait forever for Nginx to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the // CI machine is chugging (also useful when collecting code coverage). var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceAsync(redis.Resource.Name, "Waiting", waitingStateCts.Token); + await rns.WaitForResourceAsync(nginx.Resource.Name, "Waiting", waitingStateCts.Token); // Now that we know we successfully entered the Waiting state, we can swap // the dependency into a finished state which will unblock startup and @@ -183,10 +183,10 @@ await rns.PublishUpdateAsync(dependency.Resource, s => s with ExitCode = 3 // Exit code does not match expected exit code above intentionally. }); - // This time we want to wait for Redis to move into a FailedToStart state to verify that + // This time we want to wait for Nginx to move into a FailedToStart state to verify that // it didn't start if the dependency resource didn't finish with the correct exit code. var runningStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); - await rns.WaitForResourceAsync(redis.Resource.Name, KnownResourceStates.FailedToStart, runningStateCts.Token); + await rns.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.FailedToStart, runningStateCts.Token); await startTask; @@ -202,7 +202,7 @@ public async Task DependencyWithGreaterThan1ReplicaAnnotationCausesDependentReso var dependency = builder.AddResource(new CustomResource("test")) .WithAnnotation(new ReplicaAnnotation(2)); - var redis = builder.AddRedis("redis") + var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22") .WithReference(dependency) .WaitForCompletion(dependency); @@ -210,18 +210,18 @@ public async Task DependencyWithGreaterThan1ReplicaAnnotationCausesDependentReso // StartAsync will currently block until the dependency resource moves // into a Finished state, so rather than awaiting it we'll hold onto the - // task so we can inspect the state of the Redis resource which should + // task so we can inspect the state of the Nginx resource which should // be in a waiting state if everything is working correctly. var startupCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var startTask = app.StartAsync(startupCts.Token); - // We don't want to wait forever for Redis to move into a waiting state, + // We don't want to wait forever for Nginx to move into a waiting state, // it should be super quick, but we'll allow 60 seconds just in case the // CI machine is chugging (also useful when collecting code coverage). var waitingStateCts = new CancellationTokenSource(TimeSpan.FromSeconds(60)); var rns = app.Services.GetRequiredService(); - await rns.WaitForResourceAsync(redis.Resource.Name, "FailedToStart", waitingStateCts.Token); + await rns.WaitForResourceAsync(nginx.Resource.Name, "FailedToStart", waitingStateCts.Token); await startTask; From 735eba9a29764c3f94a4ca004592ac5fe8468bf2 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 30 Aug 2024 08:10:49 +1000 Subject: [PATCH 10/11] Remove WaitFor from TestShop to validate that it isn't the problem. --- playground/TestShop/TestShop.AppHost/Program.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/playground/TestShop/TestShop.AppHost/Program.cs b/playground/TestShop/TestShop.AppHost/Program.cs index e595a2e5bb1..9fc964bed04 100644 --- a/playground/TestShop/TestShop.AppHost/Program.cs +++ b/playground/TestShop/TestShop.AppHost/Program.cs @@ -20,8 +20,7 @@ var catalogService = builder.AddProject("catalogservice") .WithReference(catalogDb) - .WithReplicas(2) - .WaitForCompletion(catalogDbApp); + .WithReplicas(2); var messaging = builder.AddRabbitMQ("messaging") .WithDataVolume() @@ -38,8 +37,7 @@ .WithReference(catalogService); builder.AddProject("orderprocessor", launchProfileName: "OrderProcessor") - .WithReference(messaging) - .WaitFor(messaging); + .WithReference(messaging); builder.AddProject("apigateway") .WithReference(basketService) From 79150b07b7a7e11dff7786efb18d3b95aca3d557 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Fri, 30 Aug 2024 12:32:23 +1000 Subject: [PATCH 11/11] Skip Kafka test. --- tests/Aspire.Playground.Tests/ProjectSpecificTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs b/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs index f662e152479..acac9054981 100644 --- a/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs +++ b/tests/Aspire.Playground.Tests/ProjectSpecificTests.cs @@ -29,7 +29,7 @@ await app.WaitForTextAsync($"I'm Batman. - Batman") await app.StopAsync(); } - [Fact] + [Fact(Skip = "https://github.com/dotnet/aspire/issues/5489")] public async Task KafkaTest() { var appHostPath = Directory.GetFiles(AppContext.BaseDirectory, "KafkaBasic.AppHost.dll").Single();