Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
17 changes: 10 additions & 7 deletions src/Servers/Kestrel/Core/src/KestrelConfigurationLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Server.Kestrel;

Expand All @@ -18,7 +19,7 @@ public class KestrelConfigurationLoader
{
private readonly IHttpsConfigurationService _httpsConfigurationService;

private bool _loaded;
private IChangeToken? _reloadToken;

internal KestrelConfigurationLoader(
KestrelServerOptions options,
Expand Down Expand Up @@ -234,25 +235,27 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions)
/// </summary>
public void Load()
{
if (_loaded)
if (_reloadToken is null || _reloadToken.HasChanged)
{
// The loader has already been run.
return;
// Will update _reloadToken
_ = Reload();
}
_loaded = true;

Reload();

foreach (var action in EndpointsToAdd)
{
action();
}

// If Load is called again, we don't want to rerun these
EndpointsToAdd.Clear();
}

// Adds endpoints from config to KestrelServerOptions.ConfigurationBackedListenOptions and configures some other options.
// Any endpoints that were removed from the last time endpoints were loaded are returned.
internal (List<ListenOptions>, List<ListenOptions>) Reload()
{
_reloadToken = Configuration.GetReloadToken();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
_reloadToken = Configuration.GetReloadToken();
_reloadToken = ReloadOnChange ? Configuration.GetReloadToken() : NullChangeToken.Singleton;

We should not grab the token on load if KestrelConfigurationLoader.ReloadOnChange is false. That's what the is DoesNotReloadOnConfigurationChangeByDefault test is about. Normally it's true if you have a "default" host like WebApplication.

You'll need to add using Microsoft.Extensions.FileProviders too. Not the best namespace there.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't change the existing IChangeToken handling, so I believe that test is still passing. I intentionally decoupled rebind-on-change from determine-if-reloading-would-be-redundant, but I'm open to discussion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why am I pulling in Microsoft.Extensions.FileProviders?


var endpointsToStop = Options.ConfigurationBackedListenOptions.ToList();
var endpointsToStart = new List<ListenOptions>();
var endpointsToReuse = new List<ListenOptions>();
Expand Down
2 changes: 1 addition & 1 deletion src/Servers/Kestrel/Core/test/KestrelServerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -958,7 +958,7 @@ public async Task DoesNotReloadOnConfigurationChangeByDefault()
mockTransportFactory.Verify(f => f.BindAsync(new IPEndPoint(IPAddress.IPv6Any, 5000), It.IsAny<CancellationToken>()), Times.Once);
mockTransportFactory.Verify(f => f.BindAsync(new IPEndPoint(IPAddress.IPv6Any, 5001), It.IsAny<CancellationToken>()), Times.Once);

mockConfig.Verify(c => c.GetReloadToken(), Times.Never);
mockConfig.Verify(c => c.GetReloadToken(), Times.Once); // Grabbed on Load

await server.StopAsync(CancellationToken.None).DefaultTimeout();
}
Expand Down
120 changes: 120 additions & 0 deletions src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Metrics;
using Microsoft.Extensions.Primitives;
using Moq;

namespace Microsoft.AspNetCore.Server.Kestrel.Tests;

Expand Down Expand Up @@ -1224,6 +1226,124 @@ public void Reload_RerunsNamedEndpointConfigurationOnChange()
Assert.Equal(1, foundUnchangedCount);
}

[Fact]
public void MultipleLoads_ConfigureBetween()
{
var currentConfig = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:A:Url", "http://*:5000"),
new KeyValuePair<string, string>("Endpoints:B:Url", "http://*:5001"),
}).Build();

var mockConfig = new Mock<IConfiguration>();
mockConfig.Setup(c => c.GetSection(It.IsAny<string>())).Returns<string>(currentConfig.GetSection);
mockConfig.Setup(c => c.GetChildren()).Returns(currentConfig.GetChildren);

var serverOptions = CreateServerOptions();
serverOptions.Configure(mockConfig.Object, reloadOnChange: false);
var oldConfigurationLoader = serverOptions.ConfigurationLoader;

