Skip to content

Commit e25cb52

Browse files
authored
Promptly remove invalid resolvers (dotnet#3800)
1 parent c87ed5b commit e25cb52

8 files changed

+85
-31
lines changed

Directory.Packages.props

+1
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
<PackageVersion Include="Microsoft.Extensions.Options" Version="$(MicrosoftExtensionsOptionsPackageVersion)" />
8888
<PackageVersion Include="Microsoft.Extensions.Primitives" Version="$(MicrosoftExtensionsPrimitivesPackageVersion)" />
8989
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="$(MicrosoftExtensionsHttpResiliencePackageVersion)" />
90+
<PackageVersion Include="Microsoft.Extensions.TimeProvider.Testing" Version="$(MicrosoftExtensionsTimeProviderTestingVersion)" />
9091
<!-- external dependencies -->
9192
<PackageVersion Include="Confluent.Kafka" Version="2.3.0" />
9293
<PackageVersion Include="Dapper" Version="2.1.37" />

eng/Versions.props

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
<MicrosoftExtensionsDiagnosticsHealthChecksEntityFrameworkCorePackageVersion>8.0.4</MicrosoftExtensionsDiagnosticsHealthChecksEntityFrameworkCorePackageVersion>
4747
<MicrosoftExtensionsDiagnosticsHealthChecksPackageVersion>8.0.4</MicrosoftExtensionsDiagnosticsHealthChecksPackageVersion>
4848
<MicrosoftExtensionsFeaturesPackageVersion>8.0.4</MicrosoftExtensionsFeaturesPackageVersion>
49+
<MicrosoftExtensionsTimeProviderTestingVersion>8.4.0</MicrosoftExtensionsTimeProviderTestingVersion>
4950
<!-- EF -->
5051
<MicrosoftEntityFrameworkCoreCosmosPackageVersion>8.0.4</MicrosoftEntityFrameworkCoreCosmosPackageVersion>
5152
<MicrosoftEntityFrameworkCoreDesignPackageVersion>8.0.4</MicrosoftEntityFrameworkCoreDesignPackageVersion>

src/Microsoft.Extensions.ServiceDiscovery/Http/HttpServiceEndpointResolver.cs

+5
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ public async ValueTask<ServiceEndpoint> GetEndpointAsync(HttpRequestMessage requ
5757

5858
return endpoint;
5959
}
60+
else
61+
{
62+
_resolvers.TryRemove(KeyValuePair.Create(resolver.ServiceName, resolver));
63+
}
6064
}
6165
}
6266

