Skip to content

Commit 44b51c6

Browse files
danegstaCopilot
authored andcommitted
Refine explicit-start DCP lifecycle
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e36b4ee commit 44b51c6

3 files changed

Lines changed: 28 additions & 43 deletions

File tree

src/Aspire.Hosting/Dcp/DcpExecutor.cs

Lines changed: 25 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ internal static string GetResourceType<T>(T resource, IResource appModelResource
376376
Task IDcpObjectFactory.UpdateWithEffectiveAddressInfo(IEnumerable<Service> services, CancellationToken cancellationToken, TimeSpan? timeout)
377377
=> UpdateWithEffectiveAddressInfo(services, cancellationToken, timeout);
378378

379-
// Watches DCP object updates via a Kubernetes watch wrapped in the supplied retry pipeline,
379+
// Watches DCP object updates via a Kubernetes watch wrapped in the supplied retry pipeline,
380380
// till all objects reach desired state or a timeout occurs.
381381
// Returns names of objects that did not reach the desired state.
382382
private async Task<HashSet<string>> WatchUntilDesiredStateAsync<TDcpResource>(
@@ -591,8 +591,8 @@ private Task CreateAllDcpObjectsAsync<RT>(CancellationToken cancellationToken) w
591591
Task IDcpObjectFactory.CreateDcpObjectsAsync<T>(IEnumerable<T> objects, CancellationToken cancellationToken)
592592
=> CreateDcpObjectsAsync(objects, cancellationToken);
593593

594-
async Task<T> IDcpObjectFactory.PatchDcpObjectAsync<T>(T obj, Action<T> change, CancellationToken cancellationToken)
595-
=> await PatchDcpObjectAsync(obj, change, cancellationToken).ConfigureAwait(false);
594+
Task<T> IDcpObjectFactory.PatchDcpObjectAsync<T>(T obj, Action<T> change, CancellationToken cancellationToken)
595+
=> PatchDcpObjectAsync(obj, change, cancellationToken);
596596

597597
private async Task<T> PatchDcpObjectAsync<T>(T obj, Action<T> change, CancellationToken cancellationToken)
598598
where T : CustomResource, IKubernetesStaticMetadata
@@ -1071,20 +1071,25 @@ public async Task StartResourceAsync(IResourceReference resourceReference, Cance
10711071
await appResource.Initialized.WaitAsync(cancellationToken).ConfigureAwait(false);
10721072
using var _ = await ConcurrencyUtils.AcquireAllAsync([appResource.SerializedOpSemaphore], cancellationToken).ConfigureAwait(false);
10731073

1074-
if (await TryStartCreatedDelayedStartResourceAsync(resourceReference, resourceType, cancellationToken).ConfigureAwait(false))
1074+
// For resources that need delete/recreate startup, raise the starting event after deletion. This is required because
1075+
// deleting the existing DCP object temporarily overrides the status with a terminal state, such as "Exited".
1076+
switch (resourceReference)
10751077
{
1076-
return;
1077-
}
1078+
// We need to handle explicit start persistent resources specially on first launch as they may already be running, so we need to register them with DCP to discover their status.
1079+
case RenderedModelResource<Container> { DcpResource.Spec.Start: false } cr when !DcpModelUtilities.ShouldDeferCreateForExplicitStart(cr.ModelResource, cr.DcpResource.Spec.Start):
1080+
await PublishConnectionStringAvailableEventAsync(cr.ModelResource, cancellationToken).ConfigureAwait(false);
1081+
await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, cr.ModelResource, cr.DcpResourceName)).ConfigureAwait(false);
1082+
await PatchDcpObjectAsync(cr.DcpResource, static c => c.Spec.Start = true, cancellationToken).ConfigureAwait(false);
1083+
break;
10781084

1079-
// Reset cached callback results so they are re-evaluated on restart.
1080-
ForgetCachedCallbackResults(resourceReference.ModelResource);
1085+
case RenderedModelResource<Executable> { DcpResource.Spec.Start: false } er when !DcpModelUtilities.ShouldDeferCreateForExplicitStart(er.ModelResource, er.DcpResource.Spec.Start):
1086+
await PublishConnectionStringAvailableEventAsync(er.ModelResource, cancellationToken).ConfigureAwait(false);
1087+
await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, er.ModelResource, er.DcpResourceName)).ConfigureAwait(false);
1088+
await PatchDcpObjectAsync(er.DcpResource, static e => e.Spec.Start = true, cancellationToken).ConfigureAwait(false);
1089+
break;
10811090

