Skip to content

Kestrel reloadable endpoint config #21072

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 1, 2020
Merged
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 @@ -137,6 +137,7 @@ public KestrelServerOptions() { }
public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; }
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; }
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config, bool reloadOnChange) { throw null; }
public void ConfigureEndpointDefaults(System.Action<Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions> configureOptions) { }
public void ConfigureHttpsDefaults(System.Action<Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions> configureOptions) { }
public void Listen(System.Net.EndPoint endPoint) { }
Expand Down
7 changes: 4 additions & 3 deletions src/Servers/Kestrel/Core/src/Internal/AddressBindContext.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
Expand All @@ -10,8 +10,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
internal class AddressBindContext
{
public ICollection<string> Addresses { get; set; }
public List<ListenOptions> ListenOptions { get; set; }
public ServerAddressesFeature ServerAddressesFeature { get; set; }
public ICollection<string> Addresses => ServerAddressesFeature.InternalCollection;

public KestrelServerOptions ServerOptions { get; set; }
public ILogger Logger { get; set; }

Expand Down
25 changes: 6 additions & 19 deletions src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,30 +19,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
internal class AddressBinder
{
public static async Task BindAsync(IServerAddressesFeature addresses,
KestrelServerOptions serverOptions,
ILogger logger,
Func<ListenOptions, Task> createBinding)
public static async Task BindAsync(IEnumerable<ListenOptions> listenOptions, AddressBindContext context)
{
var listenOptions = serverOptions.ListenOptions;
var strategy = CreateStrategy(
listenOptions.ToArray(),
addresses.Addresses.ToArray(),
addresses.PreferHostingUrls);

var context = new AddressBindContext
{
Addresses = addresses.Addresses,
ListenOptions = listenOptions,
ServerOptions = serverOptions,
Logger = logger,
CreateBinding = createBinding
};
context.Addresses.ToArray(),
context.ServerAddressesFeature.PreferHostingUrls);

// reset options. The actual used options and addresses will be populated
// by the address binding feature
listenOptions.Clear();
addresses.Addresses.Clear();
context.ServerOptions.OptionsInUse.Clear();
context.Addresses.Clear();

await strategy.BindAsync(context).ConfigureAwait(false);
}
Expand Down Expand Up @@ -109,7 +96,7 @@ internal static async Task BindEndpointAsync(ListenOptions endpoint, AddressBind
throw new IOException(CoreStrings.FormatEndpointAlreadyInUse(endpoint), ex);
}

context.ListenOptions.Add(endpoint);
context.ServerOptions.OptionsInUse.Add(endpoint);
}

internal static ListenOptions ParseAddress(string address, out bool https)
Expand Down
60 changes: 60 additions & 0 deletions src/Servers/Kestrel/Core/src/Internal/ConfigSectionClone.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Configuration;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
internal class ConfigSectionClone
{
public ConfigSectionClone(IConfigurationSection configSection)
{
Value = configSection.Value;

// GetChildren() should return an empty IEnumerable instead of null, but we guard against it since it's a public interface.
var children = configSection.GetChildren() ?? Enumerable.Empty<IConfigurationSection>();
Children = children.ToDictionary(child => child.Key, child => new ConfigSectionClone(child));
}

public string Value { get; }
public Dictionary<string, ConfigSectionClone> Children { get; }

public override bool Equals(object? obj)
{
if (!(obj is ConfigSectionClone other))
{
return false;
}

if (Value != other.Value || Children.Count != other.Children.Count)
{
return false;
}

foreach (var kvp in Children)
{
if (!other.Children.TryGetValue(kvp.Key, out var child))
{
return false;
}

if (kvp.Value != child)
{
return false;
}
}

return true;
}

public override int GetHashCode() => HashCode.Combine(Value, Children.Count);

public static bool operator ==(ConfigSectionClone lhs, ConfigSectionClone rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
public static bool operator !=(ConfigSectionClone lhs, ConfigSectionClone rhs) => !(lhs == rhs);
}
}
133 changes: 67 additions & 66 deletions src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,95 +17,50 @@ internal class ConfigurationReader
private const string UrlKey = "Url";
private const string Latin1RequestHeadersKey = "Latin1RequestHeaders";

