Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 64 additions & 1 deletion playground/Stress/Stress.AppHost/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;

#pragma warning disable ASPIREDOTNETTOOL
#pragma warning disable ASPIREINTERACTION001
#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental.

var builder = DistributedApplication.CreateBuilder(args);
Expand All @@ -18,6 +19,10 @@
var manualArgSource = builder.AddExecutable("manual-arg-source", "dotnet", Environment.CurrentDirectory)
.WithHttpEndpoint(targetPort: 8088);
var manualArgEndpoint = manualArgSource.GetEndpoint("http");
var stressEmptyProjectPath = new Projects.Stress_Empty().ProjectPath;
const string interactionContainerImage = "alpine";
const string interactionContainerEntrypoint = "/bin/sh";
string[] interactionContainerArgs = ["-c", "while true; do sleep 3600; done"];

manualArgSource.WithArgs("--dashboard-port")
.WithArgs(c => c.Args.Add(manualArgEndpoint.Property(EndpointProperty.Port)));
Expand All @@ -36,6 +41,45 @@
.WithArgs(c => c.Args.Add(manualArgEndpoint.Property(EndpointProperty.Port)))
.WithExplicitStart();

builder.AddExecutable("manual-environment-interaction", "dotnet", Environment.CurrentDirectory, "run", "--project", stressEmptyProjectPath, "--no-build")
.WithExplicitStart()
.WithEnvironment(context => PromptForEnvironmentValueAsync(
context,
"MANUAL_ENVIRONMENT_INTERACTION_VALUE",
"Explicit start executable environment callback",
"This prompt should only appear after manually starting the session-scoped executable."));

builder.AddExecutable("persistent-environment-interaction", "dotnet", Environment.CurrentDirectory, "run", "--project", stressEmptyProjectPath, "--no-build")
.WithPersistentLifetime()
.WithExplicitStart()
.WithEnvironment(context => PromptForEnvironmentValueAsync(
context,
"PERSISTENT_EXECUTABLE_ENVIRONMENT_INTERACTION_VALUE",
"Persistent executable environment callback",
"This prompt appears during startup because persistent executable resources are registered with DCP immediately."));

builder.AddContainer("manual-container-environment-interaction", interactionContainerImage)
.WithEntrypoint(interactionContainerEntrypoint)
.WithArgs(interactionContainerArgs)
.WithExplicitStart()
.WithEnvironment(context => PromptForEnvironmentValueAsync(
context,
"MANUAL_CONTAINER_ENVIRONMENT_INTERACTION_VALUE",
"Explicit start container environment callback",
"This prompt should only appear after manually starting the session-scoped container."));

builder.AddContainer("persistent-container-environment-interaction", interactionContainerImage)
.WithContainerName("stress-persistent-container-environment-interaction")
.WithEntrypoint(interactionContainerEntrypoint)
.WithArgs(interactionContainerArgs)
.WithPersistentLifetime()
.WithExplicitStart()
.WithEnvironment(context => PromptForEnvironmentValueAsync(
context,
"PERSISTENT_CONTAINER_ENVIRONMENT_INTERACTION_VALUE",
"Persistent container environment callback",
"This prompt appears during startup because persistent container resources are registered with DCP immediately."));