serverOptions.ConfigurationLoader.Load();

mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.AtLeastOnce);

mockConfig.Invocations.Clear();

serverOptions.Configure(mockConfig.Object, reloadOnChange: false);
var newConfigurationLoader = serverOptions.ConfigurationLoader;
Assert.NotSame(oldConfigurationLoader, newConfigurationLoader);

serverOptions.ConfigurationLoader.Load();

// The change token is stored on the loader, so replacing the loader replaces the token.
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.AtLeastOnce);
}

[Theory]
[InlineData(true, true)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(false, false)]
public void MultipleLoads_ConfigurationChanges(bool changeBeforeInitialLoad, bool changeAfterInitialLoad)
{
var currentConfig = new ConfigurationBuilder().AddInMemoryCollection(new[]
{
new KeyValuePair<string, string>("Endpoints:A:Url", "http://*:5000"),
new KeyValuePair<string, string>("Endpoints:B:Url", "http://*:5001"),
}).Build();

var mockReloadToken = new Mock<IChangeToken>();
mockReloadToken.SetupGet(t => t.HasChanged).Returns(changeBeforeInitialLoad);

var mockConfig = new Mock<IConfiguration>();
mockConfig.Setup(c => c.GetSection(It.IsAny<string>())).Returns<string>(currentConfig.GetSection);
mockConfig.Setup(c => c.GetChildren()).Returns(currentConfig.GetChildren);
mockConfig.Setup(c => c.GetReloadToken()).Returns(mockReloadToken.Object);

var serverOptions = CreateServerOptions();
serverOptions.Configure(mockConfig.Object, reloadOnChange: false);

serverOptions.ConfigurationLoader.Load();

mockReloadToken.VerifyGet(t => t.HasChanged, Times.Never);
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), Times.AtLeastOnce);

mockReloadToken.SetupGet(t => t.HasChanged).Returns(changeAfterInitialLoad);

mockReloadToken.Invocations.Clear();
mockConfig.Invocations.Clear();

serverOptions.ConfigurationLoader.Load();

mockReloadToken.VerifyGet(t => t.HasChanged, Times.AtLeastOnce);
mockConfig.Verify(c => c.GetSection(It.IsAny<string>()), changeAfterInitialLoad ? Times.AtLeastOnce : Times.Never);
}

[Theory]
[InlineData(0, 0)]
[InlineData(3, 0)]
[InlineData(2, 1)]
[InlineData(1, 2)]
[InlineData(0, 3)]
public void MultipleLoads_AddEndpoints(int beforeInitialLoadCount, int afterInitialLoadCount)
{
int _endpointAddedCount = 0;

var serverOptions = CreateServerOptions();
serverOptions.Configure();

for (int i = 0; i < beforeInitialLoadCount; i++)
{
serverOptions.ConfigurationLoader.LocalhostEndpoint(5000 + i, _ => _endpointAddedCount++);
}

serverOptions.ConfigurationLoader.Load();

Assert.Equal(beforeInitialLoadCount, _endpointAddedCount);

for (int i = 0; i < afterInitialLoadCount; i++)
{
serverOptions.ConfigurationLoader.LocalhostEndpoint(7000 + i, _ => _endpointAddedCount++);
}

serverOptions.ConfigurationLoader.Load();

Assert.Equal(beforeInitialLoadCount + afterInitialLoadCount, _endpointAddedCount);
}

[Fact]
public void ReloadDoesNotAddEndpoints()
{
var serverOptions = CreateServerOptions();
serverOptions.Configure();

serverOptions.ConfigurationLoader.Load();

serverOptions.ConfigurationLoader.LocalhostEndpoint(7000, _ => Assert.Fail("New endpoints should not be added by Reload"));

_ = serverOptions.ConfigurationLoader.Reload();
}

private static string GetCertificatePath()
{
var appData = Environment.GetEnvironmentVariable("APPDATA");
Expand Down