private IConfiguration _configuration;
private IDictionary<string, CertificateConfig> _certificates;
private IList<EndpointConfig> _endpoints;
private EndpointDefaults _endpointDefaults;
private bool? _latin1RequestHeaders;
private readonly IConfiguration _configuration;

public ConfigurationReader(IConfiguration configuration)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
Certificates = ReadCertificates();
EndpointDefaults = ReadEndpointDefaults();
Endpoints = ReadEndpoints();
Latin1RequestHeaders = _configuration.GetValue<bool>(Latin1RequestHeadersKey);
}

public IDictionary<string, CertificateConfig> Certificates
{
get
{
if (_certificates == null)
{
ReadCertificates();
}

return _certificates;
}
}

public EndpointDefaults EndpointDefaults
{
get
{
if (_endpointDefaults == null)
{
ReadEndpointDefaults();
}

return _endpointDefaults;
}
}

public IEnumerable<EndpointConfig> Endpoints
{
get
{
if (_endpoints == null)
{
ReadEndpoints();
}

return _endpoints;
}
}
public IDictionary<string, CertificateConfig> Certificates { get; }
public EndpointDefaults EndpointDefaults { get; }
public IEnumerable<EndpointConfig> Endpoints { get; }
public bool Latin1RequestHeaders { get; }

public bool Latin1RequestHeaders
private IDictionary<string, CertificateConfig> ReadCertificates()
{
get
{
if (_latin1RequestHeaders is null)
{
_latin1RequestHeaders = _configuration.GetValue<bool>(Latin1RequestHeadersKey);
}

return _latin1RequestHeaders.Value;
}
}

private void ReadCertificates()
{
_certificates = new Dictionary<string, CertificateConfig>(0);
var certificates = new Dictionary<string, CertificateConfig>(0);

var certificatesConfig = _configuration.GetSection(CertificatesKey).GetChildren();
foreach (var certificateConfig in certificatesConfig)
{
_certificates.Add(certificateConfig.Key, new CertificateConfig(certificateConfig));
certificates.Add(certificateConfig.Key, new CertificateConfig(certificateConfig));
}

return certificates;
}

// "EndpointDefaults": {
// "Protocols": "Http1AndHttp2",
// }
private void ReadEndpointDefaults()
private EndpointDefaults ReadEndpointDefaults()
{
var configSection = _configuration.GetSection(EndpointDefaultsKey);
_endpointDefaults = new EndpointDefaults
return new EndpointDefaults
{
Protocols = ParseProtocols(configSection[ProtocolsKey])
};
}

private void ReadEndpoints()
private IEnumerable<EndpointConfig> ReadEndpoints()
{
_endpoints = new List<EndpointConfig>();
var endpoints = new List<EndpointConfig>();

var endpointsConfig = _configuration.GetSection(EndpointsKey).GetChildren();
foreach (var endpointConfig in endpointsConfig)
Expand Down Expand Up @@ -133,8 +88,11 @@ private void ReadEndpoints()
ConfigSection = endpointConfig,
Certificate = new CertificateConfig(endpointConfig.GetSection(CertificateKey)),
};
_endpoints.Add(endpoint);

endpoints.Add(endpoint);
}

return endpoints;
}

private static HttpProtocols? ParseProtocols(string protocols)
Expand All @@ -154,7 +112,6 @@ private void ReadEndpoints()
internal class EndpointDefaults
{
public HttpProtocols? Protocols { get; set; }
public IConfigurationSection ConfigSection { get; set; }
}

