Skip to content

Commit 100823a

Browse files
authored
Kestrel reloadable endpoint config (#21072)
1 parent 9025f63 commit 100823a

38 files changed

+1422
-417
lines changed

src/Servers/Kestrel/Core/ref/Microsoft.AspNetCore.Server.Kestrel.Core.netcoreapp.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ public KestrelServerOptions() { }
137137
public Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServerLimits Limits { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } }
138138
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure() { throw null; }
139139
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config) { throw null; }
140+
public Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader Configure(Microsoft.Extensions.Configuration.IConfiguration config, bool reloadOnChange) { throw null; }
140141
public void ConfigureEndpointDefaults(System.Action<Microsoft.AspNetCore.Server.Kestrel.Core.ListenOptions> configureOptions) { }
141142
public void ConfigureHttpsDefaults(System.Action<Microsoft.AspNetCore.Server.Kestrel.Https.HttpsConnectionAdapterOptions> configureOptions) { }
142143
public void Listen(System.Net.EndPoint endPoint) { }

src/Servers/Kestrel/Core/src/Internal/AddressBindContext.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -10,8 +10,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
1010
{
1111
internal class AddressBindContext
1212
{
13-
public ICollection<string> Addresses { get; set; }
14-
public List<ListenOptions> ListenOptions { get; set; }
13+
public ServerAddressesFeature ServerAddressesFeature { get; set; }
14+
public ICollection<string> Addresses => ServerAddressesFeature.InternalCollection;
15+
1516
public KestrelServerOptions ServerOptions { get; set; }
1617
public ILogger Logger { get; set; }
1718

src/Servers/Kestrel/Core/src/Internal/AddressBinder.cs

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
1919
{
2020
internal class AddressBinder
2121
{
22-
public static async Task BindAsync(IServerAddressesFeature addresses,
23-
KestrelServerOptions serverOptions,
24-
ILogger logger,
25-
Func<ListenOptions, Task> createBinding)
22+
public static async Task BindAsync(IEnumerable<ListenOptions> listenOptions, AddressBindContext context)
2623
{
27-
var listenOptions = serverOptions.ListenOptions;
2824
var strategy = CreateStrategy(
2925
listenOptions.ToArray(),
30-
addresses.Addresses.ToArray(),
31-
addresses.PreferHostingUrls);
32-
33-
var context = new AddressBindContext
34-
{
35-
Addresses = addresses.Addresses,
36-
ListenOptions = listenOptions,
37-
ServerOptions = serverOptions,
38-
Logger = logger,
39-
CreateBinding = createBinding
40-
};
26+
context.Addresses.ToArray(),
27+
context.ServerAddressesFeature.PreferHostingUrls);
4128

4229
// reset options. The actual used options and addresses will be populated
4330
// by the address binding feature
44-
listenOptions.Clear();
45-
addresses.Addresses.Clear();
31+
context.ServerOptions.OptionsInUse.Clear();
32+
context.Addresses.Clear();
4633

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

112-
context.ListenOptions.Add(endpoint);
99+
context.ServerOptions.OptionsInUse.Add(endpoint);
113100
}
114101

115102
internal static ListenOptions ParseAddress(string address, out bool https)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#nullable enable
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using Microsoft.Extensions.Configuration;
10+
11+
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
12+
{
13+
internal class ConfigSectionClone
14+
{
15+
public ConfigSectionClone(IConfigurationSection configSection)
16+
{
17+
Value = configSection.Value;
18+
19+
// GetChildren() should return an empty IEnumerable instead of null, but we guard against it since it's a public interface.
20+
var children = configSection.GetChildren() ?? Enumerable.Empty<IConfigurationSection>();
21+
Children = children.ToDictionary(child => child.Key, child => new ConfigSectionClone(child));
22+
}
23+
24+
public string Value { get; }
25+
public Dictionary<string, ConfigSectionClone> Children { get; }
26+
27+
public override bool Equals(object? obj)
28+
{
29+
if (!(obj is ConfigSectionClone other))
30+
{
31+
return false;
32+
}
33+
34+
if (Value != other.Value || Children.Count != other.Children.Count)
35+
{
36+
return false;
37+
}
38+
39+
foreach (var kvp in Children)
40+
{
41+
if (!other.Children.TryGetValue(kvp.Key, out var child))
42+
{
43+
return false;
44+
}
45+
46+
if (kvp.Value != child)
47+
{
48+
return false;
49+
}
50+
}
51+
52+
return true;
53+
}
54+
55+
public override int GetHashCode() => HashCode.Combine(Value, Children.Count);
56+
57+
public static bool operator ==(ConfigSectionClone lhs, ConfigSectionClone rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
58+
public static bool operator !=(ConfigSectionClone lhs, ConfigSectionClone rhs) => !(lhs == rhs);
59+
}
60+
}

src/Servers/Kestrel/Core/src/Internal/ConfigurationReader.cs

Lines changed: 67 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -17,95 +17,50 @@ internal class ConfigurationReader
1717
private const string UrlKey = "Url";
1818
private const string Latin1RequestHeadersKey = "Latin1RequestHeaders";
1919

20-
private IConfiguration _configuration;
21-
private IDictionary<string, CertificateConfig> _certificates;
22-
private IList<EndpointConfig> _endpoints;
23-
private EndpointDefaults _endpointDefaults;
24-
private bool? _latin1RequestHeaders;
20+
private readonly IConfiguration _configuration;
2521

2622
public ConfigurationReader(IConfiguration configuration)
2723
{
2824
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
25+
Certificates = ReadCertificates();
26+
EndpointDefaults = ReadEndpointDefaults();
27+
Endpoints = ReadEndpoints();
28+
Latin1RequestHeaders = _configuration.GetValue<bool>(Latin1RequestHeadersKey);
2929
}
3030

31-
public IDictionary<string, CertificateConfig> Certificates
32-
{
33-
get
34-
{
35-
if (_certificates == null)
36-
{
37-
ReadCertificates();
38-
}
39-
40-
return _certificates;
41-
}
42-
}
43-
44-
public EndpointDefaults EndpointDefaults
45-
{
46-
get
47-
{
48-
if (_endpointDefaults == null)
49-
{
50-
ReadEndpointDefaults();
51-
}
52-
53-
return _endpointDefaults;
54-
}
55-
}
56-
57-
public IEnumerable<EndpointConfig> Endpoints
58-
{
59-
get
60-
{
61-
if (_endpoints == null)
62-
{
63-
ReadEndpoints();
64-
}
65-
66-
return _endpoints;
67-
}
68-
}
31+
public IDictionary<string, CertificateConfig> Certificates { get; }
32+
public EndpointDefaults EndpointDefaults { get; }
33+
public IEnumerable<EndpointConfig> Endpoints { get; }
34+
public bool Latin1RequestHeaders { get; }
6935

70-
public bool Latin1RequestHeaders
36+
private IDictionary<string, CertificateConfig> ReadCertificates()
7137
{
72-
get
73-
{
74-
if (_latin1RequestHeaders is null)
75-
{
76-
_latin1RequestHeaders = _configuration.GetValue<bool>(Latin1RequestHeadersKey);
77-
}
78-
79-
return _latin1RequestHeaders.Value;
80-
}
81-
}
82-
83-
private void ReadCertificates()
84-
{
85-
_certificates = new Dictionary<string, CertificateConfig>(0);
38+
var certificates = new Dictionary<string, CertificateConfig>(0);
8639

8740
var certificatesConfig = _configuration.GetSection(CertificatesKey).GetChildren();
8841
foreach (var certificateConfig in certificatesConfig)
8942
{
90-
_certificates.Add(certificateConfig.Key, new CertificateConfig(certificateConfig));
43+
certificates.Add(certificateConfig.Key, new CertificateConfig(certificateConfig));
9144
}
45+
46+
return certificates;
9247
}
9348

9449
// "EndpointDefaults": {
9550
// "Protocols": "Http1AndHttp2",
9651
// }
97-
private void ReadEndpointDefaults()
52+
private EndpointDefaults ReadEndpointDefaults()
9853
{
9954
var configSection = _configuration.GetSection(EndpointDefaultsKey);
100-
_endpointDefaults = new EndpointDefaults
55+
return new EndpointDefaults
10156
{
10257
Protocols = ParseProtocols(configSection[ProtocolsKey])
10358
};
10459
}
10560

106-
private void ReadEndpoints()
61+
private IEnumerable<EndpointConfig> ReadEndpoints()
10762
{
108-
_endpoints = new List<EndpointConfig>();
63+
var endpoints = new List<EndpointConfig>();
10964

11065
var endpointsConfig = _configuration.GetSection(EndpointsKey).GetChildren();
11166
foreach (var endpointConfig in endpointsConfig)
@@ -133,8 +88,11 @@ private void ReadEndpoints()
13388
ConfigSection = endpointConfig,
13489
Certificate = new CertificateConfig(endpointConfig.GetSection(CertificateKey)),
13590
};
136-
_endpoints.Add(endpoint);
91+
92+
endpoints.Add(endpoint);
13793
}
94+
95+
return endpoints;
13896
}
13997

14098
private static HttpProtocols? ParseProtocols(string protocols)
@@ -154,7 +112,6 @@ private void ReadEndpoints()
154112
internal class EndpointDefaults
155113
{
156114
public HttpProtocols? Protocols { get; set; }
157-
public IConfigurationSection ConfigSection { get; set; }
158115
}
159116

160117
// "EndpointName": {
@@ -167,11 +124,41 @@ internal class EndpointDefaults
167124
// }
168125
internal class EndpointConfig
169126
{
127+
private IConfigurationSection _configSection;
128+
private ConfigSectionClone _configSectionClone;
129+
170130
public string Name { get; set; }
171131
public string Url { get; set; }
172132
public HttpProtocols? Protocols { get; set; }
173-
public IConfigurationSection ConfigSection { get; set; }
174133
public CertificateConfig Certificate { get; set; }
134+
135+
// Compare config sections because it's accessible to app developers via an Action<EndpointConfiguration> callback.
136+
// We cannot rely entirely on comparing config sections for equality, because KestrelConfigurationLoader.Reload() sets
137+
// EndpointConfig properties to their default values. If a default value changes, the properties would no longer be equal,
138+
// but the config sections could still be equal.
139+
public IConfigurationSection ConfigSection
140+
{
141+
get => _configSection;
142+
set
143+
{
144+
_configSection = value;
145+
// The IConfigrationSection will mutate, so we need to take a snapshot to compare against later and check for changes.
146+
_configSectionClone = new ConfigSectionClone(value);
147+
}
148+
}
149+
150+
public override bool Equals(object obj) =>
151+
obj is EndpointConfig other &&
152+
Name == other.Name &&
153+
Url == other.Url &&
154+
(Protocols ?? ListenOptions.DefaultHttpProtocols) == (other.Protocols ?? ListenOptions.DefaultHttpProtocols) &&
155+
Certificate == other.Certificate &&
156+
_configSectionClone == other._configSectionClone;
157+
158+
public override int GetHashCode() => HashCode.Combine(Name, Url, Protocols ?? ListenOptions.DefaultHttpProtocols, Certificate, _configSectionClone);
159+
160+
public static bool operator ==(EndpointConfig lhs, EndpointConfig rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
161+
public static bool operator !=(EndpointConfig lhs, EndpointConfig rhs) => !(lhs == rhs);
175162
}
176163

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

208195
public bool? AllowInvalid { get; set; }
196+
197+
public override bool Equals(object obj) =>
198+
obj is CertificateConfig other &&
199+
Path == other.Path &&
200+
Password == other.Password &&
201+
Subject == other.Subject &&
202+
Store == other.Store &&
203+
Location == other.Location &&
204+
(AllowInvalid ?? false) == (other.AllowInvalid ?? false);
205+
206+
public override int GetHashCode() => HashCode.Combine(Path, Password, Subject, Store, Location, AllowInvalid ?? false);
207+
208+
public static bool operator ==(CertificateConfig lhs, CertificateConfig rhs) => lhs is null ? rhs is null : lhs.Equals(rhs);
209+
public static bool operator !=(CertificateConfig lhs, CertificateConfig rhs) => !(lhs == rhs);
209210
}
210211
}

src/Servers/Kestrel/Core/src/Internal/ConnectionDispatcher.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,31 @@
1010

1111
namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal
1212
{
13-
internal class ConnectionDispatcher
13+
internal class ConnectionDispatcher<T> where T : BaseConnectionContext
1414
{
1515
private static long _lastConnectionId = long.MinValue;
1616

1717
private readonly ServiceContext _serviceContext;
18-
private readonly ConnectionDelegate _connectionDelegate;
18+
private readonly Func<T, Task> _connectionDelegate;
19+
private readonly TransportConnectionManager _transportConnectionManager;
1920
private readonly TaskCompletionSource<object> _acceptLoopTcs = new TaskCompletionSource<object>(TaskCreationOptions.RunContinuationsAsynchronously);
2021

21-
public ConnectionDispatcher(ServiceContext serviceContext, ConnectionDelegate connectionDelegate)
22+
public ConnectionDispatcher(ServiceContext serviceContext, Func<T, Task> connectionDelegate, TransportConnectionManager transportConnectionManager)
2223
{
2324
_serviceContext = serviceContext;
2425
_connectionDelegate = connectionDelegate;
26+
_transportConnectionManager = transportConnectionManager;
2527
}
2628

2729
private IKestrelTrace Log => _serviceContext.Log;
2830

29-
public Task StartAcceptingConnections(IConnectionListener listener)
31+
public Task StartAcceptingConnections(IConnectionListener<T> listener)
3032
{
3133
ThreadPool.UnsafeQueueUserWorkItem(StartAcceptingConnectionsCore, listener, preferLocal: false);
3234
return _acceptLoopTcs.Task;
3335
}
3436

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

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

58-
_serviceContext.ConnectionManager.AddConnection(id, kestrelConnection);
61+
_transportConnectionManager.AddConnection(id, kestrelConnection);
5962

6063
Log.ConnectionAccepted(connection.ConnectionId);
6164

0 commit comments

Comments
 (0)