Skip to content

Commit cda8270

Browse files
authored
Publish snapshot using DCP data before resource started in DCP (#7916)
1 parent 9e8cca4 commit cda8270

File tree

2 files changed

+90
-3
lines changed

2 files changed

+90
-3
lines changed

src/Aspire.Hosting/Dcp/DcpExecutor.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -902,6 +902,13 @@ async Task CreateResourceExecutablesAsyncCore(IResource resource, IEnumerable<Ap
902902

903903
try
904904
{
905+
// Publish snapshots built from DCP resources. Do this now to populate more values from DCP (URLs, source) to ensure they're
906+
// available if the resource isn't immediately started because it's waiting or is configured for explicit start.
907+
foreach (var er in executables)
908+
{
909+
await _executorEvents.PublishAsync(new OnResourceChangedContext(_shutdownCancellation.Token, resourceType, resource, er.DcpResourceName, new ResourceStatus(null, null, null), s => _snapshotBuilder.ToSnapshot((Executable) er.DcpResource, s))).ConfigureAwait(false);
910+
}
911+
905912
await _executorEvents.PublishAsync(new OnResourceStartingContext(cancellationToken, resourceType, resource, DcpResourceName: null)).ConfigureAwait(false);
906913

907914
foreach (var er in executables)
@@ -1167,6 +1174,10 @@ async Task CreateContainerAsyncCore(AppResource cr, CancellationToken cancellati
11671174

11681175
foreach (var cr in containerResources)
11691176
{
1177+
// Publish snapshot built from DCP resource. Do this now to populate more values from DCP (URLs, source) to ensure they're
1178+
// available if the resource isn't immediately started because it's waiting or is configured for explicit start.
1179+
await _executorEvents.PublishAsync(new OnResourceChangedContext(_shutdownCancellation.Token, KnownResourceTypes.Container, cr.ModelResource, cr.DcpResourceName, new ResourceStatus(null, null, null), s => _snapshotBuilder.ToSnapshot((Container) cr.DcpResource, s))).ConfigureAwait(false);
1180+
11701181
if (cr.ModelResource.TryGetLastAnnotation<ExplicitStartupAnnotation>(out _))
11711182
{
11721183
await _executorEvents.PublishAsync(new OnResourceChangedContext(cancellationToken, KnownResourceTypes.Container, cr.ModelResource, cr.DcpResourceName, new ResourceStatus(KnownResourceStates.NotStarted, null, null), s => s with { State = new ResourceStateSnapshot(KnownResourceStates.NotStarted, null) })).ConfigureAwait(false);

tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,10 @@ public async Task StartResourceForcesStart()
152152
}
153153

154154
[Fact]
155-
public async Task ExplicitStart_StartResource()
155+
public async Task ExplicitStart_StartExecutable()
156156
{
157-
const string testName = "explicit-start-resource";
158-
using var testProgram = CreateTestProgram(testName);
157+
const string testName = "explicit-start-executable";
158+
using var testProgram = CreateTestProgram(testName, randomizePorts: false);
159159
SetupXUnitLogging(testProgram.AppBuilder.Services);
160160

161161
var notStartedResourceName = $"{testName}-servicea";
@@ -175,6 +175,20 @@ public async Task ExplicitStart_StartResource()
175175
var notStartedResourceEvent = await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.NotStarted).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
176176
var dependentResourceEvent = await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Waiting).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
177177

178+
// Inactive URLs and source should be populated on non-started resources.
179+
Assert.Contains("TestProject.ServiceA.csproj", notStartedResourceEvent.Snapshot.Properties.Single(p => p.Name == "project.path").Value?.ToString());
180+
Assert.Collection(notStartedResourceEvent.Snapshot.Urls, u =>
181+
{
182+
Assert.Equal("http://localhost:5156", u.Url);
183+
Assert.True(u.IsInactive);
184+
});
185+
Assert.Contains("TestProject.ServiceB.csproj", dependentResourceEvent.Snapshot.Properties.Single(p => p.Name == "project.path").Value?.ToString());
186+
Assert.Collection(dependentResourceEvent.Snapshot.Urls, u =>
187+
{
188+
Assert.Equal("http://localhost:5254", u.Url);
189+
Assert.True(u.IsInactive);
190+
});
191+
178192
logger.LogInformation("Start explicit start resource.");
179193
await orchestrator.StartResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
180194
await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
@@ -194,6 +208,68 @@ public async Task ExplicitStart_StartResource()
194208
await app.StopAsync().DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
195209
}
196210

211+
[Fact]
212+
[RequiresDocker]
213+
public async Task ExplicitStart_StartContainer()
214+
{
215+
const string testName = "explicit-start-container";
216+
using var testProgram = CreateTestProgram(testName, randomizePorts: false);
217+
SetupXUnitLogging(testProgram.AppBuilder.Services);
218+
219+
var notStartedResourceName = $"{testName}-redis";
220+
var dependentResourceName = $"{testName}-serviceb";
221+
222+
var containerBuilder = testProgram.AppBuilder.AddContainer(notStartedResourceName, "redis")
223+
.WithEndpoint(port: 6379, targetPort: 6379, name: "tcp", env: "REDIS_PORT")
224+
.WithExplicitStart();
225+
226+
containerBuilder.WithExplicitStart();
227+
testProgram.ServiceBBuilder.WaitFor(containerBuilder);
228+
229+
using var app = testProgram.Build();
230+
var rns = app.Services.GetRequiredService<ResourceNotificationService>();
231+
var orchestrator = app.Services.GetRequiredService<ApplicationOrchestrator>();
232+
var logger = app.Services.GetRequiredService<ILogger<DistributedApplicationTests>>();
233+
234+
var startTask = app.StartAsync();
235+
236+
// On start, one resource won't be started and the other is waiting on it.
237+
var notStartedResourceEvent = await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.NotStarted).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
238+
var dependentResourceEvent = await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Waiting).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
239+
240+
// Inactive URLs and source should be populated on non-started resources.
241+
Assert.Equal("redis:latest", notStartedResourceEvent.Snapshot.Properties.Single(p => p.Name == "container.image").Value?.ToString());
242+
Assert.Collection(notStartedResourceEvent.Snapshot.Urls, u =>
243+
{
244+
Assert.Equal("tcp://localhost:6379", u.Url);
245+
Assert.True(u.IsInactive);
246+
});
247+
Assert.Contains("TestProject.ServiceB.csproj", dependentResourceEvent.Snapshot.Properties.Single(p => p.Name == "project.path").Value?.ToString());
248+
Assert.Collection(dependentResourceEvent.Snapshot.Urls, u =>
249+
{
250+
Assert.Equal("http://localhost:5254", u.Url);
251+
Assert.True(u.IsInactive);
252+
});
253+
254+
logger.LogInformation("Start explicit start resource.");
255+
await orchestrator.StartResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
256+
await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
257+
258+
// Dependent resource should now run.
259+
await rns.WaitForResourceAsync(dependentResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
260+
261+
logger.LogInformation("Stop resource.");
262+
await orchestrator.StopResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
263+
await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Exited).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
264+
265+
logger.LogInformation("Start resource again");
266+
await orchestrator.StartResourceAsync(notStartedResourceEvent.ResourceId, CancellationToken.None).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
267+
await rns.WaitForResourceAsync(notStartedResourceName, e => e.Snapshot.State?.Text == KnownResourceStates.Running).DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
268+
269+
await startTask.DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
270+
await app.StopAsync().DefaultTimeout(TestConstants.LongTimeoutTimeSpan);
271+
}
272+
197273
[Fact]
198274
public void RegisteredLifecycleHookIsExecutedWhenRunSynchronously()
199275
{

0 commit comments

Comments
 (0)