for (var i = 0; i < 2; i++)
{
var name = $"test-{i:0000}";
Expand Down Expand Up @@ -100,7 +144,6 @@
builder.AddExecutable("executableWithSingleArg", "dotnet", Environment.CurrentDirectory, "--version");
builder.AddExecutable("executableWithSingleEscapedArg", "dotnet", Environment.CurrentDirectory, "one two");
builder.AddExecutable("executableWithMultipleArgs", "dotnet", Environment.CurrentDirectory, "--version", "one two");
var stressEmptyProjectPath = new Projects.Stress_Empty().ProjectPath;
builder.AddExecutable("persistentExecutable", "dotnet", Environment.CurrentDirectory, "run", "--project", stressEmptyProjectPath, "--no-build")
.WithPersistentLifetime();

Expand Down Expand Up @@ -128,3 +171,23 @@
builder.AddNoStatusResource("no-status-resource");

builder.Build().Run();

static async Task PromptForEnvironmentValueAsync(
EnvironmentCallbackContext context,
string environmentVariableName,
string title,
string message)
{
var interactionService = context.ExecutionContext.ServiceProvider.GetRequiredService<IInteractionService>();
var result = await interactionService.PromptInputAsync(
title: title,
message: message,
inputLabel: "Environment value",
placeHolder: "Value from WithEnvironment callback",
cancellationToken: context.CancellationToken);

if (!result.Canceled && result.Data.Value is { } value)
{
context.EnvironmentVariables[environmentVariableName] = value;
}
}
4 changes: 1 addition & 3 deletions src/Aspire.Hosting/Dcp/ContainerCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,9 +251,7 @@ static bool IsContainerTunnelContainerName(string name)

public bool IsReadyToCreate(RenderedModelResource<Container> resource, ContainerCreationContext cctx)
{
// Containers are always "created" (submitted to DCP), they just get Spec.Start = false initially
// if explicit startup is used.
return true;
return !DcpModelUtilities.ShouldDeferCreateForExplicitStart(resource.ModelResource, resource.DcpResource.Spec.Start);
}

public async Task CreateObjectAsync(RenderedModelResource<Container> cr, ContainerCreationContext cctx, ILogger logger, IDcpObjectFactory factory, CancellationToken cancellationToken)
Expand Down
32 changes: 32 additions & 0 deletions src/Aspire.Hosting/Dcp/DcpExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,10 @@ Task IDcpObjectFactory.CreateDcpObjectsAsync<T>(IEnumerable<T> objects, Cancella
=> CreateDcpObjectsAsync(objects, cancellationToken);

async Task<T> IDcpObjectFactory.PatchDcpObjectAsync<T>(T obj, Action<T> change, CancellationToken cancellationToken)
=> await PatchDcpObjectAsync(obj, change, cancellationToken).ConfigureAwait(false);
Comment thread
danegsta marked this conversation as resolved.
Outdated

private async Task<T> PatchDcpObjectAsync<T>(T obj, Action<T> change, CancellationToken cancellationToken)
where T : CustomResource, IKubernetesStaticMetadata
{
var patch = CreatePatch(obj, change);
var result = await _kubernetesService.PatchAsync(obj, patch, cancellationToken).ConfigureAwait(false);
Expand Down Expand Up @@ -1067,6 +1071,11 @@ public async Task StartResourceAsync(IResourceReference resourceReference, Cance
await appResource.Initialized.WaitAsync(cancellationToken).ConfigureAwait(false);
using var _ = await ConcurrencyUtils.AcquireAllAsync([appResource.SerializedOpSemaphore], cancellationToken).ConfigureAwait(false);

if (await TryStartCreatedDelayedStartResourceAsync(resourceReference, resourceType, cancellationToken).ConfigureAwait(false))
Comment thread
danegsta marked this conversation as resolved.
Outdated
{
return;
}

// Reset cached callback results so they are re-evaluated on restart.
ForgetCachedCallbackResults(resourceReference.ModelResource);

Expand Down Expand Up @@ -1116,6 +1125,29 @@ public async Task StartResourceAsync(IResourceReference resourceReference, Cance
}
}

private async Task<bool> TryStartCreatedDelayedStartResourceAsync(IResourceReference resourceReference, string resourceType, CancellationToken cancellationToken)
{
switch (resourceReference)
{
case RenderedModelResource<Container> { DcpResource.Spec.Start: false } cr
when !DcpModelUtilities.ShouldDeferCreateForExplicitStart(cr.ModelResource, cr.DcpResource.Spec.Start):
await PublishConnectionStringAvailableEventAsync(cr.ModelResource, cancellationToken).ConfigureAwait(false);
await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, cr.ModelResource, cr.DcpResourceName)).ConfigureAwait(false);
await PatchDcpObjectAsync(cr.DcpResource, static c => c.Spec.Start = true, cancellationToken).ConfigureAwait(false);
return true;

case RenderedModelResource<Executable> { DcpResource.Spec.Start: false } er
when !DcpModelUtilities.ShouldDeferCreateForExplicitStart(er.ModelResource, er.DcpResource.Spec.Start):
await PublishConnectionStringAvailableEventAsync(er.ModelResource, cancellationToken).ConfigureAwait(false);
await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, er.ModelResource, er.DcpResourceName)).ConfigureAwait(false);
await PatchDcpObjectAsync(er.DcpResource, static e => e.Spec.Start = true, cancellationToken).ConfigureAwait(false);
return true;

default:
return false;
}
}

private async Task EnsureResourceDeletedAsync<T>(string resourceName, CancellationToken cancellationToken) where T : CustomResource, IKubernetesStaticMetadata
{
_logger.LogDebug("Ensuring '{ResourceName}' is deleted.", resourceName);
Expand Down
13 changes: 13 additions & 0 deletions src/Aspire.Hosting/Dcp/DcpModelUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ namespace Aspire.Hosting.Dcp;
/// </summary>
internal static class DcpModelUtilities
{
/// <summary>
/// Determines whether DCP registration should be deferred until an explicit manual start.
Comment thread
danegsta marked this conversation as resolved.
Outdated
/// </summary>
internal static bool ShouldDeferCreateForExplicitStart(IResource modelResource, bool? start)
{
// Explicit-start, non-persistent resources use manual snapshots for dashboard visibility.
// Do not register them with DCP until the manual start path flips Spec.Start=true; creation
Comment thread
danegsta marked this conversation as resolved.
Outdated
// evaluates callbacks that can prompt for input or depend on start-time state.
return start == false &&
modelResource.TryGetLastAnnotation<ExplicitStartupAnnotation>(out _) &&
modelResource.GetLifetimeType() != Lifetime.Persistent;
Comment thread
danegsta marked this conversation as resolved.
}

/// <summary>
/// Examines the Aspire resource annotations and adds equivalent ServiceProducerAnnotations to the corresponding DCP resource.
/// </summary>
Expand Down
3 changes: 1 addition & 2 deletions src/Aspire.Hosting/Dcp/ExecutableCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,7 @@ public IEnumerable<RenderedModelResource<Executable>> PrepareObjects()

public bool IsReadyToCreate(RenderedModelResource<Executable> resource, EmptyCreationContext context)
{
// Executables are always created. When explicit startup is used, DCP receives Spec.Start = false.
return true;
return !DcpModelUtilities.ShouldDeferCreateForExplicitStart(resource.ModelResource, resource.DcpResource.Spec.Start);
}

public async Task CreateObjectAsync(RenderedModelResource<Executable> er, EmptyCreationContext context, ILogger resourceLogger, IDcpObjectFactory factory, CancellationToken cancellationToken)
Expand Down
Loading
Loading