Skip to content

Commit c000b49

Browse files
Don't end resource notifications until service is disposed (#7108)
Contributes to #4878
1 parent b02fa42 commit c000b49

File tree

3 files changed

+36
-10
lines changed

3 files changed

+36
-10
lines changed

src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ namespace Aspire.Hosting.ApplicationModel;
1414
/// <summary>
1515
/// A service that allows publishing and subscribing to changes in the state of a resource.
1616
/// </summary>
17-
public class ResourceNotificationService
17+
public class ResourceNotificationService : IDisposable
1818
{
1919
// Resource state is keyed by the resource and the unique name of the resource. This could be the name of the resource, or a replica ID.
2020
private readonly ConcurrentDictionary<(IResource, string), ResourceNotificationState> _resourceNotificationStates = new();
2121
private readonly ILogger<ResourceNotificationService> _logger;
2222
private readonly IServiceProvider _serviceProvider;
23-
private readonly CancellationToken _applicationStopping;
23+
private readonly CancellationTokenSource _disposing = new();
2424
private readonly ResourceLoggerService _resourceLoggerService;
2525

2626
private Action<ResourceEvent>? OnResourceUpdated { get; set; }
@@ -43,7 +43,6 @@ public ResourceNotificationService(ILogger<ResourceNotificationService> logger,
4343
{
4444
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
4545
_serviceProvider = new NullServiceProvider();
46-
_applicationStopping = hostApplicationLifetime?.ApplicationStopping ?? throw new ArgumentNullException(nameof(hostApplicationLifetime));
4746
_resourceLoggerService = new ResourceLoggerService();
4847
}
4948

@@ -58,8 +57,10 @@ public ResourceNotificationService(ILogger<ResourceNotificationService> logger,
5857
{
5958
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
6059
_serviceProvider = serviceProvider;
61-
_applicationStopping = hostApplicationLifetime?.ApplicationStopping ?? throw new ArgumentNullException(nameof(hostApplicationLifetime));
6260
_resourceLoggerService = resourceLoggerService ?? throw new ArgumentNullException(nameof(resourceLoggerService));
61+
62+
// The IHostApplicationLifetime parameter is not used anymore, but we keep it for backwards compatibility.
63+
// Notfication updates will be cancelled when the service is disposed.
6364
}
6465

6566
private class NullServiceProvider : IServiceProvider
@@ -105,7 +106,7 @@ public Task WaitForResourceAsync(string resourceName, string? targetState = null
105106
Justification = "targetState(s) parameters are mutually exclusive.")]
106107
public async Task<string> WaitForResourceAsync(string resourceName, IEnumerable<string> targetStates, CancellationToken cancellationToken = default)
107108
{
108-
using var watchCts = CancellationTokenSource.CreateLinkedTokenSource(_applicationStopping, cancellationToken);
109+
using var watchCts = CancellationTokenSource.CreateLinkedTokenSource(_disposing.Token, cancellationToken);
109110
var watchToken = watchCts.Token;
110111
await foreach (var resourceEvent in WatchAsync(watchToken).ConfigureAwait(false))
111112
{
@@ -273,7 +274,7 @@ public async Task WaitForDependenciesAsync(IResource resource, CancellationToken
273274
Justification = "predicate and targetState(s) parameters are mutually exclusive.")]
274275
public async Task<ResourceEvent> WaitForResourceAsync(string resourceName, Func<ResourceEvent, bool> predicate, CancellationToken cancellationToken = default)
275276
{
276-
using var watchCts = CancellationTokenSource.CreateLinkedTokenSource(_applicationStopping, cancellationToken);
277+
using var watchCts = CancellationTokenSource.CreateLinkedTokenSource(_disposing.Token, cancellationToken);
277278
var watchToken = watchCts.Token;
278279
await foreach (var resourceEvent in WatchAsync(watchToken).ConfigureAwait(false))
279280
{
@@ -502,6 +503,12 @@ private static CustomResourceSnapshot GetCurrentSnapshot(IResource resource, Res
502503
private ResourceNotificationState GetResourceNotificationState(IResource resource, string resourceId) =>
503504
_resourceNotificationStates.GetOrAdd((resource, resourceId), _ => new ResourceNotificationState());
504505

506+
/// <inheritdoc/>
507+
public void Dispose()
508+
{
509+
_disposing.Cancel();
510+
}
511+
505512
/// <summary>
506513
/// The annotation that allows publishing and subscribing to changes in the state of a resource.
507514
/// </summary>

src/Aspire.Hosting/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ Aspire.Hosting.ApplicationModel.ResourceCommandState
133133
Aspire.Hosting.ApplicationModel.ResourceCommandState.Disabled = 1 -> Aspire.Hosting.ApplicationModel.ResourceCommandState
134134
Aspire.Hosting.ApplicationModel.ResourceCommandState.Enabled = 0 -> Aspire.Hosting.ApplicationModel.ResourceCommandState
135135
Aspire.Hosting.ApplicationModel.ResourceCommandState.Hidden = 2 -> Aspire.Hosting.ApplicationModel.ResourceCommandState
136+
Aspire.Hosting.ApplicationModel.ResourceNotificationService.Dispose() -> void
136137
Aspire.Hosting.ApplicationModel.ResourceNotificationService.ResourceNotificationService(Microsoft.Extensions.Logging.ILogger<Aspire.Hosting.ApplicationModel.ResourceNotificationService!>! logger, Microsoft.Extensions.Hosting.IHostApplicationLifetime! hostApplicationLifetime) -> void
137138
Aspire.Hosting.ApplicationModel.ResourceNotificationService.ResourceNotificationService(Microsoft.Extensions.Logging.ILogger<Aspire.Hosting.ApplicationModel.ResourceNotificationService!>! logger, Microsoft.Extensions.Hosting.IHostApplicationLifetime! hostApplicationLifetime, System.IServiceProvider! serviceProvider, Aspire.Hosting.ApplicationModel.ResourceLoggerService! resourceLoggerService) -> void
138139
Aspire.Hosting.ApplicationModel.ResourceNotificationService.WaitForDependenciesAsync(Aspire.Hosting.ApplicationModel.IResource! resource, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task!

tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,24 @@ public async Task WaitingOnResourceReturnsCorrectStateWhenResourceReachesOneOfTa
244244
Assert.Equal("SomeOtherState", reachedState);
245245
}
246246

247+
[Fact]
248+
public async Task WaitingOnResourceReturnsItReachesStateAfterApplicationStoppingCancellationTokenSignaled()
249+
{
250+
var resource1 = new CustomResource("myResource1");
251+
252+
using var hostApplicationLifetime = new TestHostApplicationLifetime();
253+
var notificationService = ResourceNotificationServiceTestHelpers.Create(hostApplicationLifetime: hostApplicationLifetime);
254+
255+
var waitTask = notificationService.WaitForResourceAsync("myResource1", "SomeState");
256+
hostApplicationLifetime.StopApplication();
257+
258+
await notificationService.PublishUpdateAsync(resource1, snapshot => snapshot with { State = "SomeState" }).DefaultTimeout();
259+
260+
await waitTask.DefaultTimeout();
261+
262+
Assert.True(waitTask.IsCompletedSuccessfully);
263+
}
264+
247265
[Fact]
248266
public async Task WaitingOnResourceThrowsOperationCanceledExceptionIfResourceDoesntReachStateBeforeCancellationTokenSignaled()
249267
{
@@ -261,13 +279,13 @@ await Assert.ThrowsAsync<OperationCanceledException>(async () =>
261279
}
262280

263281
[Fact]
264-
public async Task WaitingOnResourceThrowsOperationCanceledExceptionIfResourceDoesntReachStateBeforeApplicationStoppingCancellationTokenSignaled()
282+
public async Task WaitingOnResourceThrowsOperationCanceledExceptionIfResourceDoesntReachStateBeforeServiceIsDisposed()
265283
{
266-
using var hostApplicationLifetime = new TestHostApplicationLifetime();
267-
var notificationService = ResourceNotificationServiceTestHelpers.Create(hostApplicationLifetime: hostApplicationLifetime);
284+
var notificationService = ResourceNotificationServiceTestHelpers.Create();
268285

269286
var waitTask = notificationService.WaitForResourceAsync("myResource1", "SomeState");
270-
hostApplicationLifetime.StopApplication();
287+
288+
notificationService.Dispose();
271289

272290
await Assert.ThrowsAsync<OperationCanceledException>(async () =>
273291
{

0 commit comments

Comments
 (0)