Add a Sockets test to validate undesired blocking of continuations (#1973)
authorStephen Toub <stoub@microsoft.com>
Wed, 22 Jan 2020 20:55:23 +0000 (15:55 -0500)
committerGitHub <noreply@github.com>
Wed, 22 Jan 2020 20:55:23 +0000 (15:55 -0500)
We go out of our way (e.g. on Unix queueing work off of the epoll threads) to ensure that arbitrary user code running as part of socket continuations doesn't block those threads and potentially deadlock.  Add a test to help validate this.

src/libraries/System.Net.Sockets/tests/FunctionalTests/SendReceive.cs

index b64e9aa..30c8c1c 100644 (file)
@@ -1770,7 +1770,53 @@ namespace System.Net.Sockets.Tests
                 }
             }
         }
+
+        [Fact]
+        public async Task BlockingAsyncContinuations_OperationsStillCompleteSuccessfully()
+        {
+            if (UsesSync) return;
+
+            using (var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
+            using (var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
+            {
+                listener.BindToAnonymousPort(IPAddress.Loopback);
+                listener.Listen(1);
+
+                await client.ConnectAsync(listener.LocalEndPoint);
+                using (Socket server = await listener.AcceptAsync())
+                {
+                    await Task.Run(async delegate // escape the xunit sync context / task scheduler
+                    {
+                        const int SendDelayMs = 100;
+
+                        Task sendTask = Task.Delay(SendDelayMs)
+                            .ContinueWith(_ => server.SendAsync(new byte[1], SocketFlags.None))
+                            .Unwrap();
+                        await client.ReceiveAsync(new byte[1], SocketFlags.None);
+                        sendTask.GetAwaiter().GetResult(); // should have already completed
+
+                        // We may now be executing here as part of the continuation invoked synchronously
+                        // when the client ReceiveAsync task was completed. Validate that if socket callbacks block
+                        // (undesirably), other operations on that socket can still be processed.
+                        var mre = new ManualResetEventSlim();
+                        sendTask = Task.Delay(SendDelayMs)
+                            .ContinueWith(_ => server.SendAsync(new byte[1], SocketFlags.None))
+                            .Unwrap();
+                        Task receiveTask = client
+                            .ReceiveAsync(new byte[1], SocketFlags.None)
+                            .ContinueWith(t => { mre.Set(); return t; }, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default)
+                            .Unwrap();
+                        mre.Wait(); // block waiting for other operations on this socket to complete
+
+                        sendTask.GetAwaiter().GetResult();
+                        await sendTask;
+                        await receiveTask;
+                    });
+                }
+            }
+        }
     }
+
     public sealed class SendReceiveMemoryNativeTask : SendReceive<SocketHelperMemoryNativeTask>
     {
         public SendReceiveMemoryNativeTask(ITestOutputHelper output) : base(output) { }