From d530c7e55d7bf542fae5457be45e929ee530ff12 Mon Sep 17 00:00:00 2001 From: DotNet Bot Date: Thu, 6 Jul 2023 08:45:31 +0000 Subject: [PATCH 01/10] [internal/release/6.0] Update dependencies from dnceng/internal/dotnet-efcore dnceng/internal/dotnet-runtime --- NuGet.config | 8 +++--- eng/Version.Details.xml | 62 ++++++++++++++++++++--------------------- eng/Versions.props | 30 ++++++++++---------- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/NuGet.config b/NuGet.config index 5fe2b4af9609..bed55ad23787 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,10 +4,10 @@ - + - + @@ -25,10 +25,10 @@ - + - + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index e05d62f7680e..c7b164140a1f 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -9,37 +9,37 @@ --> - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 16d152f81ed675c2705aa38b8d6d4027b6525602 + 06531f7f252f34b2e6c48e3179454d36864a416d - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 16d152f81ed675c2705aa38b8d6d4027b6525602 + 06531f7f252f34b2e6c48e3179454d36864a416d - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 16d152f81ed675c2705aa38b8d6d4027b6525602 + 06531f7f252f34b2e6c48e3179454d36864a416d - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 16d152f81ed675c2705aa38b8d6d4027b6525602 + 06531f7f252f34b2e6c48e3179454d36864a416d - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 16d152f81ed675c2705aa38b8d6d4027b6525602 + 06531f7f252f34b2e6c48e3179454d36864a416d - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 16d152f81ed675c2705aa38b8d6d4027b6525602 + 06531f7f252f34b2e6c48e3179454d36864a416d - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 16d152f81ed675c2705aa38b8d6d4027b6525602 + 06531f7f252f34b2e6c48e3179454d36864a416d - + https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 16d152f81ed675c2705aa38b8d6d4027b6525602 + 06531f7f252f34b2e6c48e3179454d36864a416d https://github.com/dotnet/runtime @@ -177,9 +177,9 @@ https://github.com/dotnet/runtime 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - a08d9ce2caf02455c0b825bcdc32974bdf769a80 + db1112f52a8581622202f02951e195e57421fc0f https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -245,40 +245,40 @@ https://github.com/dotnet/runtime 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - a08d9ce2caf02455c0b825bcdc32974bdf769a80 + db1112f52a8581622202f02951e195e57421fc0f - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - a08d9ce2caf02455c0b825bcdc32974bdf769a80 + db1112f52a8581622202f02951e195e57421fc0f - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - a08d9ce2caf02455c0b825bcdc32974bdf769a80 + db1112f52a8581622202f02951e195e57421fc0f - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - a08d9ce2caf02455c0b825bcdc32974bdf769a80 + db1112f52a8581622202f02951e195e57421fc0f - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - a08d9ce2caf02455c0b825bcdc32974bdf769a80 + db1112f52a8581622202f02951e195e57421fc0f - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - a08d9ce2caf02455c0b825bcdc32974bdf769a80 + db1112f52a8581622202f02951e195e57421fc0f https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - a08d9ce2caf02455c0b825bcdc32974bdf769a80 + db1112f52a8581622202f02951e195e57421fc0f https://github.com/dotnet/arcade diff --git a/eng/Versions.props b/eng/Versions.props index 9dc73dcfe989..999cc24d5443 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -63,12 +63,12 @@ 6.0.0 - 6.0.20 - 6.0.20 - 6.0.20 - 6.0.20 - 6.0.20 - 6.0.20-servicing.23320.17 + 6.0.21 + 6.0.21 + 6.0.21 + 6.0.21 + 6.0.21 + 6.0.21-servicing.23355.12 6.0.0 6.0.1 6.0.0 @@ -103,7 +103,7 @@ 6.0.0 6.0.0 6.0.0 - 6.0.20-servicing.23320.17 + 6.0.21-servicing.23355.12 6.0.1 6.0.0 6.0.2 @@ -122,14 +122,14 @@ 6.0.11 - 6.0.20 - 6.0.20 - 6.0.20 - 6.0.20 - 6.0.20 - 6.0.20 - 6.0.20 - 6.0.20 + 6.0.21 + 6.0.21 + 6.0.21 + 6.0.21 + 6.0.21 + 6.0.21 + 6.0.21 + 6.0.21 6.0.0-beta.23313.5 6.0.0-beta.23313.5 From e49d53e68b2ea8dd5e38b88fda2be587b540cace Mon Sep 17 00:00:00 2001 From: DotNet Bot Date: Thu, 6 Jul 2023 19:52:46 +0000 Subject: [PATCH 02/10] [internal/release/6.0] Update dependencies from dnceng/internal/dotnet-efcore --- NuGet.config | 4 ++-- eng/Version.Details.xml | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/NuGet.config b/NuGet.config index bed55ad23787..eb22c5afdc17 100644 --- a/NuGet.config +++ b/NuGet.config @@ -7,7 +7,7 @@ - + @@ -25,7 +25,7 @@ - + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index c7b164140a1f..52a5f4eb7e05 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -11,35 +11,35 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 06531f7f252f34b2e6c48e3179454d36864a416d + 54d936f07e28cfd2edda5a53de8e70739f6e9675 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 06531f7f252f34b2e6c48e3179454d36864a416d + 54d936f07e28cfd2edda5a53de8e70739f6e9675 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 06531f7f252f34b2e6c48e3179454d36864a416d + 54d936f07e28cfd2edda5a53de8e70739f6e9675 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 06531f7f252f34b2e6c48e3179454d36864a416d + 54d936f07e28cfd2edda5a53de8e70739f6e9675 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 06531f7f252f34b2e6c48e3179454d36864a416d + 54d936f07e28cfd2edda5a53de8e70739f6e9675 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 06531f7f252f34b2e6c48e3179454d36864a416d + 54d936f07e28cfd2edda5a53de8e70739f6e9675 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 06531f7f252f34b2e6c48e3179454d36864a416d + 54d936f07e28cfd2edda5a53de8e70739f6e9675 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 06531f7f252f34b2e6c48e3179454d36864a416d + 54d936f07e28cfd2edda5a53de8e70739f6e9675 https://github.com/dotnet/runtime From 79a2b3a29442351143c4a514450a317faf1411f0 Mon Sep 17 00:00:00 2001 From: Brennan Conroy Date: Thu, 6 Jul 2023 20:12:44 +0000 Subject: [PATCH 03/10] Merged PR 31898: Avoid Redis pattern matching Avoid Redis pattern matching --- .../src/Internal/RedisChannels.cs | 3 + .../src/RedisHubLifetimeManager.cs | 20 +++--- .../StackExchangeRedis/test/RedisEndToEnd.cs | 67 +++++++++++++++++++ .../test/RedisHubLifetimeManagerTests.cs | 20 ++++++ .../test/TestConnectionMultiplexer.cs | 27 ++++++++ 5 files changed, 127 insertions(+), 10 deletions(-) diff --git a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs index 6f973ea87ec9..2c783329b230 100644 --- a/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs +++ b/src/SignalR/server/StackExchangeRedis/src/Internal/RedisChannels.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.CompilerServices; +using StackExchange.Redis; namespace Microsoft.AspNetCore.SignalR.StackExchangeRedis.Internal { @@ -71,5 +72,7 @@ public string Ack(string serverName) { return _prefix + ":internal:ack:" + serverName; } + + public static RedisChannel GetChannel(string channelName) => new RedisChannel(channelName, RedisChannel.PatternMode.Literal); } } diff --git a/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs b/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs index ee9fa0e209fc..8007eb34f3f6 100644 --- a/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs +++ b/src/SignalR/server/StackExchangeRedis/src/RedisHubLifetimeManager.cs @@ -114,7 +114,7 @@ public override Task OnDisconnectedAsync(HubConnectionContext connection) var connectionChannel = _channels.Connection(connection.ConnectionId); RedisLog.Unsubscribe(_logger, connectionChannel); - tasks.Add(_bus!.UnsubscribeAsync(connectionChannel)); + tasks.Add(_bus!.UnsubscribeAsync(RedisChannels.GetChannel(connectionChannel))); var feature = connection.Features.Get()!; var groupNames = feature.Groups; @@ -315,7 +315,7 @@ private async Task PublishAsync(string channel, byte[] payload) { await EnsureRedisServerConnection(); RedisLog.PublishToChannel(_logger, channel); - await _bus!.PublishAsync(channel, payload); + await _bus!.PublishAsync(RedisChannels.GetChannel(channel), payload); } private Task AddGroupAsyncCore(HubConnectionContext connection, string groupName) @@ -347,7 +347,7 @@ private async Task RemoveGroupAsyncCore(HubConnectionContext connection, string await _groups.RemoveSubscriptionAsync(groupChannel, connection, channelName => { RedisLog.Unsubscribe(_logger, channelName); - return _bus!.UnsubscribeAsync(channelName); + return _bus!.UnsubscribeAsync(RedisChannels.GetChannel(channelName)); }); var feature = connection.Features.Get()!; @@ -379,7 +379,7 @@ private Task RemoveUserAsync(HubConnectionContext connection) return _users.RemoveSubscriptionAsync(userChannel, connection, channelName => { RedisLog.Unsubscribe(_logger, channelName); - return _bus!.UnsubscribeAsync(channelName); + return _bus!.UnsubscribeAsync(RedisChannels.GetChannel(channelName)); }); } @@ -396,7 +396,7 @@ public void Dispose() private async Task SubscribeToAll() { RedisLog.Subscribing(_logger, _channels.All); - var channel = await _bus!.SubscribeAsync(_channels.All); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(_channels.All)); channel.OnMessage(async channelMessage => { try @@ -426,7 +426,7 @@ private async Task SubscribeToAll() private async Task SubscribeToGroupManagementChannel() { - var channel = await _bus!.SubscribeAsync(_channels.GroupManagement); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(_channels.GroupManagement)); channel.OnMessage(async channelMessage => { try @@ -463,7 +463,7 @@ private async Task SubscribeToGroupManagementChannel() private async Task SubscribeToAckChannel() { // Create server specific channel in order to send an ack to a single server - var channel = await _bus!.SubscribeAsync(_channels.Ack(_serverName)); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(_channels.Ack(_serverName))); channel.OnMessage(channelMessage => { var ackId = _protocol.ReadAck((byte[])channelMessage.Message); @@ -477,7 +477,7 @@ private async Task SubscribeToConnection(HubConnectionContext connection) var connectionChannel = _channels.Connection(connection.ConnectionId); RedisLog.Subscribing(_logger, connectionChannel); - var channel = await _bus!.SubscribeAsync(connectionChannel); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(connectionChannel)); channel.OnMessage(channelMessage => { var invocation = _protocol.ReadInvocation((byte[])channelMessage.Message); @@ -492,7 +492,7 @@ private Task SubscribeToUser(HubConnectionContext connection) return _users.AddSubscriptionAsync(userChannel, connection, async (channelName, subscriptions) => { RedisLog.Subscribing(_logger, channelName); - var channel = await _bus!.SubscribeAsync(channelName); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(channelName)); channel.OnMessage(async channelMessage => { try @@ -518,7 +518,7 @@ private Task SubscribeToUser(HubConnectionContext connection) private async Task SubscribeToGroupAsync(string groupChannel, HubConnectionStore groupConnections) { RedisLog.Subscribing(_logger, groupChannel); - var channel = await _bus!.SubscribeAsync(groupChannel); + var channel = await _bus!.SubscribeAsync(RedisChannels.GetChannel(groupChannel)); channel.OnMessage(async (channelMessage) => { try diff --git a/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs b/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs index 2915168d27c8..c8a71ef2321d 100644 --- a/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs +++ b/src/SignalR/server/StackExchangeRedis/test/RedisEndToEnd.cs @@ -149,6 +149,73 @@ public async Task CanSendAndReceiveUserMessagesWhenOneConnectionWithUserDisconne } } + [ConditionalTheory] + [SkipIfDockerNotPresent] + [MemberData(nameof(TransportTypesAndProtocolTypes))] + public async Task HubConnectionCanSendAndReceiveGroupMessagesGroupNameWithPatternIsTreatedAsLiteral(HttpTransportType transportType, string protocolName) + { + using (StartVerifiableLog()) + { + var protocol = HubProtocolHelpers.GetHubProtocol(protocolName); + + var connection = CreateConnection(_serverFixture.FirstServer.Url + "/echo", transportType, protocol, LoggerFactory); + var secondConnection = CreateConnection(_serverFixture.SecondServer.Url + "/echo", transportType, protocol, LoggerFactory); + + var tcs = new TaskCompletionSource(); + connection.On("Echo", message => tcs.TrySetResult(message)); + var tcs2 = new TaskCompletionSource(); + secondConnection.On("Echo", message => tcs2.TrySetResult(message)); + + var groupName = $"TestGroup_{transportType}_{protocolName}_{Guid.NewGuid()}"; + + await secondConnection.StartAsync().DefaultTimeout(); + await connection.StartAsync().DefaultTimeout(); + await connection.InvokeAsync("AddSelfToGroup", "*").DefaultTimeout(); + await secondConnection.InvokeAsync("AddSelfToGroup", groupName).DefaultTimeout(); + await connection.InvokeAsync("EchoGroup", groupName, "Hello, World!").DefaultTimeout(); + + Assert.Equal("Hello, World!", await tcs2.Task.DefaultTimeout()); + Assert.False(tcs.Task.IsCompleted); + + await connection.InvokeAsync("EchoGroup", "*", "Hello, World!").DefaultTimeout(); + Assert.Equal("Hello, World!", await tcs.Task.DefaultTimeout()); + + await connection.DisposeAsync().DefaultTimeout(); + } + } + + [ConditionalTheory] + [SkipIfDockerNotPresent] + [MemberData(nameof(TransportTypesAndProtocolTypes))] + public async Task CanSendAndReceiveUserMessagesUserNameWithPatternIsTreatedAsLiteral(HttpTransportType transportType, string protocolName) + { + using (StartVerifiableLog()) + { + var protocol = HubProtocolHelpers.GetHubProtocol(protocolName); + + var connection = CreateConnection(_serverFixture.FirstServer.Url + "/echo", transportType, protocol, LoggerFactory, userName: "*"); + var secondConnection = CreateConnection(_serverFixture.SecondServer.Url + "/echo", transportType, protocol, LoggerFactory, userName: "userA"); + + var tcs = new TaskCompletionSource(); + connection.On("Echo", message => tcs.TrySetResult(message)); + var tcs2 = new TaskCompletionSource(); + secondConnection.On("Echo", message => tcs2.TrySetResult(message)); + + await secondConnection.StartAsync().DefaultTimeout(); + await connection.StartAsync().DefaultTimeout(); + await connection.InvokeAsync("EchoUser", "userA", "Hello, World!").DefaultTimeout(); + + Assert.Equal("Hello, World!", await tcs2.Task.DefaultTimeout()); + Assert.False(tcs.Task.IsCompleted); + + await connection.InvokeAsync("EchoUser", "*", "Hello, World!").DefaultTimeout(); + Assert.Equal("Hello, World!", await tcs.Task.DefaultTimeout()); + + await connection.DisposeAsync().DefaultTimeout(); + await secondConnection.DisposeAsync().DefaultTimeout(); + } + } + private static HubConnection CreateConnection(string url, HttpTransportType transportType, IHubProtocol protocol, ILoggerFactory loggerFactory, string userName = null) { var hubConnectionBuilder = new HubConnectionBuilder() diff --git a/src/SignalR/server/StackExchangeRedis/test/RedisHubLifetimeManagerTests.cs b/src/SignalR/server/StackExchangeRedis/test/RedisHubLifetimeManagerTests.cs index 7d9a86ee8c0b..5a399426b7e7 100644 --- a/src/SignalR/server/StackExchangeRedis/test/RedisHubLifetimeManagerTests.cs +++ b/src/SignalR/server/StackExchangeRedis/test/RedisHubLifetimeManagerTests.cs @@ -83,6 +83,26 @@ public async Task CamelCasedJsonIsPreservedAcrossRedisBoundary() } } + // Smoke test that Debug.Asserts in TestSubscriber aren't hit + [Fact] + public async Task PatternGroupAndUser() + { + var server = new TestRedisServer(); + using (var client = new TestClient()) + { + var manager = CreateLifetimeManager(server); + + var connection = HubConnectionContextUtils.Create(client.Connection); + connection.UserIdentifier = "*"; + + await manager.OnConnectedAsync(connection).DefaultTimeout(); + + var groupName = "*"; + + await manager.AddToGroupAsync(connection.ConnectionId, groupName).DefaultTimeout(); + } + } + public override TestRedisServer CreateBackplane() { return new TestRedisServer(); diff --git a/src/SignalR/server/StackExchangeRedis/test/TestConnectionMultiplexer.cs b/src/SignalR/server/StackExchangeRedis/test/TestConnectionMultiplexer.cs index e68ace5982c2..0e21b56a52b5 100644 --- a/src/SignalR/server/StackExchangeRedis/test/TestConnectionMultiplexer.cs +++ b/src/SignalR/server/StackExchangeRedis/test/TestConnectionMultiplexer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Net; using System.Reflection; @@ -11,6 +12,7 @@ using System.Threading.Tasks; using StackExchange.Redis; using StackExchange.Redis.Profiling; +using Xunit; namespace Microsoft.AspNetCore.SignalR.Tests { @@ -230,6 +232,8 @@ public class TestRedisServer public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { + AssertRedisChannel(channel); + if (_subscriptions.TryGetValue(channel, out var handlers)) { foreach (var (_, handler) in handlers) @@ -243,6 +247,8 @@ public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags public void Subscribe(ChannelMessageQueue messageQueue, int subscriberId, CommandFlags flags = CommandFlags.None) { + AssertRedisChannel(messageQueue.Channel); + Action handler = (channel, value) => { // Workaround for https://github.com/StackExchange/StackExchange.Redis/issues/969 @@ -260,11 +266,20 @@ public void Subscribe(ChannelMessageQueue messageQueue, int subscriberId, Comman public void Unsubscribe(RedisChannel channel, int subscriberId, CommandFlags flags = CommandFlags.None) { + AssertRedisChannel(channel); + if (_subscriptions.TryGetValue(channel, out var list)) { list.RemoveAll((item) => item.Item1 == subscriberId); } } + + internal static void AssertRedisChannel(RedisChannel channel) + { + var patternField = typeof(RedisChannel).GetField("IsPatternBased", BindingFlags.NonPublic | BindingFlags.Instance); + Assert.NotNull(patternField); + Assert.False((bool)patternField.GetValue(channel)); + } } public class TestSubscriber : ISubscriber @@ -310,11 +325,15 @@ public Task PingAsync(CommandFlags flags = CommandFlags.None) public long Publish(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + return _server.Publish(channel, message, flags); } public async Task PublishAsync(RedisChannel channel, RedisValue message, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + await Task.Yield(); return Publish(channel, message, flags); } @@ -326,6 +345,8 @@ public void Subscribe(RedisChannel channel, Action han public Task SubscribeAsync(RedisChannel channel, Action handler, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + Subscribe(channel, handler, flags); return Task.CompletedTask; } @@ -342,6 +363,8 @@ public bool TryWait(Task task) public void Unsubscribe(RedisChannel channel, Action handler = null, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + _server.Unsubscribe(channel, _id, flags); } @@ -357,6 +380,8 @@ public Task UnsubscribeAllAsync(CommandFlags flags = CommandFlags.None) public Task UnsubscribeAsync(RedisChannel channel, Action handler = null, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + Unsubscribe(channel, handler, flags); return Task.CompletedTask; } @@ -391,6 +416,8 @@ public ChannelMessageQueue Subscribe(RedisChannel channel, CommandFlags flags = public Task SubscribeAsync(RedisChannel channel, CommandFlags flags = CommandFlags.None) { + TestRedisServer.AssertRedisChannel(channel); + var t = Subscribe(channel, flags); return Task.FromResult(t); } From 311a4f964c4fc6d9a674e5befcbce30e50bb5e6c Mon Sep 17 00:00:00 2001 From: DotNet Bot Date: Tue, 11 Jul 2023 03:44:04 +0000 Subject: [PATCH 04/10] [internal/release/6.0] Update dependencies from dnceng/internal/dotnet-runtime --- NuGet.config | 4 ++-- eng/Version.Details.xml | 20 ++++++++++---------- eng/Versions.props | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/NuGet.config b/NuGet.config index eb22c5afdc17..ef73f074be4e 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,7 +4,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 52a5f4eb7e05..646ddb1307c6 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -177,9 +177,9 @@ https://github.com/dotnet/runtime 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - db1112f52a8581622202f02951e195e57421fc0f + 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -247,15 +247,15 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - db1112f52a8581622202f02951e195e57421fc0f + 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - db1112f52a8581622202f02951e195e57421fc0f + 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - db1112f52a8581622202f02951e195e57421fc0f + 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - db1112f52a8581622202f02951e195e57421fc0f + 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - db1112f52a8581622202f02951e195e57421fc0f + 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - db1112f52a8581622202f02951e195e57421fc0f + 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - db1112f52a8581622202f02951e195e57421fc0f + 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 https://github.com/dotnet/arcade diff --git a/eng/Versions.props b/eng/Versions.props index 999cc24d5443..f6560c1b5fa3 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -68,7 +68,7 @@ 6.0.21 6.0.21 6.0.21 - 6.0.21-servicing.23355.12 + 6.0.21-servicing.23360.16 6.0.0 6.0.1 6.0.0 @@ -103,7 +103,7 @@ 6.0.0 6.0.0 6.0.0 - 6.0.21-servicing.23355.12 + 6.0.21-servicing.23360.16 6.0.1 6.0.0 6.0.2 From b7bd64b3744869ecdd1760d40d1f6872494a603d Mon Sep 17 00:00:00 2001 From: DotNet Bot Date: Wed, 12 Jul 2023 00:34:36 +0000 Subject: [PATCH 05/10] [internal/release/6.0] Update dependencies from dnceng/internal/dotnet-efcore --- NuGet.config | 4 ++-- eng/Version.Details.xml | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/NuGet.config b/NuGet.config index ef73f074be4e..898e661d5e9a 100644 --- a/NuGet.config +++ b/NuGet.config @@ -7,7 +7,7 @@ - + @@ -25,7 +25,7 @@ - + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 646ddb1307c6..22d4d311255e 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -11,35 +11,35 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 54d936f07e28cfd2edda5a53de8e70739f6e9675 + 0d690b8b75b98568908726c87525c6cd4aa00362 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 54d936f07e28cfd2edda5a53de8e70739f6e9675 + 0d690b8b75b98568908726c87525c6cd4aa00362 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 54d936f07e28cfd2edda5a53de8e70739f6e9675 + 0d690b8b75b98568908726c87525c6cd4aa00362 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 54d936f07e28cfd2edda5a53de8e70739f6e9675 + 0d690b8b75b98568908726c87525c6cd4aa00362 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 54d936f07e28cfd2edda5a53de8e70739f6e9675 + 0d690b8b75b98568908726c87525c6cd4aa00362 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 54d936f07e28cfd2edda5a53de8e70739f6e9675 + 0d690b8b75b98568908726c87525c6cd4aa00362 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 54d936f07e28cfd2edda5a53de8e70739f6e9675 + 0d690b8b75b98568908726c87525c6cd4aa00362 https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 54d936f07e28cfd2edda5a53de8e70739f6e9675 + 0d690b8b75b98568908726c87525c6cd4aa00362 https://github.com/dotnet/runtime From 8c5ae080d423a9d4238f47c57955ee4233bb719c Mon Sep 17 00:00:00 2001 From: Matt Mitchell Date: Wed, 12 Jul 2023 22:47:22 +0000 Subject: [PATCH 06/10] Merged PR 32558: [internal/release/6.0] Merge from public Merge from public release/6.0 to internal/release/6.0 and resolve conflicts if necessary --- eng/Version.Details.xml | 16 ++++++++-------- eng/Versions.props | 4 ++-- global.json | 4 ++-- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 22d4d311255e..9a53a0a9992f 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -280,22 +280,22 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 - + https://github.com/dotnet/arcade - 91616785a1a6578c83f7e93d98c34a1eb83d6223 + cd79d2e8f7844d0a9aca607d4d5b9b6ab78e2f34 - + https://github.com/dotnet/arcade - 91616785a1a6578c83f7e93d98c34a1eb83d6223 + cd79d2e8f7844d0a9aca607d4d5b9b6ab78e2f34 - + https://github.com/dotnet/arcade - 91616785a1a6578c83f7e93d98c34a1eb83d6223 + cd79d2e8f7844d0a9aca607d4d5b9b6ab78e2f34 - + https://github.com/dotnet/arcade - 91616785a1a6578c83f7e93d98c34a1eb83d6223 + cd79d2e8f7844d0a9aca607d4d5b9b6ab78e2f34 diff --git a/eng/Versions.props b/eng/Versions.props index dcb2136b98c9..1bdf24c11527 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -131,8 +131,8 @@ 6.0.21 6.0.21 - 6.0.0-beta.23313.5 - 6.0.0-beta.23313.5 + 6.0.0-beta.23361.3 + 6.0.0-beta.23361.3 - + @@ -25,7 +25,7 @@ - + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index 9a53a0a9992f..c7674040cf9d 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -11,35 +11,35 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0d690b8b75b98568908726c87525c6cd4aa00362 + fa741b8bad8eebb503bba99e078538f97aff3f5d https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0d690b8b75b98568908726c87525c6cd4aa00362 + fa741b8bad8eebb503bba99e078538f97aff3f5d https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0d690b8b75b98568908726c87525c6cd4aa00362 + fa741b8bad8eebb503bba99e078538f97aff3f5d https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0d690b8b75b98568908726c87525c6cd4aa00362 + fa741b8bad8eebb503bba99e078538f97aff3f5d https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0d690b8b75b98568908726c87525c6cd4aa00362 + fa741b8bad8eebb503bba99e078538f97aff3f5d https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0d690b8b75b98568908726c87525c6cd4aa00362 + fa741b8bad8eebb503bba99e078538f97aff3f5d https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0d690b8b75b98568908726c87525c6cd4aa00362 + fa741b8bad8eebb503bba99e078538f97aff3f5d https://dev.azure.com/dnceng/internal/_git/dotnet-efcore - 0d690b8b75b98568908726c87525c6cd4aa00362 + fa741b8bad8eebb503bba99e078538f97aff3f5d https://github.com/dotnet/runtime From a62c85f2091a3e36e15eb9fb36e0c6806c29c95f Mon Sep 17 00:00:00 2001 From: DotNet Bot Date: Fri, 14 Jul 2023 02:14:58 +0000 Subject: [PATCH 08/10] [internal/release/6.0] Update dependencies from dnceng/internal/dotnet-runtime --- NuGet.config | 4 ++-- eng/Version.Details.xml | 18 +++++++++--------- eng/Versions.props | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/NuGet.config b/NuGet.config index c466d1bd5df3..65bb92f0d5be 100644 --- a/NuGet.config +++ b/NuGet.config @@ -4,7 +4,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/eng/Version.Details.xml b/eng/Version.Details.xml index c7674040cf9d..8cf765e85742 100644 --- a/eng/Version.Details.xml +++ b/eng/Version.Details.xml @@ -177,9 +177,9 @@ https://github.com/dotnet/runtime 4822e3c3aa77eb82b2fb33c9321f923cf11ddde6 - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 + e40b3abf1b41621d4298642a5fd300ebf7cccf6d https://dev.azure.com/dnceng/internal/_git/dotnet-runtime @@ -247,15 +247,15 @@ https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 + e40b3abf1b41621d4298642a5fd300ebf7cccf6d https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 + e40b3abf1b41621d4298642a5fd300ebf7cccf6d https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 + e40b3abf1b41621d4298642a5fd300ebf7cccf6d https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 + e40b3abf1b41621d4298642a5fd300ebf7cccf6d https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 + e40b3abf1b41621d4298642a5fd300ebf7cccf6d - + https://dev.azure.com/dnceng/internal/_git/dotnet-runtime - 0545d9fd7d80e0e8eaaff87aa0011ad5bc13fcc8 + e40b3abf1b41621d4298642a5fd300ebf7cccf6d diff --git a/eng/Versions.props b/eng/Versions.props index 1bdf24c11527..9a77756a55fe 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -68,7 +68,7 @@ 6.0.21 6.0.21 6.0.21 - 6.0.21-servicing.23360.16 + 6.0.21-servicing.23363.11 6.0.0 6.0.1 6.0.0 @@ -103,7 +103,7 @@ 6.0.0 6.0.0 6.0.0 - 6.0.21-servicing.23360.16 + 6.0.21-servicing.23363.11 6.0.1 6.0.0 6.0.2 From cec88a329584349fda070f4c7346b10538210ace Mon Sep 17 00:00:00 2001 From: Andrew Casey Date: Sat, 15 Jul 2023 02:24:48 +0000 Subject: [PATCH 09/10] Merged PR 32094: [6.0] Forcibly close socket on abort Forcibly close socket when connection is aborted. --- .../Common.FunctionalTests/ShutdownTests.cs | 7 + .../Core/src/Internal/Http/Http1Connection.cs | 9 +- .../src/Internal/Http/Http1MessageBody.cs | 2 +- .../src/Internal/Http2/Http2Connection.cs | 3 +- .../Kestrel/Core/src/KestrelServerOptions.cs | 11 + .../src/Internal/ILibuvTrace.cs | 2 + .../src/Internal/LibuvConnection.cs | 60 +++- .../src/Internal/LibuvConnectionListener.cs | 26 +- .../src/Internal/LibuvTrace.cs | 3 + .../src/Internal/ListenerContext.cs | 2 +- .../src/Internal/ListenerPrimary.cs | 28 +- .../src/LibuvTransportOptions.cs | 11 + .../test/ListenerPrimaryTests.cs | 182 +++++++++++- .../src/Internal/ISocketsTrace.cs | 1 + .../src/Internal/SocketConnection.cs | 18 +- .../src/Internal/SocketsTrace.cs | 11 + .../src/SocketConnectionContextFactory.cs | 3 +- .../src/SocketConnectionFactoryOptions.cs | 4 + .../src/SocketTransportOptions.cs | 11 + .../shared/test/StreamBackedTestConnection.cs | 2 +- .../test/TransportTestHelpers/TestServer.cs | 7 +- .../FunctionalTests/Http2/ShutdownTests.cs | 53 ++++ .../test/FunctionalTests/RequestTests.cs | 266 ++++++++++++++++-- .../test/FunctionalTests/ResponseTests.cs | 180 +++++++++--- 24 files changed, 811 insertions(+), 91 deletions(-) diff --git a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs index 07d723e4da87..ea0bb16a0432 100644 --- a/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs +++ b/src/Servers/IIS/IIS/test/Common.FunctionalTests/ShutdownTests.cs @@ -486,8 +486,15 @@ public async Task ClosesConnectionOnServerAbortOutOfProcess() var response = await deploymentResult.HttpClient.GetAsync("/Abort").TimeoutAfter(TimeoutExtensions.DefaultTimeoutValue); Assert.Equal(HttpStatusCode.BadGateway, response.StatusCode); + +#if NEWSHIM_FUNCTIONALS + // In-proc SocketConnection isn't used and there's no abort // 0x80072f78 ERROR_HTTP_INVALID_SERVER_RESPONSE The server returned an invalid or unrecognized response Assert.Contains("0x80072f78", await response.Content.ReadAsStringAsync()); +#else + // 0x80072efe ERROR_INTERNET_CONNECTION_ABORTED The connection with the server was terminated abnormally + Assert.Contains("0x80072efe", await response.Content.ReadAsStringAsync()); +#endif } catch (HttpRequestException) { diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs index ad1ffff50e52..2a5927b901e6 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1Connection.cs @@ -93,7 +93,14 @@ protected override void OnRequestProcessingEnded() _http1Output.Dispose(); } - public void OnInputOrOutputCompleted() + void IRequestProcessor.OnInputOrOutputCompleted() + { + // Closed gracefully. + _http1Output.Abort(ServerOptions.FinOnError ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!); + CancelRequestAbortedToken(); + } + + void IHttpOutputAborter.OnInputOrOutputCompleted() { _http1Output.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient)); CancelRequestAbortedToken(); diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs index 5a3051ae56c8..b9711909eb76 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/Http1MessageBody.cs @@ -207,7 +207,7 @@ protected void ThrowUnexpectedEndOfRequestContent() // so we call OnInputOrOutputCompleted() now to prevent a race in our tests where a 400 // response is written after observing the unexpected end of request content instead of just // closing the connection without a response as expected. - _context.OnInputOrOutputCompleted(); + ((IHttpOutputAborter)_context).OnInputOrOutputCompleted(); KestrelBadHttpRequestException.Throw(RequestRejectionReason.UnexpectedEndOfRequestContent); } diff --git a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs index b197c7fccb28..ac3e9d166d4d 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs @@ -150,7 +150,8 @@ public Http2Connection(HttpConnectionContext context) public void OnInputOrOutputCompleted() { TryClose(); - _frameWriter.Abort(new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient)); + var useException = _context.ServiceContext.ServerOptions.FinOnError || _clientActiveStreamCount != 0; + _frameWriter.Abort(useException ? new ConnectionAbortedException(CoreStrings.ConnectionAbortedByClient) : null!); } public void Abort(ConnectionAbortedException ex) diff --git a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs index cf930c744cbb..f732302c251f 100644 --- a/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs +++ b/src/Servers/Kestrel/Core/src/KestrelServerOptions.cs @@ -29,9 +29,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// public class KestrelServerOptions { + private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError"; + private static readonly bool _finOnError; + + static KestrelServerOptions() + { + AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError); + } + // internal to fast-path header decoding when RequestHeaderEncodingSelector is unchanged. internal static readonly Func DefaultHeaderEncodingSelector = _ => null; + // Opt-out flag for back compat. Remove in 9.0 (or make public). + internal bool FinOnError { get; set; } = _finOnError; + private Func _requestHeaderEncodingSelector = DefaultHeaderEncodingSelector; private Func _responseHeaderEncodingSelector = DefaultHeaderEncodingSelector; diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs index 60966046a3a3..260657cff99b 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ILibuvTrace.cs @@ -14,6 +14,8 @@ internal interface ILibuvTrace : ILogger void ConnectionWriteFin(string connectionId, string reason); + void ConnectionWriteRst(string connectionId, string reason); + void ConnectionWrite(string connectionId, int count); void ConnectionWriteCallback(string connectionId, int status); diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs index fdfc852ba243..ca163d6d2aff 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnection.cs @@ -6,6 +6,8 @@ using System.IO; using System.IO.Pipelines; using System.Net; +using System.Net.Sockets; +using System.Reflection.Metadata; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Connections; @@ -28,6 +30,8 @@ internal partial class LibuvConnection : TransportConnection private readonly IDuplexPipe _originalTransport; private readonly CancellationTokenSource _connectionClosedTokenSource = new CancellationTokenSource(); + private readonly bool _finOnError; + private volatile ConnectionAbortedException _abortReason; private MemoryHandle _bufferHandle; @@ -43,9 +47,11 @@ public LibuvConnection(UvStreamHandle socket, PipeOptions inputOptions = null, PipeOptions outputOptions = null, long? maxReadBufferSize = null, - long? maxWriteBufferSize = null) + long? maxWriteBufferSize = null, + bool finOnError = false) { _socket = socket; + _finOnError = finOnError; LocalEndPoint = localEndPoint; RemoteEndPoint = remoteEndPoint; @@ -124,6 +130,13 @@ private async Task StartCore() { inputError ??= _abortReason ?? new ConnectionAbortedException("The libuv transport's send loop completed gracefully."); + if (!_finOnError && _abortReason is not null) + { + // When shutdown isn't clean (note that we're using _abortReason, rather than inputError, to exclude that case), + // we set the DontLinger socket option to cause libuv to send a RST and release any buffered response data. + SetDontLingerOption(_socket); + } + // Now, complete the input so that no more reads can happen Input.Complete(inputError); Output.Complete(outputError); @@ -132,8 +145,16 @@ private async Task StartCore() // on the stream handle Input.CancelPendingFlush(); - // Send a FIN - Log.ConnectionWriteFin(ConnectionId, inputError.Message); + if (!_finOnError && _abortReason is not null) + { + // Send a RST + Log.ConnectionWriteRst(ConnectionId, inputError.Message); + } + else + { + // Send a FIN + Log.ConnectionWriteFin(ConnectionId, inputError.Message); + } // We're done with the socket now _socket.Dispose(); @@ -150,6 +171,27 @@ private async Task StartCore() } } + /// + /// This should be called on before it is disposed. + /// Both and call dispose but, rather than predict + /// which will do so first (which varies), we make this method idempotent and call it in both. + /// + private static void SetDontLingerOption(UvStreamHandle socket) + { + if (!socket.IsClosed && !socket.IsInvalid) + { + var libuv = socket.Libuv; + var pSocket = IntPtr.Zero; + libuv.uv_fileno(socket, ref pSocket); + + // libuv doesn't expose setsockopt, so we take advantage of the fact that + // Socket already has a PAL + using var managedHandle = new SafeSocketHandle(pSocket, ownsHandle: false); + using var managedSocket = new Socket(managedHandle); + managedSocket.LingerState = new LingerOption(enable: true, seconds: 0); + } + } + public override void Abort(ConnectionAbortedException abortReason) { _abortReason = abortReason; @@ -157,8 +199,16 @@ public override void Abort(ConnectionAbortedException abortReason) // Cancel WriteOutputAsync loop after setting _abortReason. Output.CancelPendingRead(); - // This cancels any pending I/O. - Thread.Post(s => s.Dispose(), _socket); + Thread.Post(static (self) => + { + if (!self._finOnError) + { + SetDontLingerOption(self._socket); + } + + // This cancels any pending I/O. + self._socket.Dispose(); + }, this); } public override async ValueTask DisposeAsync() diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs index bdfcc2e65b58..f03f8e603097 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvConnectionListener.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; using System.Threading; @@ -134,9 +135,24 @@ internal async Task BindAsync() { // TODO: Move thread management to LibuvTransportFactory // TODO: Split endpoint management from thread management + + // When `FinOnError` is false (the default), we need to be able to forcibly abort connections. + // On Windows, libuv 1.10.0 will call `shutdown`, preventing forcible abort, on any socket + // not flagged as `UV_HANDLE_SHARED_TCP_SOCKET`. The only way we've found to cause socket + // to be flagged as `UV_HANDLE_SHARED_TCP_SOCKET` is to share it across a named pipe (which + // must, itself, be flagged `ipc`), which naturally happens when a `ListenerPrimary` dispatches + // a connection to a `ListenerSecondary`. Therefore, in scenarios where this is required, we + // tell the `ListenerPrimary` to dispatch *all* connections to secondary and create an + // additional `ListenerSecondary` to replace the lost capacity. + var dispatchAllToSecondary = Libuv.IsWindows && !TransportContext.Options.FinOnError; + #pragma warning disable CS0618 - for (var index = 0; index < TransportOptions.ThreadCount; index++) + var threadCount = dispatchAllToSecondary + ? TransportOptions.ThreadCount + 1 + : TransportOptions.ThreadCount; #pragma warning restore CS0618 + + for (var index = 0; index < threadCount; index++) { Threads.Add(new LibuvThread(Libuv, TransportContext)); } @@ -148,10 +164,10 @@ internal async Task BindAsync() try { -#pragma warning disable CS0618 - if (TransportOptions.ThreadCount == 1) -#pragma warning restore CS0618 + if (threadCount == 1) { + Debug.Assert(!dispatchAllToSecondary, "Should have taken the primary/secondary code path"); + var listener = new Listener(TransportContext); _listeners.Add(listener); await listener.StartAsync(EndPoint, Threads[0]).ConfigureAwait(false); @@ -162,7 +178,7 @@ internal async Task BindAsync() var pipeName = (Libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); var pipeMessage = Guid.NewGuid().ToByteArray(); - var listenerPrimary = new ListenerPrimary(TransportContext); + var listenerPrimary = new ListenerPrimary(TransportContext, dispatchAllToSecondary); _listeners.Add(listenerPrimary); await listenerPrimary.StartAsync(pipeName, pipeMessage, EndPoint, Threads[0]).ConfigureAwait(false); EndPoint = listenerPrimary.EndPoint; diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs index f4e4d64aafff..c360efc7bf93 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/LibuvTrace.cs @@ -27,6 +27,9 @@ public void ConnectionRead(string connectionId, int count) [LoggerMessage(7, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending FIN because: ""{Reason}""", EventName = nameof(ConnectionWriteFin))] public partial void ConnectionWriteFin(string connectionId, string reason); + [LoggerMessage(8, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending RST because: ""{Reason}""", EventName = nameof(ConnectionWriteRst))] + public partial void ConnectionWriteRst(string connectionId, string reason); + public void ConnectionWrite(string connectionId, int count) { // Don't log for now since this could be *too* verbose. diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs index 7ad346f6ea59..8ab2f5f34503 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerContext.cs @@ -110,7 +110,7 @@ protected internal void HandleConnection(UvStreamHandle socket) var options = TransportContext.Options; #pragma warning disable CS0618 - var connection = new LibuvConnection(socket, TransportContext.Log, Thread, remoteEndPoint, localEndPoint, InputOptions, OutputOptions, options.MaxReadBufferSize, options.MaxWriteBufferSize); + var connection = new LibuvConnection(socket, TransportContext.Log, Thread, remoteEndPoint, localEndPoint, InputOptions, OutputOptions, options.MaxReadBufferSize, options.MaxWriteBufferSize, options.FinOnError); #pragma warning restore CS0618 connection.Start(); diff --git a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs index 8a899c91c44e..18ae24af5adb 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/Internal/ListenerPrimary.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Net; using System.Runtime.InteropServices; @@ -22,6 +23,10 @@ internal class ListenerPrimary : Listener private readonly List _dispatchPipes = new List(); // The list of pipes we've created but may not be part of _dispatchPipes private readonly List _createdPipes = new List(); + + // If true, dispatch all connections to _dispatchPipes - don't process any in the primary + private readonly bool _dispatchAll; + private int _dispatchIndex; private string _pipeName; private byte[] _pipeMessage; @@ -32,8 +37,9 @@ internal class ListenerPrimary : Listener // but it has no other functional significance private readonly ArraySegment> _dummyMessage = new ArraySegment>(new[] { new ArraySegment(new byte[] { 1, 2, 3, 4 }) }); - public ListenerPrimary(LibuvTransportContext transportContext) : base(transportContext) + public ListenerPrimary(LibuvTransportContext transportContext, bool dispatchAll) : base(transportContext) { + _dispatchAll = dispatchAll; } /// @@ -107,9 +113,27 @@ private void OnListenPipe(UvStreamHandle pipe, int status, UvException error) protected override void DispatchConnection(UvStreamHandle socket) { - var index = _dispatchIndex++ % (_dispatchPipes.Count + 1); + var modulus = _dispatchAll ? _dispatchPipes.Count : (_dispatchPipes.Count + 1); + if (modulus == 0) + { + if (_createdPipes.Count == 0) + { +#pragma warning disable CS0618 // Type or member is obsolete + Log.LogError(0, $"Connection received before listeners were initialized - see https://aka.ms/dotnet/aspnet/finonerror for possible mitigations"); +#pragma warning restore CS0618 // Type or member is obsolete + } + else + { + Log.LogError(0, "Unable to process connection since listeners failed to initialize - see https://aka.ms/dotnet/aspnet/finonerror for possible mitigations"); + } + + return; + } + + var index = _dispatchIndex++ % modulus; if (index == _dispatchPipes.Count) { + Debug.Assert(!_dispatchAll, "Should have dispatched to a secondary listener"); base.DispatchConnection(socket); } else diff --git a/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs b/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs index 142a2f99c4ce..9e58ac1a2d7a 100644 --- a/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Libuv/src/LibuvTransportOptions.cs @@ -12,6 +12,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv [Obsolete("The libuv transport is obsolete and will be removed in a future release. See https://aka.ms/libuvtransport for details.", error: false)] // Remove after .NET 6. public class LibuvTransportOptions { + private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError"; + private static readonly bool _finOnError; + + static LibuvTransportOptions() + { + AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError); + } + + // Opt-out flag for back compat. Remove in 7.0. + internal bool FinOnError { get; set; } = _finOnError; + /// /// The number of libuv I/O threads used to process requests. /// diff --git a/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs b/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs index 82d04cd1ac14..58d57451490e 100644 --- a/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs +++ b/src/Servers/Kestrel/Transport.Libuv/test/ListenerPrimaryTests.cs @@ -5,11 +5,14 @@ using System.IO; using System.Linq; using System.Net; +using System.Net.Sockets; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networking; using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Tests.TestHelpers; using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Xunit; @@ -33,7 +36,7 @@ public async Task ConnectionsGetRoundRobinedToSecondaryListeners() // Start primary listener var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); await libuvThreadPrimary.StartAsync(); - var listenerPrimary = new ListenerPrimary(transportContextPrimary); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: false); await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); var address = GetUri(listenerPrimary.EndPoint); @@ -50,6 +53,8 @@ public async Task ConnectionsGetRoundRobinedToSecondaryListeners() } var listenerCount = listenerPrimary.UvPipeCount; + Assert.Equal(0, listenerCount); + // Add secondary listener var libuvThreadSecondary = new LibuvThread(libuv, transportContextSecondary); await libuvThreadSecondary.StartAsync(); @@ -85,6 +90,74 @@ public async Task ConnectionsGetRoundRobinedToSecondaryListeners() await listenerPrimary.DisposeAsync(); await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5)); } + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public async Task ConnectionsGetRoundRobinedToSecondaryListeners_DispatchAll(int secondaryCount) + { + var libuv = new LibuvFunctions(); + + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); + + var transportContextPrimary = new TestLibuvTransportContext(); + var transportContextSecondary = new TestLibuvTransportContext(); + + var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); + var pipeMessage = Guid.NewGuid().ToByteArray(); + + // Start primary listener + var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); + await libuvThreadPrimary.StartAsync(); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: true); + await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); + var address = GetUri(listenerPrimary.EndPoint); + + Assert.Equal(0, listenerPrimary.UvPipeCount); + + // Add secondary listeners + var listenerSecondaries = new ListenerSecondary[secondaryCount]; + for (int i = 0; i < secondaryCount; i++) + { + var libuvThread = new LibuvThread(libuv, transportContextSecondary); + await libuvThread.StartAsync(); + var listener = new ListenerSecondary(transportContextSecondary); + await listener.StartAsync(pipeName, pipeMessage, endpoint, libuvThread); + listenerSecondaries[i] = listener; + } + + var maxWait = Task.Delay(TestConstants.DefaultTimeout); + // wait for ListenerPrimary.ReadCallback to add the secondary pipe + while (listenerPrimary.UvPipeCount < secondaryCount) + { + var completed = await Task.WhenAny(maxWait, Task.Delay(100)); + if (ReferenceEquals(completed, maxWait)) + { + throw new TimeoutException("Timed out waiting for secondary listener to become available"); + } + } + + // Check that the secondaries are visited in order and that it wraps + // around without hitting the primary + for (int i = 0; i < secondaryCount + 1; i++) + { + var expectedTask = listenerSecondaries[i % secondaryCount].AcceptAsync().AsTask(); + + using var socket = await HttpClientSlim.GetSocket(address); + + await using var connection = await expectedTask.DefaultTimeout(); + } + + foreach (var listenerSecondary in listenerSecondaries) + { + var libuvThread = listenerSecondary.Thread; + await listenerSecondary.DisposeAsync(); + await libuvThread.StopAsync(TimeSpan.FromSeconds(5)); + } + + await listenerPrimary.DisposeAsync(); + await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5)); + } // https://github.com/aspnet/KestrelHttpServer/issues/1182 [Fact] @@ -103,7 +176,7 @@ public async Task NonListenerPipeConnectionsAreLoggedAndIgnored() // Start primary listener var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); await libuvThreadPrimary.StartAsync(); - var listenerPrimary = new ListenerPrimary(transportContextPrimary); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: false); await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); var address = GetUri(listenerPrimary.EndPoint); @@ -181,7 +254,6 @@ public async Task NonListenerPipeConnectionsAreLoggedAndIgnored() Assert.Equal(LogLevel.Debug, logMessage.LogLevel); } - [Fact] public async Task PipeConnectionsWithWrongMessageAreLoggedAndIgnored() { @@ -199,7 +271,7 @@ public async Task PipeConnectionsWithWrongMessageAreLoggedAndIgnored() // Start primary listener var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); await libuvThreadPrimary.StartAsync(); - var listenerPrimary = new ListenerPrimary(transportContextPrimary); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: false); await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); var address = GetUri(listenerPrimary.EndPoint); @@ -235,6 +307,108 @@ public async Task PipeConnectionsWithWrongMessageAreLoggedAndIgnored() Assert.Contains("Bad data", errorMessage.Exception.ToString()); } + [Fact] + public async Task PipeConnectionsWithWrongMessageAreLoggedAndIgnored_DispatchAllNoneRemaining() + { + var libuv = new LibuvFunctions(); + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); + + var logger = new TestApplicationErrorLogger(); + + var transportContextPrimary = new TestLibuvTransportContext { Log = new LibuvTrace(logger) }; + var transportContextSecondary = new TestLibuvTransportContext(); + + var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); + var pipeMessage = Guid.NewGuid().ToByteArray(); + + // Start primary listener + var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); + await libuvThreadPrimary.StartAsync(); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: true); + await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); + var address = GetUri(listenerPrimary.EndPoint); + + // Add secondary listener with wrong pipe message + var libuvThreadSecondary = new LibuvThread(libuv, transportContextSecondary); + await libuvThreadSecondary.StartAsync(); + var listenerSecondary = new ListenerSecondary(transportContextSecondary); + await listenerSecondary.StartAsync(pipeName, Guid.NewGuid().ToByteArray(), endpoint, libuvThreadSecondary); + + // Wait up to 10 seconds for error to be logged + for (var i = 0; i < 10 && logger.TotalErrorsLogged == 0; i++) + { + await Task.Delay(100); + } + + Assert.Equal(1, logger.TotalErrorsLogged); + + var badDataMessage = Assert.Single(logger.Messages.Where(m => m.LogLevel == LogLevel.Error)); + Assert.IsType(badDataMessage.Exception); + Assert.Contains("Bad data", badDataMessage.Exception.ToString()); + + using var socket = await HttpClientSlim.GetSocket(address); + + var _ = listenerPrimary.AcceptAsync(); + + // Wait up to 10 seconds for error to be logged + for (var i = 0; i < 10 && logger.TotalErrorsLogged <= 1; i++) + { + await Task.Delay(100); + } + + var noSecondariesMessage = logger.Messages.Last(m => m.LogLevel == LogLevel.Error); + Assert.Null(noSecondariesMessage.Exception); + Assert.Contains("listeners failed to initialize", noSecondariesMessage.Message); + + Assert.Null(libuvThreadPrimary.FatalError); + + await listenerSecondary.DisposeAsync(); + await libuvThreadSecondary.StopAsync(TimeSpan.FromSeconds(5)); + + await listenerPrimary.DisposeAsync(); + await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5)); + } + + [Fact] + public async Task DispatchAllConnectionBeforeSecondaries() + { + var libuv = new LibuvFunctions(); + var endpoint = new IPEndPoint(IPAddress.Loopback, 0); + + var logger = new TestApplicationErrorLogger(); + + var transportContextPrimary = new TestLibuvTransportContext { Log = new LibuvTrace(logger) }; + var transportContextSecondary = new TestLibuvTransportContext(); + + var pipeName = (libuv.IsWindows ? @"\\.\pipe\kestrel_" : "/tmp/kestrel_") + Guid.NewGuid().ToString("n"); + var pipeMessage = Guid.NewGuid().ToByteArray(); + + // Start primary listener + var libuvThreadPrimary = new LibuvThread(libuv, transportContextPrimary); + await libuvThreadPrimary.StartAsync(); + var listenerPrimary = new ListenerPrimary(transportContextPrimary, dispatchAll: true); + await listenerPrimary.StartAsync(pipeName, pipeMessage, endpoint, libuvThreadPrimary); + var address = GetUri(listenerPrimary.EndPoint); + + using var socket = await HttpClientSlim.GetSocket(address); + + var _ = listenerPrimary.AcceptAsync(); + + // Wait up to 10 seconds for error to be logged + for (var i = 0; i < 10 && logger.TotalErrorsLogged <= 1; i++) + { + await Task.Delay(100); + } + + var noSecondariesMessage = logger.Messages.Last(m => m.LogLevel == LogLevel.Error); + Assert.Null(noSecondariesMessage.Exception); + Assert.Contains("before listeners", noSecondariesMessage.Message); + + Assert.Null(libuvThreadPrimary.FatalError); + + await listenerPrimary.DisposeAsync(); + await libuvThreadPrimary.StopAsync(TimeSpan.FromSeconds(5)); + } private static async Task AssertRoundRobin(Uri address, ListenerPrimary listenerPrimary, ListenerSecondary listenerSecondary, ListenerContext currentListener, Task expected = null, int connections = 4) { diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs index f5e5c4963786..cdd82f481e8f 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/ISocketsTrace.cs @@ -11,6 +11,7 @@ internal interface ISocketsTrace : ILogger void ConnectionReadFin(SocketConnection connection); void ConnectionWriteFin(SocketConnection connection, string reason); + void ConnectionWriteRst(SocketConnection connection, string reason); void ConnectionError(SocketConnection connection, Exception ex); diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs index f43c2e1454f1..c7f58a128a82 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketConnection.cs @@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.Internal { + internal sealed partial class SocketConnection : TransportConnection { private static readonly int MinAllocBufferSize = PinnedBlockMemoryPool.BlockSize / 2; @@ -30,6 +31,7 @@ internal sealed partial class SocketConnection : TransportConnection private readonly TaskCompletionSource _waitForConnectionClosedTcs = new TaskCompletionSource(); private bool _connectionClosed; private readonly bool _waitForData; + private readonly bool _finOnError; internal SocketConnection(Socket socket, MemoryPool memoryPool, @@ -38,7 +40,8 @@ internal SocketConnection(Socket socket, SocketSenderPool socketSenderPool, PipeOptions inputOptions, PipeOptions outputOptions, - bool waitForData = true) + bool waitForData = true, + bool finOnError = false) { Debug.Assert(socket != null); Debug.Assert(memoryPool != null); @@ -49,6 +52,7 @@ internal SocketConnection(Socket socket, _trace = trace; _waitForData = waitForData; _socketSenderPool = socketSenderPool; + _finOnError = finOnError; LocalEndPoint = _socket.LocalEndPoint; RemoteEndPoint = _socket.RemoteEndPoint; @@ -329,11 +333,21 @@ private void Shutdown(Exception? shutdownReason) // ever observe the nondescript ConnectionAbortedException except for connection middleware attempting // to half close the connection which is currently unsupported. _shutdownReason = shutdownReason ?? new ConnectionAbortedException("The Socket transport's send loop completed gracefully."); + + // NB: not _shutdownReason since we don't want to do this on graceful completion + if (!_finOnError && shutdownReason is not null) + { + _trace.ConnectionWriteRst(this, shutdownReason.Message); + + // This forces an abortive close with linger time 0 (and implies Dispose) + _socket.Close(timeout: 0); + return; + } + _trace.ConnectionWriteFin(this, _shutdownReason.Message); try { - // Try to gracefully close the socket even for aborts to match libuv behavior. _socket.Shutdown(SocketShutdown.Both); } catch diff --git a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs index 1a48fa189a15..f4108eae678a 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/Internal/SocketsTrace.cs @@ -43,6 +43,17 @@ public void ConnectionWriteFin(SocketConnection connection, string reason) } } + [LoggerMessage(8, LogLevel.Debug, @"Connection id ""{ConnectionId}"" sending RST because: ""{Reason}""", EventName = "ConnectionWriteRst", SkipEnabledCheck = true)] + private static partial void ConnectionWriteRst(ILogger logger, string connectionId, string reason); + + public void ConnectionWriteRst(SocketConnection connection, string reason) + { + if (_logger.IsEnabled(LogLevel.Debug)) + { + ConnectionWriteRst(_logger, connection.ConnectionId, reason); + } + } + public void ConnectionWrite(SocketConnection connection, int count) { // Don't log for now since this could be *too* verbose. diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs index 71f3cdc0fc0c..2635af9792d7 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionContextFactory.cs @@ -104,7 +104,8 @@ public ConnectionContext Create(Socket socket) setting.SocketSenderPool, setting.InputOptions, setting.OutputOptions, - waitForData: _options.WaitForDataBeforeAllocatingBuffer); + waitForData: _options.WaitForDataBeforeAllocatingBuffer, + finOnError: _options.FinOnError); connection.Start(); return connection; diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs index d0c88c0535f3..fec0b3bef938 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketConnectionFactoryOptions.cs @@ -23,8 +23,12 @@ internal SocketConnectionFactoryOptions(SocketTransportOptions transportOptions) MaxWriteBufferSize = transportOptions.MaxWriteBufferSize; UnsafePreferInlineScheduling = transportOptions.UnsafePreferInlineScheduling; MemoryPoolFactory = transportOptions.MemoryPoolFactory; + FinOnError = transportOptions.FinOnError; } + // Opt-out flag for back compat. Remove in 9.0 (or make public). + internal bool FinOnError { get; set; } + /// /// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool. /// diff --git a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs index 9130cb980454..4f22c079dc1f 100644 --- a/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs +++ b/src/Servers/Kestrel/Transport.Sockets/src/SocketTransportOptions.cs @@ -13,6 +13,17 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets /// public class SocketTransportOptions { + private const string FinOnErrorSwitch = "Microsoft.AspNetCore.Server.Kestrel.FinOnError"; + private static readonly bool _finOnError; + + static SocketTransportOptions() + { + AppContext.TryGetSwitch(FinOnErrorSwitch, out _finOnError); + } + + // Opt-out flag for back compat. Remove in 9.0 (or make public). + internal bool FinOnError { get; set; } = _finOnError; + /// /// The number of I/O queues used to process requests. Set to 0 to directly schedule I/O to the ThreadPool. /// diff --git a/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs b/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs index 5dd91c4337d9..eb4f310e7a29 100644 --- a/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs +++ b/src/Servers/Kestrel/shared/test/StreamBackedTestConnection.cs @@ -138,7 +138,7 @@ public async Task ReceiveEnd(params string[] lines) public async Task WaitForConnectionClose() { var buffer = new byte[128]; - var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).TimeoutAfter(Timeout); + var bytesTransferred = await _stream.ReadAsync(buffer, 0, 128).ContinueWith(t => t.IsFaulted ? 0 : t.Result).TimeoutAfter(Timeout); if (bytesTransferred > 0) { diff --git a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs index 79fc65849640..0c8109cb9952 100644 --- a/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs +++ b/src/Servers/Kestrel/shared/test/TransportTestHelpers/TestServer.cs @@ -48,7 +48,7 @@ public TestServer(RequestDelegate app, TestServiceContext context, ListenOptions { } - public TestServer(RequestDelegate app, TestServiceContext context, Action configureListenOptions) + public TestServer(RequestDelegate app, TestServiceContext context, Action configureListenOptions, Action configureServices = null) : this(app, context, options => { var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)) @@ -57,7 +57,10 @@ public TestServer(RequestDelegate app, TestServiceContext context, Action { }) + }, s => + { + configureServices?.Invoke(s); + }) { } diff --git a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs index 1a12f4629e44..ff5f06ea21a5 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/Http2/ShutdownTests.cs @@ -46,6 +46,59 @@ public ShutdownTests() }; } + [ConditionalFact] + public async Task ConnectionClosedWithoutActiveRequestsOrGoAwayFIN() + { + var connectionClosed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var readFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var writeFin = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + TestSink.MessageLogged += context => + { + if (context.EventId.Name == "Http2ConnectionClosed") + { + connectionClosed.SetResult(); + } + else if (context.EventId.Name == "ConnectionReadFin") + { + readFin.SetResult(); + } + else if (context.EventId.Name == "ConnectionWriteFin") + { + writeFin.SetResult(); + } + }; + + var testContext = new TestServiceContext(LoggerFactory); + + testContext.InitializeHeartbeat(); + + await using (var server = new TestServer(context => + { + return context.Response.WriteAsync("hello world " + context.Request.Protocol); + }, + testContext, + kestrelOptions => + { + kestrelOptions.Listen(IPAddress.Loopback, 0, listenOptions => + { + listenOptions.Protocols = HttpProtocols.Http2; + listenOptions.UseHttps(_x509Certificate2); + }); + })) + { + var response = await Client.GetStringAsync($"https://localhost:{server.Port}/"); + Assert.Equal("hello world HTTP/2", response); + Client.Dispose(); // Close the socket, no GoAway is sent. + + await readFin.Task.DefaultTimeout(); + await writeFin.Task.DefaultTimeout(); + await connectionClosed.Task.DefaultTimeout(); + + await server.StopAsync(); + } + } + [CollectDump] [ConditionalFact] public async Task GracefulShutdownWaitsForRequestsToFinish() diff --git a/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs b/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs index e10a5dc3ad3d..af391389ade3 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/RequestTests.cs @@ -21,7 +21,13 @@ using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.AspNetCore.Server.Kestrel.FunctionalTests; +#if LIBUV +using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv; +#elif SOCKETS +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; +#endif using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Moq; @@ -40,6 +46,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests public class RequestTests : LoggedTest { private const int _connectionStartedEventId = 1; + private const int _connectionReadFinEventId = 6; private const int _connectionResetEventId = 19; private static readonly int _semaphoreWaitTimeout = Debugger.IsAttached ? 10000 : 2500; @@ -234,6 +241,59 @@ await connection2.Receive($"HTTP/1.1 200 OK", } } + [Fact] + public async Task ConnectionClosedPriorToRequestIsLoggedAsDebug() + { + var connectionStarted = new SemaphoreSlim(0); + var connectionReadFin = new SemaphoreSlim(0); + var loggedHigherThanDebug = false; + + TestSink.MessageLogged += context => + { + if (context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Connections" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets") + { + return; + } + + if (context.EventId.Id == _connectionStartedEventId) + { + connectionStarted.Release(); + } + else if (context.EventId.Id == _connectionReadFinEventId) + { + connectionReadFin.Release(); + } + + if (context.LogLevel > LogLevel.Debug) + { + loggedHigherThanDebug = true; + } + }; + + await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + // Wait until connection is established + Assert.True(await connectionStarted.WaitAsync(TestConstants.DefaultTimeout)); + + connection.ShutdownSend(); + + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout)); + await connection.ReceiveEnd(); + } + } + + Assert.False(loggedHigherThanDebug); + } + [Fact] public async Task ConnectionResetPriorToRequestIsLoggedAsDebug() { @@ -286,6 +346,66 @@ public async Task ConnectionResetPriorToRequestIsLoggedAsDebug() Assert.False(loggedHigherThanDebug); } + [Fact] + public async Task ConnectionClosedBetweenRequestsIsLoggedAsDebug() + { + var connectionReadFin = new SemaphoreSlim(0); + var loggedHigherThanDebug = false; + + TestSink.MessageLogged += context => + { + if (context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" && + context.LoggerName != "Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets") + { + return; + } + + if (context.LogLevel > LogLevel.Debug) + { + loggedHigherThanDebug = true; + } + + if (context.EventId.Id == _connectionReadFinEventId) + { + connectionReadFin.Release(); + } + }; + + await using (var server = new TestServer(context => Task.CompletedTask, new TestServiceContext(LoggerFactory))) + { + using (var connection = server.CreateConnection()) + { + await connection.Send( + "GET / HTTP/1.1", + "Host:", + "", + ""); + + // Make sure the response is fully received, so a write failure (e.g. EPIPE) doesn't cause + // a more critical log message. + await connection.Receive( + "HTTP/1.1 200 OK", + "Content-Length: 0", + $"Date: {server.Context.DateHeaderValue}", + "", + ""); + + connection.ShutdownSend(); + + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout)); + + await connection.ReceiveEnd(); + } + } + + Assert.False(loggedHigherThanDebug); + } + [Fact] public async Task ConnectionResetBetweenRequestsIsLoggedAsDebug() { @@ -345,10 +465,13 @@ await connection.Receive( Assert.False(loggedHigherThanDebug); } - [Fact] - public async Task ConnectionResetMidRequestIsLoggedAsDebug() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ConnectionClosedOrResetMidRequestIsLoggedAsDebug(bool close) { var requestStarted = new SemaphoreSlim(0); + var connectionReadFin = new SemaphoreSlim(0); var connectionReset = new SemaphoreSlim(0); var connectionClosing = new SemaphoreSlim(0); var loggedHigherThanDebug = false; @@ -367,6 +490,11 @@ public async Task ConnectionResetMidRequestIsLoggedAsDebug() loggedHigherThanDebug = true; } + if (context.EventId.Id == _connectionReadFinEventId) + { + connectionReadFin.Release(); + } + if (context.EventId.Id == _connectionResetEventId) { connectionReset.Release(); @@ -387,15 +515,23 @@ public async Task ConnectionResetMidRequestIsLoggedAsDebug() // Wait until connection is established Assert.True(await requestStarted.WaitAsync(TestConstants.DefaultTimeout), "request should have started"); - connection.Reset(); - } + if (close) + { + connection.ShutdownSend(); + Assert.True(await connectionReadFin.WaitAsync(TestConstants.DefaultTimeout), "Connection close event should have been logged"); + } + else + { + connection.Reset(); - // If the reset is correctly logged as Debug, the wait below should complete shortly. - // This check MUST come before disposing the server, otherwise there's a race where the RST - // is still in flight when the connection is aborted, leading to the reset never being received - // and therefore not logged. - Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout), "Connection reset event should have been logged"); - connectionClosing.Release(); + // If the reset is correctly logged as Debug, the wait below should complete shortly. + // This check MUST come before disposing the server, otherwise there's a race where the RST + // is still in flight when the connection is aborted, leading to the reset never being received + // and therefore not logged. + Assert.True(await connectionReset.WaitAsync(TestConstants.DefaultTimeout), "Connection reset event should have been logged"); + } + connectionClosing.Release(); + } } Assert.False(loggedHigherThanDebug, "Logged event should not have been higher than debug."); @@ -494,18 +630,43 @@ public async Task RequestAbortedTokenFiredOnClientFIN() } } - [Fact] - public async Task AbortingTheConnectionSendsFIN() + [Theory] +#if LIBUV + [InlineData(true, 1)] + [InlineData(false, 1)] + [InlineData(true, 2)] + [InlineData(false, 2)] + public async Task AbortingTheConnection(bool fin, int threadCount) +#else + [InlineData(true)] + [InlineData(false)] + public async Task AbortingTheConnection(bool fin) +#endif { + var connectionAborted = new SemaphoreSlim(0); + var builder = TransportSelector.GetHostBuilder() +#if LIBUV + .ConfigureServices(services => + { +#pragma warning disable CS0618 // Type or member is obsolete + services.Configure(options => + { + options.ThreadCount = threadCount; + }); +#pragma warning restore CS0618 // Type or member is obsolete + }) +#endif .ConfigureWebHost(webHostBuilder => { webHostBuilder + .ConfigureServices(s => SetFinOnError(s, fin)) .UseKestrel() .UseUrls("http://127.0.0.1:0") .Configure(app => app.Run(context => { context.Abort(); + connectionAborted.Release(); return Task.CompletedTask; })); }) @@ -519,8 +680,18 @@ public async Task AbortingTheConnectionSendsFIN() { socket.Connect(new IPEndPoint(IPAddress.Loopback, host.GetPort())); socket.Send(Encoding.ASCII.GetBytes("GET / HTTP/1.1\r\nHost:\r\n\r\n")); - int result = socket.Receive(new byte[32]); - Assert.Equal(0, result); + + Assert.True(await connectionAborted.WaitAsync(_semaphoreWaitTimeout)); + + if (fin) + { + int result = socket.Receive(new byte[32]); + Assert.Equal(0, result); + } + else + { + Assert.Throws(() => socket.Receive(new byte[32])); + } } await host.StopAsync(); @@ -731,16 +902,21 @@ await connection.Send("POST / HTTP/1.1", } [Theory] - [MemberData(nameof(ConnectionMiddlewareDataName))] - public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOptionsName) + [InlineData("Loopback", true)] + [InlineData("PassThrough", true)] + [InlineData("Loopback", false)] + [InlineData("PassThrough", false)] + public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOptionsName, bool fin) { const int connectionPausedEventId = 4; const int connectionFinSentEventId = 7; + const int connectionRstSentEventId = 8; const int maxRequestBufferSize = 4096; var readCallbackUnwired = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var clientClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var serverClosedConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverFinConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var serverRstConnection = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); TestSink.MessageLogged += context => @@ -757,7 +933,11 @@ public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOpti } else if (context.EventId == connectionFinSentEventId) { - serverClosedConnection.SetResult(); + serverFinConnection.SetResult(); + } + else if (context.EventId == connectionRstSentEventId) + { + serverRstConnection.SetResult(); } }; @@ -766,6 +946,7 @@ public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOpti { ServerOptions = { + FinOnError = fin, Limits = { MaxRequestBufferSize = maxRequestBufferSize, @@ -783,10 +964,24 @@ public async Task ServerCanAbortConnectionAfterUnobservedClose(string listenOpti context.Abort(); - await serverClosedConnection.Task; + if (fin) + { + await serverFinConnection.Task.DefaultTimeout(); + } + else + { + await serverRstConnection.Task.DefaultTimeout(); + } appFuncCompleted.SetResult(); - }, testContext, ConnectionMiddlewareData[listenOptionsName]())) + }, testContext, listen => + { + if (listenOptionsName == "PassThrough") + { + listen.UsePassThrough(); + } + }, + services => SetFinOnError(services, fin))) { using (var connection = server.CreateConnection()) { @@ -956,21 +1151,21 @@ await context.Response.WriteAsync(JsonConvert.SerializeObject(new private static async Task AssertStreamContains(Stream stream, string expectedSubstring) { var expectedBytes = Encoding.ASCII.GetBytes(expectedSubstring); - var exptectedLength = expectedBytes.Length; - var responseBuffer = new byte[exptectedLength]; + var expectedLength = expectedBytes.Length; + var responseBuffer = new byte[expectedLength]; var matchedChars = 0; - while (matchedChars < exptectedLength) + while (matchedChars < expectedLength) { - var count = await stream.ReadAsync(responseBuffer, 0, exptectedLength - matchedChars).DefaultTimeout(); + var count = await stream.ReadAsync(responseBuffer, 0, expectedLength - matchedChars).DefaultTimeout(); if (count == 0) { Assert.True(false, "Stream completed without expected substring."); } - for (var i = 0; i < count && matchedChars < exptectedLength; i++) + for (var i = 0; i < count && matchedChars < expectedLength; i++) { if (responseBuffer[i] == expectedBytes[matchedChars]) { @@ -983,5 +1178,26 @@ private static async Task AssertStreamContains(Stream stream, string expectedSub } } } + + private static void SetFinOnError(IServiceCollection services, bool finOnError) + { +#if LIBUV +#pragma warning disable CS0618 // Type or member is obsolete + services.Configure(options => + { + options.FinOnError = finOnError; + }); +#pragma warning restore CS0618 // Type or member is obsolete +#elif SOCKETS + services.Configure(o => + { + o.FinOnError = finOnError; + }); +#endif + services.Configure(o => + { + o.FinOnError = finOnError; + }); + } } } diff --git a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs index 991044dbf8f0..cbda94bd05a6 100644 --- a/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs +++ b/src/Servers/Kestrel/test/FunctionalTests/ResponseTests.cs @@ -23,7 +23,13 @@ using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; using Microsoft.AspNetCore.Server.Kestrel.Tests; +#if LIBUV +using Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv; +#elif SOCKETS +using Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets; +#endif using Microsoft.AspNetCore.Testing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; @@ -462,8 +468,10 @@ await connection.Send( Assert.Empty(coreLogs.Where(w => w.LogLevel > LogLevel.Information)); } - [Fact] - public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate(bool fin) { var logger = LoggerFactory.CreateLogger($"{ typeof(ResponseTests).FullName}.{ nameof(ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate)}"); const int chunkSize = 1024; @@ -473,21 +481,35 @@ public async Task ConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(); - mockKestrelTrace - .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny(), It.IsAny())) - .Callback(() => responseRateTimeoutMessageLogged.SetResult()); - mockKestrelTrace - .Setup(trace => trace.ConnectionStop(It.IsAny())) - .Callback(() => connectionStopMessageLogged.SetResult()); + TestSink.MessageLogged += context => + { + switch (context.EventId.Name) + { + case "ResponseMinimumDataRateNotSatisfied": + responseRateTimeoutMessageLogged.SetResult(); + break; + case "ConnectionStop": + connectionStopMessageLogged.SetResult(); + break; + case "ConnectionWriteFin": + connectionWriteFinMessageLogged.SetResult(); + break; + case "ConnectionWriteRst": + connectionWriteRstMessageLogged.SetResult(); + break; + } + }; - var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) + var testContext = new TestServiceContext(LoggerFactory) { ServerOptions = { + FinOnError = fin, Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) @@ -533,7 +555,7 @@ async Task App(HttpContext context) } } - await using (var server = new TestServer(App, testContext)) + await using (var server = new TestServer(App, testContext, configureListenOptions: _ => { }, services => SetFinOnError(services, fin))) { using (var connection = server.CreateConnection()) { @@ -553,8 +575,16 @@ await connection.Send( await requestAborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30)); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); + if (fin) + { + await connectionWriteFinMessageLogged.Task.DefaultTimeout(); + } + else + { + await connectionWriteRstMessageLogged.Task.DefaultTimeout(); + } await appFuncCompleted.Task.DefaultTimeout(); - await AssertStreamAborted(connection.Stream, chunkSize * chunks); + await AssertStreamAborted(connection.Stream, chunkSize * chunks, fin); sw.Stop(); logger.LogInformation("Connection was aborted after {totalMilliseconds}ms.", sw.ElapsedMilliseconds); @@ -562,8 +592,10 @@ await connection.Send( } } - [Fact] - public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate(bool fin) { const int chunkSize = 1024; const int chunks = 256 * 1024; @@ -573,21 +605,35 @@ public async Task HttpsConnectionClosedWhenResponseDoesNotSatisfyMinimumDataRate var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var aborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var appFuncCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(); - mockKestrelTrace - .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny(), It.IsAny())) - .Callback(() => responseRateTimeoutMessageLogged.SetResult()); - mockKestrelTrace - .Setup(trace => trace.ConnectionStop(It.IsAny())) - .Callback(() => connectionStopMessageLogged.SetResult()); + TestSink.MessageLogged += context => + { + switch (context.EventId.Name) + { + case "ResponseMinimumDataRateNotSatisfied": + responseRateTimeoutMessageLogged.SetResult(); + break; + case "ConnectionStop": + connectionStopMessageLogged.SetResult(); + break; + case "ConnectionWriteFin": + connectionWriteFinMessageLogged.SetResult(); + break; + case "ConnectionWriteRst": + connectionWriteRstMessageLogged.SetResult(); + break; + } + }; - var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) + var testContext = new TestServiceContext(LoggerFactory) { ServerOptions = { + FinOnError = fin, Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)) @@ -627,7 +673,8 @@ void ConfigureListenOptions(ListenOptions listenOptions) { await aborted.Task.DefaultTimeout(); } - }, testContext, ConfigureListenOptions)) + }, testContext, ConfigureListenOptions, + services => SetFinOnError(services, fin))) { using (var connection = server.CreateConnection()) { @@ -642,16 +689,26 @@ void ConfigureListenOptions(ListenOptions listenOptions) await aborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30)); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); + if (fin) + { + await connectionWriteFinMessageLogged.Task.DefaultTimeout(); + } + else + { + await connectionWriteRstMessageLogged.Task.DefaultTimeout(); + } await appFuncCompleted.Task.DefaultTimeout(); - await AssertStreamAborted(connection.Stream, chunkSize * chunks); + await AssertStreamAborted(connection.Stream, chunkSize * chunks, fin); } } } } - [Fact] - public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressure() + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressure(bool fin) { const int bufferSize = 65536; const int bufferCount = 100; @@ -660,21 +717,35 @@ public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressu var responseRateTimeoutMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var connectionStopMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteFinMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var connectionWriteRstMessageLogged = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var requestAborted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var copyToAsyncCts = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var mockKestrelTrace = new Mock(); - mockKestrelTrace - .Setup(trace => trace.ResponseMinimumDataRateNotSatisfied(It.IsAny(), It.IsAny())) - .Callback(() => responseRateTimeoutMessageLogged.SetResult()); - mockKestrelTrace - .Setup(trace => trace.ConnectionStop(It.IsAny())) - .Callback(() => connectionStopMessageLogged.SetResult()); + TestSink.MessageLogged += context => + { + switch (context.EventId.Name) + { + case "ResponseMinimumDataRateNotSatisfied": + responseRateTimeoutMessageLogged.SetResult(); + break; + case "ConnectionStop": + connectionStopMessageLogged.SetResult(); + break; + case "ConnectionWriteFin": + connectionWriteFinMessageLogged.SetResult(); + break; + case "ConnectionWriteRst": + connectionWriteRstMessageLogged.SetResult(); + break; + } + }; - var testContext = new TestServiceContext(LoggerFactory, mockKestrelTrace.Object) + var testContext = new TestServiceContext(LoggerFactory) { ServerOptions = { + FinOnError = fin, Limits = { MinResponseDataRate = new MinDataRate(bytesPerSecond: 1024 * 1024, gracePeriod: TimeSpan.FromSeconds(2)), @@ -685,8 +756,6 @@ public async Task ConnectionClosedWhenBothRequestAndResponseExperienceBackPressu testContext.InitializeHeartbeat(); - var listenOptions = new ListenOptions(new IPEndPoint(IPAddress.Loopback, 0)); - async Task App(HttpContext context) { context.RequestAborted.Register(() => @@ -711,7 +780,7 @@ async Task App(HttpContext context) copyToAsyncCts.SetException(new Exception("This shouldn't be reached.")); } - await using (var server = new TestServer(App, testContext, listenOptions)) + await using (var server = new TestServer(App, testContext, configureListenOptions: _ => { }, services => SetFinOnError(services, fin))) { using (var connection = server.CreateConnection()) { @@ -736,10 +805,18 @@ await connection.Send( await requestAborted.Task.DefaultTimeout(TimeSpan.FromSeconds(30)); await responseRateTimeoutMessageLogged.Task.DefaultTimeout(); await connectionStopMessageLogged.Task.DefaultTimeout(); + if (fin) + { + await connectionWriteFinMessageLogged.Task.DefaultTimeout(); + } + else + { + await connectionWriteRstMessageLogged.Task.DefaultTimeout(); + } // Expect OperationCanceledException instead of IOException because the server initiated the abort due to a response rate timeout. await Assert.ThrowsAnyAsync(() => copyToAsyncCts.Task).DefaultTimeout(); - await AssertStreamAborted(connection.Stream, responseSize); + await AssertStreamAborted(connection.Stream, responseSize, graceful: false); } } } @@ -985,7 +1062,7 @@ await connection.Receive( Assert.False(requestAborted); } - private async Task AssertStreamAborted(Stream stream, int totalBytes) + private async Task AssertStreamAborted(Stream stream, int totalBytes, bool graceful) { var receiveBuffer = new byte[64 * 1024]; var totalReceived = 0; @@ -998,6 +1075,8 @@ private async Task AssertStreamAborted(Stream stream, int totalBytes) if (bytes == 0) { + Assert.True(graceful, "Stream completed gracefully."); + break; } @@ -1006,7 +1085,7 @@ private async Task AssertStreamAborted(Stream stream, int totalBytes) } catch (IOException) { - // This is expected given an abort. + Assert.False(graceful, "Stream completed abortively."); } Assert.True(totalReceived < totalBytes, $"{nameof(AssertStreamAborted)} Stream completed successfully."); @@ -1080,5 +1159,26 @@ public static TheoryData NullHeaderData return dataset; } } + + private static void SetFinOnError(IServiceCollection services, bool finOnError) + { +#if LIBUV +#pragma warning disable CS0618 // Type or member is obsolete + services.Configure(options => + { + options.FinOnError = finOnError; + }); +#pragma warning restore CS0618 // Type or member is obsolete +#elif SOCKETS + services.Configure(o => + { + o.FinOnError = finOnError; + }); +#endif + services.Configure(o => + { + o.FinOnError = finOnError; + }); + } } } From 8df26a129f8cec3ac4afe429f14dc42955a4e73a Mon Sep 17 00:00:00 2001 From: wtgodbe Date: Tue, 8 Aug 2023 11:00:08 -0700 Subject: [PATCH 10/10] Update baseline, SDK --- eng/Baseline.Designer.props | 444 ++++++++++++++++++------------------ eng/Baseline.xml | 214 ++++++++--------- eng/Versions.props | 2 +- global.json | 4 +- 4 files changed, 332 insertions(+), 332 deletions(-) diff --git a/eng/Baseline.Designer.props b/eng/Baseline.Designer.props index ae468b57d8d0..e79ad5771518 100644 --- a/eng/Baseline.Designer.props +++ b/eng/Baseline.Designer.props @@ -2,28 +2,28 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - - - + + + @@ -34,120 +34,120 @@ - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - - + + - 6.0.20 + 6.0.21 - - + + - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - + - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 @@ -155,114 +155,114 @@ - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - + - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - + - 6.0.20 + 6.0.21 - - + + - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - - + + - 6.0.20 + 6.0.21 - + - 6.0.20 + 6.0.21 - - - + + + - 6.0.20 + 6.0.21 - - + + - 6.0.20 + 6.0.21 - - + + - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - - + + @@ -270,7 +270,7 @@ - 6.0.20 + 6.0.21 @@ -278,50 +278,50 @@ - 6.0.20 + 6.0.21 - + - + - + - + - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - - + + @@ -331,8 +331,8 @@ - - + + @@ -340,8 +340,8 @@ - - + + @@ -352,58 +352,58 @@ - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - - + + - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - + - 6.0.20 + 6.0.21 @@ -411,71 +411,71 @@ - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - + - + - + - + - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - - + + - 6.0.20 + 6.0.21 - - + + - 6.0.20 + 6.0.21 @@ -491,195 +491,195 @@ - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - + - 6.0.20 + 6.0.21 - - + + - 6.0.20 + 6.0.21 - - + + - 6.0.20 + 6.0.21 - + - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - + - 6.0.20 + 6.0.21 - - + + - - + + - - + + - 6.0.20 + 6.0.21 - - + + - - + + - - + + - - + + - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - - - - + + + + - 6.0.20 + 6.0.21 @@ -688,69 +688,69 @@ - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - + - 6.0.20 + 6.0.21 - + - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 @@ -769,7 +769,7 @@ - 6.0.20 + 6.0.21 @@ -788,7 +788,7 @@ - 6.0.20 + 6.0.21 @@ -804,46 +804,46 @@ - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - - - + + + - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 @@ -853,7 +853,7 @@ - 6.0.20 + 6.0.21 @@ -862,73 +862,73 @@ - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - + - + - + - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 @@ -957,11 +957,11 @@ - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 @@ -979,13 +979,13 @@ - 6.0.20 + 6.0.21 - 6.0.20 + 6.0.21 - + \ No newline at end of file diff --git a/eng/Baseline.xml b/eng/Baseline.xml index 604ee1e588e2..bb265398da62 100644 --- a/eng/Baseline.xml +++ b/eng/Baseline.xml @@ -4,111 +4,111 @@ This file contains a list of all the packages and their versions which were rele Update this list when preparing for a new patch. --> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/eng/Versions.props b/eng/Versions.props index 364271f41f9d..087f57c1fc1c 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -9,7 +9,7 @@ 6 0 22 - false + true diff --git a/global.json b/global.json index b96453f5621d..90b85c957751 100644 --- a/global.json +++ b/global.json @@ -1,9 +1,9 @@ { "sdk": { - "version": "6.0.120" + "version": "6.0.121" }, "tools": { - "dotnet": "6.0.120", + "dotnet": "6.0.121", "runtimes": { "dotnet/x64": [ "2.1.30",