|
1 | 1 | // Licensed to the .NET Foundation under one or more agreements.
|
2 | 2 | // The .NET Foundation licenses this file to you under the MIT license.
|
3 | 3 |
|
| 4 | +using System.Buffers; |
4 | 5 | using System.Diagnostics;
|
5 | 6 | using System.Diagnostics.Metrics;
|
6 | 7 | using System.Net;
|
@@ -1145,6 +1146,137 @@ public async Task POST_Bidirectional_LargeData_Cancellation_Error(HttpProtocols
|
1145 | 1146 | }
|
1146 | 1147 | }
|
1147 | 1148 |
|
| 1149 | + internal class MemoryPoolFeature : IMemoryPoolFeature |
| 1150 | + { |
| 1151 | + public MemoryPool<byte> MemoryPool { get; set; } |
| 1152 | + } |
| 1153 | + |
| 1154 | + [ConditionalTheory] |
| 1155 | + [MsQuicSupported] |
| 1156 | + [InlineData(HttpProtocols.Http3)] |
| 1157 | + [InlineData(HttpProtocols.Http2)] |
| 1158 | + public async Task ApplicationWriteWhenConnectionClosesPreservesMemory(HttpProtocols protocol) |
| 1159 | + { |
| 1160 | + // Arrange |
| 1161 | + var memoryPool = new DiagnosticMemoryPool(new PinnedBlockMemoryPool(), allowLateReturn: true); |
| 1162 | + |
| 1163 | + var writingTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); |
| 1164 | + var cancelTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); |
| 1165 | + var completionTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); |
| 1166 | + |
| 1167 | + var builder = CreateHostBuilder(async context => |
| 1168 | + { |
| 1169 | + try |
| 1170 | + { |
| 1171 | + var requestBody = context.Request.Body; |
| 1172 | + |
| 1173 | + await context.Response.BodyWriter.FlushAsync(); |
| 1174 | + |
| 1175 | + // Test relies on Htt2Stream/Http3Stream aborting the token after stopping Http2OutputProducer/Http3OutputProducer |
| 1176 | + // It's very fragile but it is sort of a best effort test anyways |
| 1177 | + // Additionally, Http2 schedules it's stopping, so doesn't directly do anything to the PipeWriter when calling stop on Http2OutputProducer |
| 1178 | + context.RequestAborted.Register(() => |
| 1179 | + { |
| 1180 | + cancelTcs.SetResult(); |
| 1181 | + }); |
| 1182 | + |
| 1183 | + while (true) |
| 1184 | + { |
| 1185 | + var memory = context.Response.BodyWriter.GetMemory(); |
| 1186 | + |
| 1187 | + // Unblock client-side to close the connection |
| 1188 | + writingTcs.TrySetResult(); |
| 1189 | + |
| 1190 | + await cancelTcs.Task; |
| 1191 | + |
| 1192 | + // Verify memory is still rented from the memory pool after the producer has been stopped |
| 1193 | + Assert.True(memoryPool.ContainsMemory(memory)); |
| 1194 | + |
| 1195 | + context.Response.BodyWriter.Advance(memory.Length); |
| 1196 | + var flushResult = await context.Response.BodyWriter.FlushAsync(); |
| 1197 | + |
| 1198 | + if (flushResult.IsCanceled || flushResult.IsCompleted) |
| 1199 | + { |
| 1200 | + break; |
| 1201 | + } |
| 1202 | + } |
| 1203 | + |
| 1204 | + completionTcs.SetResult(); |
| 1205 | + } |
| 1206 | + catch (Exception ex) |
| 1207 | + { |
| 1208 | + writingTcs.TrySetException(ex); |
| 1209 | + // Exceptions annoyingly don't show up on the client side when doing E2E + cancellation testing |
| 1210 | + // so we need to use a TCS to observe any unexpected errors |
| 1211 | + completionTcs.TrySetException(ex); |
| 1212 | + throw; |
| 1213 | + } |
| 1214 | + }, protocol: protocol, |
| 1215 | + configureKestrel: o => |
| 1216 | + { |
| 1217 | + o.Listen(IPAddress.Parse("127.0.0.1"), 0, listenOptions => |
| 1218 | + { |
| 1219 | + listenOptions.Protocols = protocol; |
| 1220 | + listenOptions.UseHttps(TestResources.GetTestCertificate()).Use(@delegate => |
| 1221 | + { |
| 1222 | + // Connection middleware for Http/1.1 and Http/2 |
| 1223 | + return (context) => |
| 1224 | + { |
| 1225 | + // Set the memory pool used by the connection so we can observe if memory from the PipeWriter is still rented from the pool |
| 1226 | + context.Features.Set<IMemoryPoolFeature>(new MemoryPoolFeature() { MemoryPool = memoryPool }); |
| 1227 | + return @delegate(context); |
| 1228 | + }; |
| 1229 | + }); |
| 1230 | + |
| 1231 | + IMultiplexedConnectionBuilder multiplexedConnectionBuilder = listenOptions; |
| 1232 | + multiplexedConnectionBuilder.Use(@delegate => |
| 1233 | + { |
| 1234 | + // Connection middleware for Http/3 |
| 1235 | + return (context) => |
| 1236 | + { |
| 1237 | + // Set the memory pool used by the connection so we can observe if memory from the PipeWriter is still rented from the pool |
| 1238 | + context.Features.Set<IMemoryPoolFeature>(new MemoryPoolFeature() { MemoryPool = memoryPool }); |
| 1239 | + return @delegate(context); |
| 1240 | + }; |
| 1241 | + }); |
| 1242 | + }); |
| 1243 | + }); |
| 1244 | + |
| 1245 | + var httpClientHandler = new HttpClientHandler(); |
| 1246 | + httpClientHandler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; |
| 1247 | + |
| 1248 | + using (var host = builder.Build()) |
| 1249 | + using (var client = new HttpClient(httpClientHandler)) |
| 1250 | + { |
| 1251 | + await host.StartAsync().DefaultTimeout(); |
| 1252 | + |
| 1253 | + var cts = new CancellationTokenSource(); |
| 1254 | + |
| 1255 | + var request = new HttpRequestMessage(HttpMethod.Post, $"https://127.0.0.1:{host.GetPort()}/"); |
| 1256 | + request.Version = GetProtocol(protocol); |
| 1257 | + request.VersionPolicy = HttpVersionPolicy.RequestVersionExact; |
| 1258 | + |
| 1259 | + // Act |
| 1260 | + var responseTask = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); |
| 1261 | + |
| 1262 | + Logger.LogInformation("Client waiting for headers."); |
| 1263 | + var response = await responseTask.DefaultTimeout(); |
| 1264 | + await writingTcs.Task; |
| 1265 | + |
| 1266 | + Logger.LogInformation("Client canceled request."); |
| 1267 | + response.Dispose(); |
| 1268 | + |
| 1269 | + // Assert |
| 1270 | + await host.StopAsync().DefaultTimeout(); |
| 1271 | + |
| 1272 | + await completionTcs.Task; |
| 1273 | + |
| 1274 | + memoryPool.Dispose(); |
| 1275 | + |
| 1276 | + await memoryPool.WhenAllBlocksReturnedAsync(TimeSpan.FromSeconds(15)); |
| 1277 | + } |
| 1278 | + } |
| 1279 | + |
1148 | 1280 | // Verify HTTP/2 and HTTP/3 match behavior
|
1149 | 1281 | [ConditionalTheory]
|
1150 | 1282 | [MsQuicSupported]
|
|
0 commit comments