|
1 |
| -#region Copyright notice and license |
| 1 | +#region Copyright notice and license |
2 | 2 |
|
3 | 3 | // Copyright 2019 The gRPC Authors
|
4 | 4 | //
|
|
17 | 17 | #endregion
|
18 | 18 |
|
19 | 19 | using System.Net;
|
| 20 | +using System.Threading.Tasks; |
20 | 21 | using Greet;
|
21 | 22 | using Grpc.Core;
|
22 | 23 | using Grpc.Net.Client.Internal;
|
23 | 24 | using Grpc.Net.Client.Internal.Http;
|
24 | 25 | using Grpc.Net.Client.Tests.Infrastructure;
|
25 | 26 | using Grpc.Shared;
|
26 | 27 | using Grpc.Tests.Shared;
|
| 28 | +using Microsoft.Extensions.DependencyInjection; |
| 29 | +using Microsoft.Extensions.Logging; |
27 | 30 | using NUnit.Framework;
|
28 | 31 |
|
29 | 32 | namespace Grpc.Net.Client.Tests;
|
@@ -177,4 +180,81 @@ await streamContent.AddDataAndWait(await ClientTestHelpers.GetResponseDataAsync(
|
177 | 180 | Assert.IsTrue(moveNextTask4.IsCompleted);
|
178 | 181 | Assert.IsFalse(await moveNextTask3.DefaultTimeout());
|
179 | 182 | }
|
| 183 | + |
| 184 | + [Test] |
| 185 | + public async Task AsyncDuplexStreamingCall_CancellationDisposeRace_Success() |
| 186 | + { |
| 187 | + // Arrange |
| 188 | + var services = new ServiceCollection(); |
| 189 | + services.AddNUnitLogger(); |
| 190 | + var loggerFactory = services.BuildServiceProvider().GetRequiredService<ILoggerFactory>(); |
| 191 | + var logger = loggerFactory.CreateLogger(GetType()); |
| 192 | + |
| 193 | + for (var i = 0; i < 20; i++) |
| 194 | + { |
| 195 | + // Let's mimic a real call first to get GrpcCall.RunCall where we need to for reproducing the deadlock. |
| 196 | + var streamContent = new SyncPointMemoryStream(); |
| 197 | + var requestContentTcs = new TaskCompletionSource<Task<Stream>>(TaskCreationOptions.RunContinuationsAsynchronously); |
| 198 | + |
| 199 | + PushStreamContent<HelloRequest, HelloReply>? content = null; |
| 200 | + |
| 201 | + var handler = TestHttpMessageHandler.Create(async request => |
| 202 | + { |
| 203 | + content = (PushStreamContent<HelloRequest, HelloReply>)request.Content!; |
| 204 | + var streamTask = content.ReadAsStreamAsync(); |
| 205 | + requestContentTcs.SetResult(streamTask); |
| 206 | + // Wait for RequestStream.CompleteAsync() |
| 207 | + await streamTask; |
| 208 | + return ResponseUtils.CreateResponse(HttpStatusCode.OK, new StreamContent(streamContent)); |
| 209 | + }); |
| 210 | + var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions |
| 211 | + { |
| 212 | + HttpHandler = handler, |
| 213 | + LoggerFactory = loggerFactory |
| 214 | + }); |
| 215 | + var invoker = channel.CreateCallInvoker(); |
| 216 | + |
| 217 | + var cts = new CancellationTokenSource(); |
| 218 | + |
| 219 | + var call = invoker.AsyncDuplexStreamingCall<HelloRequest, HelloReply>(ClientTestHelpers.ServiceMethod, string.Empty, new CallOptions(cancellationToken: cts.Token)); |
| 220 | + await call.RequestStream.WriteAsync(new HelloRequest { Name = "1" }).DefaultTimeout(); |
| 221 | + await call.RequestStream.CompleteAsync().DefaultTimeout(); |
| 222 | + |
| 223 | + // Let's read a response |
| 224 | + var deserializationContext = new DefaultDeserializationContext(); |
| 225 | + var requestContent = await await requestContentTcs.Task.DefaultTimeout(); |
| 226 | + var requestMessage = await StreamSerializationHelper.ReadMessageAsync( |
| 227 | + requestContent, |
| 228 | + ClientTestHelpers.ServiceMethod.RequestMarshaller.ContextualDeserializer, |
| 229 | + GrpcProtocolConstants.IdentityGrpcEncoding, |
| 230 | + maximumMessageSize: null, |
| 231 | + GrpcProtocolConstants.DefaultCompressionProviders, |
| 232 | + singleMessage: false, |
| 233 | + CancellationToken.None).DefaultTimeout(); |
| 234 | + Assert.AreEqual("1", requestMessage!.Name); |
| 235 | + |
| 236 | + var actTcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously); |
| 237 | + |
| 238 | + var cancellationTask = Task.Run(async () => |
| 239 | + { |
| 240 | + await actTcs.Task; |
| 241 | + cts.Cancel(); |
| 242 | + }); |
| 243 | + var disposingTask = Task.Run(async () => |
| 244 | + { |
| 245 | + await actTcs.Task; |
| 246 | + channel.Dispose(); |
| 247 | + }); |
| 248 | + |
| 249 | + // Small pause to make sure we're waiting at the TCS everywhere. |
| 250 | + await Task.Delay(50); |
| 251 | + |
| 252 | + // Act |
| 253 | + actTcs.SetResult(null); |
| 254 | + |
| 255 | + // Assert |
| 256 | + // Cancellation and disposing should both complete quickly. If there is a deadlock then the await will timeout. |
| 257 | + await Task.WhenAll(cancellationTask, disposingTask).DefaultTimeout(); |
| 258 | + } |
| 259 | + } |
180 | 260 | }
|
0 commit comments