public static void BasicHttp_Abort_ChannelFactory_Operations_Active() { // Test creates 2 channels from a single channel factory and // aborts the channel factory while both channels are executing // operations. This verifies the operations are cancelled and // the channel factory is in the correct state. BasicHttpBinding binding = null; TimeSpan delayOperation = TimeSpan.FromSeconds(3); ChannelFactory <IWcfService> factory = null; IWcfService serviceProxy1 = null; IWcfService serviceProxy2 = null; try { // *** SETUP *** \\ binding = new BasicHttpBinding(BasicHttpSecurityMode.None); binding.CloseTimeout = ScenarioTestHelpers.TestTimeout; factory = new ChannelFactory <IWcfService>(binding, new EndpointAddress(Endpoints.HttpBaseAddress_Basic)); serviceProxy1 = factory.CreateChannel(); serviceProxy2 = factory.CreateChannel(); // *** EXECUTE *** \\ Task <string> t1 = serviceProxy1.EchoWithTimeoutAsync("first", delayOperation); Task <string> t2 = serviceProxy2.EchoWithTimeoutAsync("second", delayOperation); factory.Abort(); // *** VALIDATE *** \\ Assert.True(factory.State == CommunicationState.Closed, String.Format("Expected factory state 'Closed', actual was '{0}'", factory.State)); Assert.Throws <TaskCanceledException>(() => t1.GetAwaiter().GetResult()); Assert.Throws <TaskCanceledException>(() => t2.GetAwaiter().GetResult()); Assert.True(((ICommunicationObject)serviceProxy1).State == CommunicationState.Closed, String.Format("Expected channel 1 state 'Closed', actual was '{0}'", ((ICommunicationObject)serviceProxy1).State)); Assert.True(((ICommunicationObject)serviceProxy2).State == CommunicationState.Closed, String.Format("Expected channel 2 state 'Closed', actual was '{0}'", ((ICommunicationObject)serviceProxy2).State)); // *** CLEANUP *** \\ ((ICommunicationObject)serviceProxy1).Abort(); ((ICommunicationObject)serviceProxy2).Abort(); } finally { // *** ENSURE CLEANUP *** \\ ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)serviceProxy1, (ICommunicationObject)serviceProxy2, factory); } }
public static void BasicHttp_Async_Close_ChannelFactory_Operations_Active() { // Test creates 2 channels from a single channel factory and // asynchronously closes the channel factory while both channels are // executing operations. This verifies the operations are cancelled and // the channel factory is in the correct state. BasicHttpBinding binding = null; TimeSpan delayOperation = TimeSpan.FromSeconds(3); ChannelFactory <IWcfService> factory = null; IWcfService serviceProxy1 = null; IWcfService serviceProxy2 = null; string expectedEcho1 = "first"; string expectedEcho2 = "second"; try { // *** SETUP *** \\ binding = new BasicHttpBinding(BasicHttpSecurityMode.None); binding.CloseTimeout = ScenarioTestHelpers.TestTimeout; binding.SendTimeout = ScenarioTestHelpers.TestTimeout; factory = new ChannelFactory <IWcfService>(binding, new EndpointAddress(Endpoints.HttpBaseAddress_Basic)); serviceProxy1 = factory.CreateChannel(); serviceProxy2 = factory.CreateChannel(); // *** EXECUTE *** \\ Task <string> t1 = serviceProxy1.EchoWithTimeoutAsync(expectedEcho1, delayOperation); Task <string> t2 = serviceProxy2.EchoWithTimeoutAsync(expectedEcho2, delayOperation); Task factoryTask = Task.Factory.FromAsync(factory.BeginClose, factory.EndClose, TaskCreationOptions.None); // *** VALIDATE *** \\ factoryTask.GetAwaiter().GetResult(); Assert.True(factory.State == CommunicationState.Closed, String.Format("Expected factory state 'Closed', actual was '{0}'", factory.State)); Exception exception1 = null; Exception exception2 = null; string actualEcho1 = null; string actualEcho2 = null; // Verification is slightly more complex for the close with active operations because // we don't know which might have completed first and whether the channel factory // was able to close and dispose either channel before it completed. So we just // ensure the Tasks complete with an exception or a successful return and have // been closed by the factory. try { actualEcho1 = t1.GetAwaiter().GetResult(); } catch (Exception e) { exception1 = e; } try { actualEcho2 = t2.GetAwaiter().GetResult(); } catch (Exception e) { exception2 = e; } Assert.True(exception1 != null || actualEcho1 != null, "First operation should have thrown Exception or returned an echo"); Assert.True(exception2 != null || actualEcho2 != null, "Second operation should have thrown Exception or returned an echo"); Assert.True(actualEcho1 == null || String.Equals(expectedEcho1, actualEcho1), String.Format("First operation returned '{0}' but expected '{1}'.", expectedEcho1, actualEcho1)); Assert.True(actualEcho2 == null || String.Equals(expectedEcho2, actualEcho2), String.Format("Second operation returned '{0}' but expected '{1}'.", expectedEcho2, actualEcho2)); Assert.True(((ICommunicationObject)serviceProxy1).State == CommunicationState.Closed, String.Format("Expected channel 1 state 'Closed', actual was '{0}'", ((ICommunicationObject)serviceProxy1).State)); Assert.True(((ICommunicationObject)serviceProxy2).State == CommunicationState.Closed, String.Format("Expected channel 2 state 'Closed', actual was '{0}'", ((ICommunicationObject)serviceProxy2).State)); // *** CLEANUP *** \\ ((ICommunicationObject)serviceProxy1).Abort(); ((ICommunicationObject)serviceProxy2).Abort(); } finally { // *** ENSURE CLEANUP *** \\ ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)serviceProxy1, (ICommunicationObject)serviceProxy2, factory); } }
public static void Abort_During_Implicit_Open_Closes_Async_Waiters() { // This test is a regression test of an issue with CallOnceManager. // When a single proxy is used to make several service calls without // explicitly opening it, the CallOnceManager queues up all the requests // that happen while it is opening the channel (or handling previously // queued service calls. If the channel was closed or faulted during // the handling of any queued requests, it caused a pathological worst // case where every queued request waited for its complete SendTimeout // before failing. // // This test operates by making multiple concurrent asynchronous service // calls, but stalls the Opening event to allow them to be queued before // any of them are allowed to proceed. It then closes the channel when // the first service operation is allowed to proceed. This causes the // CallOnce manager to deal with all its queued operations and cause // them to complete other than by timing out. BasicHttpBinding binding = null; ChannelFactory <IWcfService> factory = null; IWcfService serviceProxy = null; int timeoutMs = 20000; long operationsQueued = 0; int operationCount = 5; Task <string>[] tasks = new Task <string> [operationCount]; Exception[] exceptions = new Exception[operationCount]; string[] results = new string[operationCount]; bool isClosed = false; DateTime endOfOpeningStall = DateTime.Now; int serverDelayMs = 100; TimeSpan serverDelayTimeSpan = TimeSpan.FromMilliseconds(serverDelayMs); string testMessage = "testMessage"; try { // *** SETUP *** \\ binding = new BasicHttpBinding(BasicHttpSecurityMode.None); binding.TransferMode = TransferMode.Streamed; // SendTimeout is the timeout used for implicit opens binding.SendTimeout = TimeSpan.FromMilliseconds(timeoutMs); factory = new ChannelFactory <IWcfService>(binding, new EndpointAddress(Endpoints.HttpBaseAddress_Basic_Text)); serviceProxy = factory.CreateChannel(); // Force the implicit open to stall until we have multiple concurrent calls pending. // This forces the CallOnceManager to have a queue of waiters it will need to notify. ((ICommunicationObject)serviceProxy).Opening += (s, e) => { // Wait until we see sync calls have been queued DateTime startOfOpeningStall = DateTime.Now; while (true) { endOfOpeningStall = DateTime.Now; // Don't wait forever -- if we stall longer than the SendTimeout, it means something // is wrong other than what we are testing, so just fail early. if ((endOfOpeningStall - startOfOpeningStall).TotalMilliseconds > timeoutMs) { Assert.True(false, "The Opening event timed out waiting for operations to queue, which was not expected for this test."); } // As soon as we have all our Tasks at least running, wait a little // longer to allow them finish queuing up their waiters, then stop stalling the Opening if (Interlocked.Read(ref operationsQueued) >= operationCount) { Task.Delay(500).Wait(); endOfOpeningStall = DateTime.Now; return; } Task.Delay(100).Wait(); } }; // Each task will make a synchronous service call, which will cause all but the // first to be queued for the implicit open. The first call to complete then closes // the channel so that it is forced to deal with queued waiters. Func <string> callFunc = () => { // We increment the # ops queued before making the actual sync call, which is // technically a short race condition in the test. But reversing the order would // timeout the implicit open and fault the channel. Interlocked.Increment(ref operationsQueued); // The call of the operation is what creates the entry in the CallOnceManager queue. // So as each Task below starts, it increments the count and adds a waiter to the // queue. We ask for a small delay on the server side just to introduce a small // stall after the sync request has been made before it can complete. Otherwise // fast machines can finish all the requests before the first one finishes the Close(). Task <string> t = serviceProxy.EchoWithTimeoutAsync(testMessage, serverDelayTimeSpan); lock (tasks) { if (!isClosed) { try { isClosed = true; ((ICommunicationObject)serviceProxy).Abort(); } catch { } } } return(t.GetAwaiter().GetResult()); }; // *** EXECUTE *** \\ DateTime startTime = DateTime.Now; for (int i = 0; i < operationCount; ++i) { tasks[i] = Task.Run(callFunc); } for (int i = 0; i < operationCount; ++i) { try { results[i] = tasks[i].GetAwaiter().GetResult(); } catch (Exception ex) { exceptions[i] = ex; } } // *** VALIDATE *** \\ double elapsedMs = (DateTime.Now - endOfOpeningStall).TotalMilliseconds; // Before validating that the issue was fixed, first validate that we received the exceptions or the // results we expected. This is to verify the fix did not introduce a behavioral change other than the // elimination of the long unnecessary timeouts after the channel was closed. int nFailures = 0; for (int i = 0; i < operationCount; ++i) { if (exceptions[i] == null) { Assert.True((String.Equals("test", results[i])), String.Format("Expected operation #{0} to return '{1}' but actual was '{2}'", i, testMessage, results[i])); } else { ++nFailures; TimeoutException toe = exceptions[i] as TimeoutException; Assert.True(toe == null, String.Format("Task [{0}] should not have failed with TimeoutException", i)); } } Assert.True(nFailures > 0, String.Format("Expected at least one operation to throw an exception, but none did. Elapsed time = {0} ms.", elapsedMs)); // --- Here is the test of the actual bug fix --- // The original issue was that sync waiters in the CallOnceManager were not notified when // the channel became unusable and therefore continued to time out for the full amount. // Additionally, because they were executed sequentially, it was also possible for each one // to time out for the full amount. Given that we closed the channel, we expect all the queued // waiters to have been immediately waked up and detected failure. int expectedElapsedMs = (operationCount * serverDelayMs) + timeoutMs / 2; Assert.True(elapsedMs < expectedElapsedMs, String.Format("The {0} operations took {1} ms to complete which exceeds the expected {2} ms", operationCount, elapsedMs, expectedElapsedMs)); // *** CLEANUP *** \\ ((ICommunicationObject)serviceProxy).Close(); factory.Close(); } finally { // *** ENSURE CLEANUP *** \\ ScenarioTestHelpers.CloseCommunicationObjects((ICommunicationObject)serviceProxy, factory); } }