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