// "EndpointName": {
Expand All @@ -167,11 +124,41 @@ internal class EndpointDefaults
// }
internal class EndpointConfig
{
private IConfigurationSection _configSection;
private ConfigSectionClone _configSectionClone;

public string Name { get; set; }
public string Url { get; set; }
public HttpProtocols? Protocols { get; set; }
public IConfigurationSection ConfigSection { get; set; }
public CertificateConfig Certificate { get; set; }

// Compare config sections because it's accessible to app developers via an Action<EndpointConfiguration> callback.
// We cannot rely entirely on comparing config sections for equality, because KestrelConfigurationLoader.Reload() sets
// EndpointConfig properties to their default values. If a default value changes, the properties would no longer be equal,
// but the config sections could still be equal.
public IConfigurationSection ConfigSection
{
get => _configSection;
set
{
_configSection = value;
// The IConfigrationSection will mutate, so we need to take a snapshot to compare against later and check for changes.
_configSectionClone = new ConfigSectionClone(value);
}
}

public override bool Equals(object obj) =>
obj is EndpointConfig other &&
Name == other.Name &&
Url == other.Url &&
(Protocols ?? ListenOptions.DefaultHttpProtocols) == (other.Protocols ?? ListenOptions.DefaultHttpProtocols) &&
Certificate == other.Certificate &&
_configSectionClone == other._configSectionClone;

public override int GetHashCode() => HashCode.Combine(Name, Url, Protocols ?? ListenOptions.DefaultHttpProtocols, Certificate, _configSectionClone);

public static bool operator ==(EndpointConfig lhs, EndpointConfig rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
public static bool operator !=(EndpointConfig lhs, EndpointConfig rhs) => !(lhs == rhs);
}

// "CertificateName": {
Expand Down Expand Up @@ -206,5 +193,19 @@ public CertificateConfig(IConfigurationSection configSection)
public string Location { get; set; }

public bool? AllowInvalid { get; set; }

public override bool Equals(object obj) =>
obj is CertificateConfig other &&
Path == other.Path &&
Password == other.Password &&
Subject == other.Subject &&
Store == other.Store &&
Location == other.Location &&
(AllowInvalid ?? false) == (other.AllowInvalid ?? false);

public override int GetHashCode() => HashCode.Combine(Path, Password, Subject, Store, Location, AllowInvalid ?? false);

public static bool operator ==(CertificateConfig lhs, CertificateConfig rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
public static bool operator !=(CertificateConfig lhs, CertificateConfig rhs) => !(lhs == rhs);
}
}
17 changes: 10 additions & 7 deletions src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,29 +10,31 @@

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
{
internal class ConnectionDispatcher
internal class ConnectionDispatcher<T> where T : BaseConnectionContext
{
private static long _lastConnectionId = long.MinValue;

private readonly ServiceContext _serviceContext;
private readonly ConnectionDelegate _connectionDelegate;
private readonly Func<T, Task> _connectionDelegate;
private readonly TransportConnectionManager _transportConnectionManager;
private readonly TaskCompletionSource<object> _acceptLoopTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);

public ConnectionDispatcher(ServiceContext serviceContext, ConnectionDelegate connectionDelegate)
public ConnectionDispatcher(ServiceContext serviceContext, Func<T, Task> connectionDelegate, TransportConnectionManager transportConnectionManager)
{
_serviceContext = serviceContext;
_connectionDelegate = connectionDelegate;
_transportConnectionManager = transportConnectionManager;
}

private IKestrelTrace Log => _serviceContext.Log;

public Task StartAcceptingConnections(IConnectionListener listener)
public Task StartAcceptingConnections(IConnectionListener<T> listener)
{
ThreadPool.UnsafeQueueUserWorkItem(StartAcceptingConnectionsCore, listener, preferLocal: false);
return _acceptLoopTcs.Task;
}

private void StartAcceptingConnectionsCore(IConnectionListener listener)
private void StartAcceptingConnectionsCore(IConnectionListener<T> listener)
{
// REVIEW: Multiple accept loops in parallel?
_ = AcceptConnectionsAsync();
Expand All @@ -53,9 +55,10 @@ async Task AcceptConnectionsAsync()

// Add the connection to the connection manager before we queue it for execution
var id = Interlocked.Increment(ref _lastConnectionId);
var kestrelConnection = new KestrelConnection<ConnectionContext>(id, _serviceContext, c => _connectionDelegate(c), connection, Log);
var kestrelConnection = new KestrelConnection<T>(
id, _serviceContext, _transportConnectionManager, _connectionDelegate, connection, Log);

_serviceContext.ConnectionManager.AddConnection(id, kestrelConnection);
_transportConnectionManager.AddConnection(id, kestrelConnection);

Log.ConnectionAccepted(connection.ConnectionId);

Expand Down
Loading