@@ -140,6 +144,7 @@ private async Task CleanupResolversAsyncCore()
140144
cleanupTasks.Add(resolver.DisposeAsync().AsTask());
141145
}
142146
}
147+
143148
if (cleanupTasks is not null)
144149
{
145150
await Task.WhenAll(cleanupTasks).ConfigureAwait(false);

src/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointResolver.cs

+6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public async ValueTask<ServiceEndpointSource> GetEndpointsAsync(string serviceNa
4949
while (true)
5050
{
5151
ObjectDisposedException.ThrowIf(_disposed, this);
52+
cancellationToken.ThrowIfCancellationRequested();
5253
var resolver = _resolvers.GetOrAdd(
5354
serviceName,
5455
static (name, self) => self.CreateResolver(name),
@@ -64,6 +65,10 @@ public async ValueTask<ServiceEndpointSource> GetEndpointsAsync(string serviceNa
6465

6566
return result;
6667
}
68+
else
69+
{
70+
_resolvers.TryRemove(KeyValuePair.Create(resolver.ServiceName, resolver));
71+
}
6772
}
6873
}
6974

@@ -148,6 +153,7 @@ private async Task CleanupResolversAsyncCore()
148153
cleanupTasks.Add(resolver.DisposeAsync().AsTask());
149154
}
150155
}
156+
151157
if (cleanupTasks is not null)
152158
{
153159
await Task.WhenAll(cleanupTasks).ConfigureAwait(false);

src/Microsoft.Extensions.ServiceDiscovery/ServiceEndpointWatcher.cs

+34-14
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ internal sealed partial class ServiceEndpointWatcher(
3232
private ServiceEndpointSource? _cachedEndpoints;
3333
private Task _refreshTask = Task.CompletedTask;
3434
private volatile CacheStatus _cacheState;
35+
private IDisposable? _changeTokenRegistration;
3536

3637
/// <summary>
3738
/// Gets the service name.
@@ -60,6 +61,8 @@ public void Start()
6061
public ValueTask<ServiceEndpointSource> GetEndpointsAsync(CancellationToken cancellationToken = default)
6162
{
6263
ThrowIfNoProviders();
64+
ObjectDisposedException.ThrowIf(_disposalCancellation.IsCancellationRequested, this);
65+
cancellationToken.ThrowIfCancellationRequested();
6366

6467
// If the cache is valid, return the cached value.
6568
if (_cachedEndpoints is { ChangeToken.HasChanged: false } cached)
@@ -76,9 +79,11 @@ async ValueTask<ServiceEndpointSource> GetEndpointsInternal(CancellationToken ca
7679
ServiceEndpointSource? result;
7780
do
7881
{
82+
cancellationToken.ThrowIfCancellationRequested();
7983
await RefreshAsync(force: false).WaitAsync(cancellationToken).ConfigureAwait(false);
8084
result = _cachedEndpoints;
8185
} while (result is null);
86+
8287
return result;
8388
}
8489
}
@@ -89,7 +94,7 @@ private Task RefreshAsync(bool force)
8994
lock (_lock)
9095
{
9196
// If the cache is invalid or needs invalidation, refresh the cache.
92-
if (_refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force))
97+
if (!_disposalCancellation.IsCancellationRequested && _refreshTask.IsCompleted && (_cacheState == CacheStatus.Invalid || _cachedEndpoints is null or { ChangeToken.HasChanged: true } || force))
9398
{
9499
// Indicate that the cache is being updated and start a new refresh task.
95100
_cacheState = CacheStatus.Refreshing;
@@ -128,10 +133,18 @@ private async Task RefreshAsyncInternal()
128133
CacheStatus newCacheState;
129134
try
130135
{
136+
lock (_lock)
137+
{
138+
// Dispose the existing change token registration, if any.
139+
_changeTokenRegistration?.Dispose();
140+
_changeTokenRegistration = null;
141+
}
142+
131143
Log.ResolvingEndpoints(_logger, ServiceName);
132144
var builder = new ServiceEndpointBuilder();
133145
foreach (var provider in _providers)
134146
{
147+
cancellationToken.ThrowIfCancellationRequested();
135148
await provider.PopulateAsync(builder, cancellationToken).ConfigureAwait(false);
136149
}
137150

@@ -143,13 +156,12 @@ private async Task RefreshAsyncInternal()
143156
// Check if we need to poll for updates or if we can register for change notification callbacks.
144157
if (endpoints.ChangeToken.ActiveChangeCallbacks)
145158
{
146-
// Initiate a background refresh, if necessary.
147-
endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this);
148-
if (_pollingTimer is { } timer)
149-
{
150-
_pollingTimer = null;
151-
timer.Dispose();
152-
}
159+
// Initiate a background refresh when the change token fires.
160+
_changeTokenRegistration = endpoints.ChangeToken.RegisterChangeCallback(static state => _ = ((ServiceEndpointWatcher)state!).RefreshAsync(force: false), this);
161+
162+
// Dispose the existing timer, if any, since we are reliant on change tokens for updates.
163+
_pollingTimer?.Dispose();
164+
_pollingTimer = null;
153165
}
154166
else
155167
{
@@ -211,6 +223,13 @@ private void SchedulePollingTimer()
211223
{
212224
lock (_lock)
213225
{
226+
if (_disposalCancellation.IsCancellationRequested)
227+
{
228+
_pollingTimer?.Dispose();
229+
_pollingTimer = null;
230+
return;
231+
}
232+
214233
if (_pollingTimer is null)
215234
{
216235
_pollingTimer = _timeProvider.CreateTimer(s_pollingAction, this, _options.RefreshPeriod, TimeSpan.Zero);
@@ -227,14 +246,15 @@ public async ValueTask DisposeAsync()
227246
{
228247
lock (_lock)
229248
{
230-
if (_pollingTimer is { } timer)
231-
{
232-
_pollingTimer = null;
233-
timer.Dispose();
234-
}
249+
_disposalCancellation.Cancel();
250+
251+
_changeTokenRegistration?.Dispose();
252+
_changeTokenRegistration = null;
253+
254+
_pollingTimer?.Dispose();
255+
_pollingTimer = null;
235256
}
236257

237-
_disposalCancellation.Cancel();
238258
if (_refreshTask is { } task)
239259
{
240260
await task.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Hosting;
6+
using Microsoft.Extensions.Time.Testing;
7+
using Xunit;
8+
9+
namespace Microsoft.Extensions.ServiceDiscovery.Dns.Tests;
10+
11+
public class DnsServiceEndpointResolverTests
12+
{
13+
[Fact]
14+
public async Task ResolveServiceEndpoint_Dns_MultiShot()
15+
{
16+
var timeProvider = new FakeTimeProvider();
17+
var services = new ServiceCollection()
18+
.AddSingleton<TimeProvider>(timeProvider)
19+
.AddServiceDiscoveryCore()
20+
.AddDnsServiceEndpointProvider(o => o.DefaultRefreshPeriod = TimeSpan.FromSeconds(30))
21+
.BuildServiceProvider();
22+
var resolver = services.GetRequiredService<ServiceEndpointResolver>();
23+
var initialResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None);
24+
Assert.NotNull(initialResult);
25+
Assert.True(initialResult.Endpoints.Count > 0);
26+
timeProvider.Advance(TimeSpan.FromSeconds(7));
27+
var secondResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None);
28+
Assert.NotNull(secondResult);
29+
Assert.True(initialResult.Endpoints.Count > 0);
30+
timeProvider.Advance(TimeSpan.FromSeconds(80));
31+
var thirdResult = await resolver.GetEndpointsAsync("https://localhost", CancellationToken.None);
32+
Assert.NotNull(thirdResult);
33+
Assert.True(initialResult.Endpoints.Count > 0);
34+
}
35+
}

tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/DnsSrvServiceEndpointResolverTests.cs

+2-17
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ private sealed class FakeDnsQueryResponse : IDnsQueryResponse
7373
}
7474

7575
[Fact]
76-
public async Task ResolveServiceEndpoint_Dns()
76+
public async Task ResolveServiceEndpoint_DnsSrv()
7777
{
7878
var dnsClientMock = new FakeDnsClient
7979
{
@@ -134,7 +134,7 @@ public async Task ResolveServiceEndpoint_Dns()
134134
[InlineData(true)]
135135
[InlineData(false)]
136136
[Theory]
137-
public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(bool dnsFirst)
137+
public async Task ResolveServiceEndpoint_DnsSrv_MultipleProviders_PreventMixing(bool dnsFirst)
138138
{
139139
var dnsClientMock = new FakeDnsClient
140140
{
@@ -233,19 +233,4 @@ public async Task ResolveServiceEndpoint_Dns_MultipleProviders_PreventMixing(boo
233233
}
234234
}
235235
}
236-
237-
public class MyConfigurationProvider : ConfigurationProvider, IConfigurationSource
238-
{
239-
public IConfigurationProvider Build(IConfigurationBuilder builder) => this;
240-
public void SetValues(IEnumerable<KeyValuePair<string, string?>> values)
241-
{
242-
Data.Clear();
243-
foreach (var (key, value) in values)
244-
{
245-
Data[key] = value;
246-
}
247-
248-
OnReload();
249-
}
250-
}
251236
}

tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests/Microsoft.Extensions.ServiceDiscovery.Dns.Tests.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<ItemGroup>
1010
<PackageReference Include="Microsoft.DotNet.XUnitExtensions" />
1111
<PackageReference Include="Microsoft.Extensions.Hosting" />
12+
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" />
1213
</ItemGroup>
1314

1415
<ItemGroup>

0 commit comments

Comments
 (0)