public void ReceiveFailConsistency([Values(true, false)] bool failInBody) { using (var serverMock = new ServerMock()) { var endpoint = serverMock.ListenEndPoint; serverMock.ResponseBody = new byte[24]; if (failInBody) { // the Opaque vs RequestId is check in the body receiving callback // I put 2 different values new MemcacheResponseHeader { Cas = 8, DataType = 12, ExtraLength = 0, KeyLength = 0, Opaque = 0, Status = Status.NoError, Opcode = Opcode.Set, TotalBodyLength = 0, }.ToData(serverMock.ResponseHeader); } else { // the magic number is checked in the header receiving callback // I corrupt it new MemcacheResponseHeader { Cas = 8, DataType = 12, ExtraLength = 0, KeyLength = 0, Opaque = 1, Status = Status.NoError, Opcode = Opcode.Set, TotalBodyLength = 0, }.ToData(serverMock.ResponseHeader); serverMock.ResponseHeader.CopyFrom(0, (uint)42); } var config = new MemcacheClientConfiguration { PoolSize = 1, }; var node = new Memcache.Node.MemcacheNode(endpoint, config); var errorMutex = new ManualResetEventSlim(false); var callbackMutex = new ManualResetEventSlim(false); Exception expectedException = null; node.TransportError += e => { Interlocked.Exchange<Exception>(ref expectedException, e); errorMutex.Set(); }; int nodeAliveCount = 0; node.NodeAlive += t => ++nodeAliveCount; int nodeDeadCount = 0; node.NodeDead += t => ++nodeDeadCount; Status receivedStatus = Status.NoError; node.TrySend( new SetRequest { RequestOpcode = Opcode.Set, RequestId = 1, Expire = TimeSpan.FromSeconds(1), Key = "Key".Select(c => (byte)c).ToArray(), Message = new byte[] { 0, 1, 2, 3, 4 }, CallBack = s => { receivedStatus = s; callbackMutex.Set(); }, }, 1000); // must wait before the next test because the TransportError event is fired after the callback call // Expected result : // * The callback must have been called before 1 sec // * The failure callback must have been called, so the received status must be InternalError // * An MemcacheException should have been raised by the receive due to the bad response // * The node should have exaclty one transport in its pool // more mean that we added it twice after a failure, less means we didn't putted it back in the pool // * The node should not be seen has dead yet Assert.IsTrue(callbackMutex.Wait(1000), @"The 1st callback has not been received after 1 second"); Assert.IsTrue(errorMutex.Wait(1000), @"The 1st error has not been received after 1 second"); Assert.AreEqual(Status.InternalError, receivedStatus, @"A bad response has not sent an InternalError to the request callback"); Assert.IsInstanceOf<Memcache.Exceptions.MemcacheException>(expectedException, @"A bad response has not triggered a transport error. Expected a MemcacheException."); Assert.AreEqual(0, nodeDeadCount, @"The node has been detected has dead before a new send has been made"); // After a short delay, the transport should be back in the transport pool (node.PoolSize == 1) Assert.That(() => node.PoolSize, new DelayedConstraint(new EqualConstraint(1), 2000, 100), "After a while, the transport should be back in the pool"); new MemcacheResponseHeader { Cas = 8, DataType = 12, ExtraLength = 0, KeyLength = 0, // must be the same or it will crash : TODO add a test to ensure we detect this fail Opaque = 1, Status = Status.NoError, Opcode = Opcode.Set, TotalBodyLength = 0, }.ToData(serverMock.ResponseHeader); serverMock.ResponseBody = null; expectedException = null; callbackMutex.Reset(); receivedStatus = Status.NoError; var result = node.TrySend( new SetRequest { RequestOpcode = Opcode.Set, RequestId = 1, Expire = TimeSpan.FromSeconds(1), Key = "Key".Select(c => (byte)c).ToArray(), Message = new byte[] { 0, 1, 2, 3, 4 }, CallBack = s => { receivedStatus = s; callbackMutex.Set(); }, }, 1000); // Expected result : // * An SocketException should have been raised by the send, since the previous receice has disconnected the socket // * The return must be true, because the request have been enqueued before the transport seen the socket died // * The failure callback must have been called, so the received status must be InternalError Assert.IsTrue(callbackMutex.Wait(1000), @"The 2nd callback has not been received after 1 second"); Assert.IsTrue(result, @"The first failed request should not see a false return"); Assert.AreEqual(Status.InternalError, receivedStatus, @"The send operation should have detected that the socket is dead"); // After a short delay, the transport should connect Assert.That(() => node.PoolSize, new DelayedConstraint(new EqualConstraint(1), 2000, 100), "After a while, the transport should manage to connect"); expectedException = null; callbackMutex.Reset(); result = node.TrySend( new SetRequest { RequestOpcode = Opcode.Set, RequestId = 1, Expire = TimeSpan.FromSeconds(1), Key = "Key".Select(c => (byte)c).ToArray(), Message = new byte[] { 0, 1, 2, 3, 4 }, CallBack = s => { receivedStatus = s; callbackMutex.Set(); }, }, 1000); // Expected result : everything should works fine now // * The return must be true, because the new transport should be available now // * No exception should have been raised // * The callback must have been called and with a NoError status Assert.IsTrue(result, @"The node has not been able to send a new request after a disconnection"); Assert.IsTrue(callbackMutex.Wait(1000), @"The message has not been received after 1 second, case after reconnection"); Assert.AreEqual(Status.NoError, receivedStatus, @"The response after a reconnection is still not NoError"); Assert.IsNull(expectedException, "The request shouldn't have thrown an exception"); } }