Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@
namespace Microsoft.Extensions.ServiceDiscovery.Http;

internal sealed class ServiceDiscoveryHttpMessageHandlerFactory(
TimeProvider timeProvider,
IServiceProvider serviceProvider,
ServiceEndpointWatcherFactory factory,
HttpServiceEndpointResolver resolver,
IOptions<ServiceDiscoveryOptions> options) : IServiceDiscoveryHttpMessageHandlerFactory
{
public HttpMessageHandler CreateHandler(HttpMessageHandler handler)
{
var registry = new HttpServiceEndpointResolver(factory, serviceProvider, timeProvider);
return new ResolvingHttpDelegatingHandler(registry, options, handler);
}
=> new ResolvingHttpDelegatingHandler(resolver, options, handler);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,9 @@ public static IHttpClientBuilder AddServiceDiscovery(this IHttpClientBuilder htt
services.AddServiceDiscoveryCore();
httpClientBuilder.AddHttpMessageHandler(services =>
{
var timeProvider = services.GetService<TimeProvider>() ?? TimeProvider.System;
var watcherFactory = services.GetRequiredService<ServiceEndpointWatcherFactory>();
var registry = new HttpServiceEndpointResolver(watcherFactory, services, timeProvider);
var resolver = services.GetRequiredService<HttpServiceEndpointResolver>();
var options = services.GetRequiredService<IOptions<ServiceDiscoveryOptions>>();
return new ResolvingHttpDelegatingHandler(registry, options);
return new ResolvingHttpDelegatingHandler(resolver, options);
});

#if NET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,13 @@ public static IServiceCollection AddServiceDiscoveryCore(this IServiceCollection
services.TryAddSingleton<ServiceEndpointWatcherFactory>();
services.TryAddSingleton<IServiceDiscoveryHttpMessageHandlerFactory, ServiceDiscoveryHttpMessageHandlerFactory>();
services.TryAddSingleton(sp => new ServiceEndpointResolver(sp.GetRequiredService<ServiceEndpointWatcherFactory>(), sp.GetRequiredService<TimeProvider>()));

// Registered as a singleton (rather than created per HTTP message handler) so its refresh
// timers and configuration change-token subscriptions are created once and disposed with the
// container, instead of leaking on every handler rotation. It has no per-client state and
// caches watchers per service name internally, so a single shared instance is correct.
services.TryAddSingleton(sp => new HttpServiceEndpointResolver(sp.GetRequiredService<ServiceEndpointWatcherFactory>(), sp, sp.GetRequiredService<TimeProvider>()));

if (configureOptions is not null)
{
services.Configure(configureOptions);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.ServiceDiscovery.Http;
using Xunit;

namespace Microsoft.Extensions.ServiceDiscovery.Tests;

/// <summary>
/// Tests that a single <see cref="HttpServiceEndpointResolver"/> is shared across HTTP message handlers
/// rather than created per handler build. Creating one per build leaks the resolver (an
/// <see cref="IAsyncDisposable"/> that roots refresh timers and configuration change-token subscriptions),
/// because <see cref="ResolvingHttpDelegatingHandler"/> never disposes it.
/// </summary>
public class HttpServiceEndpointResolverSharingTests
{
[Fact]
public async Task AddServiceDiscoveryCore_RegistersResolverAsSingleton()
{
await using var services = new ServiceCollection()
.AddServiceDiscoveryCore()
.BuildServiceProvider();

var first = services.GetRequiredService<HttpServiceEndpointResolver>();
var second = services.GetRequiredService<HttpServiceEndpointResolver>();

Assert.Same(first, second);
}

[Fact]
public async Task AddServiceDiscovery_HttpClient_HandlerUsesSharedResolverSingleton()
{
var services = new ServiceCollection();
services.AddHttpClient("test").AddServiceDiscovery();
await using var provider = services.BuildServiceProvider();

using var handler = provider.GetRequiredService<IHttpMessageHandlerFactory>().CreateHandler("test");
var resolvingHandler = FindHandler<ResolvingHttpDelegatingHandler>(handler);
Assert.NotNull(resolvingHandler);

var resolverField = typeof(ResolvingHttpDelegatingHandler)
.GetField("_resolver", BindingFlags.Instance | BindingFlags.NonPublic);
var usedResolver = resolverField!.GetValue(resolvingHandler);

Assert.Same(provider.GetRequiredService<HttpServiceEndpointResolver>(), usedResolver);
}

private static T? FindHandler<T>(HttpMessageHandler handler)
where T : HttpMessageHandler
{
for (var current = handler; current is not null; current = (current as DelegatingHandler)?.InnerHandler)
{
if (current is T match)
{
return match;
}
}

return null;
}
}
Loading