1082-
// Raise event after resource has been deleted. This is required because the event sets the status to "Starting" and resources being
1083-
// deleted will temporarily override the status to a terminal state, such as "Exited".
1084-
switch (resourceReference)
1085-
{
10861091
case RenderedModelResource<Container> cr:
1087-
await EnsureResourceDeletedAsync<Container>(resourceReference.DcpResourceName, cancellationToken).ConfigureAwait(false);
1092+
await EnsureResourceDeletedAsync<Container>(resourceReference, cancellationToken).ConfigureAwait(false);
10881093

10891094
// Ensure we explicitly start the container even if original container was created in "delay-start" mode.
10901095
cr.DcpResource.Spec.Start = true;
@@ -1095,7 +1100,7 @@ public async Task StartResourceAsync(IResourceReference resourceReference, Cance
10951100
await _containerCreator.CreateObjectAsync(cr, cctx, resourceLogger, this, cancellationToken).ConfigureAwait(false);
10961101
break;
10971102
case RenderedModelResource<Executable> er:
1098-
await EnsureResourceDeletedAsync<Executable>(resourceReference.DcpResourceName, cancellationToken).ConfigureAwait(false);
1103+
await EnsureResourceDeletedAsync<Executable>(resourceReference, cancellationToken).ConfigureAwait(false);
10991104

11001105
// Ensure we explicitly start the executable even if original executable was created in "delay-start" mode.
11011106
er.DcpResource.Spec.Start = true;
@@ -1125,32 +1130,12 @@ public async Task StartResourceAsync(IResourceReference resourceReference, Cance
11251130
}
11261131
}
11271132

1128-
private async Task<bool> TryStartCreatedDelayedStartResourceAsync(IResourceReference resourceReference, string resourceType, CancellationToken cancellationToken)
1133+
private async Task EnsureResourceDeletedAsync<T>(IResourceReference resource, CancellationToken cancellationToken) where T : CustomResource, IKubernetesStaticMetadata
11291134
{
1130-
switch (resourceReference)
1131-
{
1132-
case RenderedModelResource<Container> { DcpResource.Spec.Start: false } cr
1133-
when !DcpModelUtilities.ShouldDeferCreateForExplicitStart(cr.ModelResource, cr.DcpResource.Spec.Start):
1134-
await PublishConnectionStringAvailableEventAsync(cr.ModelResource, cancellationToken).ConfigureAwait(false);
1135-
await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, cr.ModelResource, cr.DcpResourceName)).ConfigureAwait(false);
1136-
await PatchDcpObjectAsync(cr.DcpResource, static c => c.Spec.Start = true, cancellationToken).ConfigureAwait(false);
1137-
return true;
1138-
1139-
case RenderedModelResource<Executable> { DcpResource.Spec.Start: false } er
1140-
when !DcpModelUtilities.ShouldDeferCreateForExplicitStart(er.ModelResource, er.DcpResource.Spec.Start):
1141-
await PublishConnectionStringAvailableEventAsync(er.ModelResource, cancellationToken).ConfigureAwait(false);
1142-
await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, er.ModelResource, er.DcpResourceName)).ConfigureAwait(false);
1143-
await PatchDcpObjectAsync(er.DcpResource, static e => e.Spec.Start = true, cancellationToken).ConfigureAwait(false);
1144-
return true;
1145-
1146-
default:
1147-
return false;
1148-
}
1149-
}
1135+
_logger.LogDebug("Ensuring '{ResourceName}' is deleted.", resource.DcpResourceName);
11501136

1151-
private async Task EnsureResourceDeletedAsync<T>(string resourceName, CancellationToken cancellationToken) where T : CustomResource, IKubernetesStaticMetadata
1152-
{
1153-
_logger.LogDebug("Ensuring '{ResourceName}' is deleted.", resourceName);
1137+
// Reset cached callback results so they are re-evaluated on restart.
1138+
ForgetCachedCallbackResults(resource.ModelResource);
11541139

11551140
var result = await DeleteResourceRetryPipeline.ExecuteAsync(async (resourceName, attemptCancellationToken) =>
11561141
{
@@ -1194,11 +1179,11 @@ private async Task EnsureResourceDeletedAsync<T>(string resourceName, Cancellati
11941179
// Success.
11951180
return true;
11961181
}
1197-
}, resourceName, cancellationToken).ConfigureAwait(false);
1182+
}, resource.DcpResourceName, cancellationToken).ConfigureAwait(false);
11981183

11991184
if (!result)
12001185
{
1201-
throw new DistributedApplicationException($"Failed to delete '{resourceName}' successfully before restart.");
1186+
throw new DistributedApplicationException($"Failed to delete '{resource.DcpResourceName}' successfully before restart.");
12021187
}
12031188
}
12041189

src/Aspire.Hosting/Dcp/DcpModelUtilities.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ namespace Aspire.Hosting.Dcp;
1616
internal static class DcpModelUtilities
1717
{
1818
/// <summary>
19-
/// Determines whether DCP registration should be deferred until an explicit manual start.
19+
/// Determines whether DCP object creation should be deferred until an explicit manual start.
2020
/// </summary>
2121
internal static bool ShouldDeferCreateForExplicitStart(IResource modelResource, bool? start)
2222
{
2323
// Explicit-start, non-persistent resources use manual snapshots for dashboard visibility.
24-
// Do not register them with DCP until the manual start path flips Spec.Start=true; creation
24+
// Do not create corresponding DCP objects until the manual start path flips Spec.Start=true; creation
2525
// evaluates callbacks that can prompt for input or depend on start-time state.
2626
return start == false &&
2727
modelResource.TryGetLastAnnotation<ExplicitStartupAnnotation>(out _) &&

tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2586,7 +2586,7 @@ public async Task PersistentPlainExecutable_UsesStableCertificateOutputPath()
25862586
}
25872587

25882588
[Fact]
2589-
public async Task ExplicitStartPlainExecutable_IsNotCreatedUntilManualStart()
2589+
public async Task SessionScopedExplicitStartPlainExecutable_DefersDcpObjectCreationUntilManualStart()
25902590
{
25912591
var builder = DistributedApplication.CreateBuilder();
25922592

0 commit comments

Comments
 (0)