package net.jxta.impl.endpoint;
import static org.junit.Assert.*;
import java.io.IOException;
import java.util.LinkedList;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import net.jxta.endpoint.EndpointAddress;
import net.jxta.endpoint.Message;
import net.jxta.endpoint.Messenger;
import net.jxta.endpoint.MessengerStateListener;
import net.jxta.endpoint.OutgoingMessageEvent;
import net.jxta.id.IDFactory;
import net.jxta.peergroup.PeerGroupID;
import net.jxta.test.util.JUnitRuleMockery;
import org.jmock.Expectations;
import org.jmock.Sequence;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
public class AsynchronousMessengerTest {
private static final int QUEUE_SIZE = 10;
private TestableAsynchronousMessenger messenger;
private MessengerStateListener mockListener;
private Message msg;
private ExecutorService concurrentExecutor;
@Rule
public JUnitRuleMockery mockContext = new JUnitRuleMockery();
@Before
public void setUp() throws Exception {
messenger = new TestableAsynchronousMessenger(PeerGroupID.defaultNetPeerGroupID, new EndpointAddress("http", "1.2.3.4", null, null), QUEUE_SIZE);
mockListener = mockContext.mock(MessengerStateListener.class);
msg = new Message();
concurrentExecutor = Executors.newSingleThreadExecutor();
}
@After
public void tearDown() throws Exception {
concurrentExecutor.shutdownNow();
}
@Test
public void testInitiallyConnected() {
assertTrue(messenger.getState() == Messenger.CONNECTED);
}
@Test
public void testSendMessageN_returnsTrueOnEnqueue() {
assertTrue(messenger.sendMessageN(new Message(), "TestService", null));
}
@Test
public void testSendMessageN_enqueuedMessagesArePushed() {
messenger.sendMessageN(msg, "TestService", null);
assertEquals(1, messenger.sentMessages.size());
assertSame(msg, messenger.sentMessages.poll().getMessage());
}
@Test
public void testSendMessageN_returnsFalseWhenSaturated() {
saturateMessenger();
messenger.refuseToSend.set(true);
assertFalse(messenger.sendMessageN(msg, null, null));
assertEquals(OutgoingMessageEvent.OVERFLOW, msg.getMessageProperty(Messenger.class));
}
private void saturateMessenger() {
assertTrue(messenger.refuseToSend.compareAndSet(false, true));
while(messenger.sendMessageN(new Message(), null, null));
messenger.refuseToSend.set(false);
}
@Test
public void testPullNextWrite() {
enqueueMessages(3);
messenger.pullMessages();
assertEquals(3, messenger.sentMessages.size());
}
@Test
public void testSendMessageN_failure() {
messenger.sendException = new Exception("Bad stuff happened!");
assertFalse(messenger.sendMessageN(msg, null, null));
assertTrue(TransportUtils.isMarkedWithFailure(msg));
assertSame(messenger.sendException, TransportUtils.getFailureCause(msg));
}
@Test
public void testMessagesMarkedOnSuccess() {
messenger.sendMessageN(msg, null, null);
messenger.sentMessages.poll().getWriteListener().writeSuccess();
assertTrue(TransportUtils.isMarkedWithSuccess(msg));
}
@Test
public void testSendMessageB_blocksUntilSent() throws Exception {
Future<Void> sendTask = concurrentExecutor.submit(new BlockingSender(messenger, msg));
QueuedMessage sentMessage = messenger.sentMessages.poll(100L, TimeUnit.MILLISECONDS);
assertNotNull(sentMessage);
assertSame(msg, sentMessage.getMessage());
sentMessage.getWriteListener().writeSuccess();
assertNull(sendTask.get(100L, TimeUnit.MILLISECONDS));
}
@Test
public void testSendMessageB_blocksForSendQueueSpace() throws Exception {
saturateMessenger();
Future<Void> sendTask = concurrentExecutor.submit(new BlockingSender(messenger, msg));
// we expect that, despite the messenger now being able to send messages,
// the sender will not send anything as it cannot put anything on to the queue.
try {
sendTask.get(100L, TimeUnit.MILLISECONDS);
fail("expected send task to be stalled");
} catch(TimeoutException e) {
// timed out as expected
sendTask.cancel(true);
}
}
@Test
public void testSendMessageB_canSendMultiple() throws Exception {
enqueueMessages(3);
assertEquals(0, messenger.sentMessages.size());
Future<Void> sendTask = concurrentExecutor.submit(new BlockingSender(messenger, msg));
markAsSent(4, 100L);
assertNull(sendTask.get(100L, TimeUnit.MILLISECONDS));
}
@Test(timeout=100L)
public void testSendMessageB_failure() throws Exception {
messenger.sendException = new Exception("bad stuff happened");
try {
messenger.sendMessageB(msg, null, null);
fail("Expected exception on send");
} catch(IOException e) {
// if an InterruptedException is seen here, it typically
// means JUnit interrupted execution due to the timeout
// specified in the annotation
assertEquals("Failed to write message", e.getMessage());
assertSame(messenger.sendException, e.getCause());
}
}
@Test
public void testSendMessageB_interrupted() throws Exception {
final AtomicReference<IOException> result = new AtomicReference<IOException>();
final CountDownLatch latch = new CountDownLatch(1);
Thread sendThread = new Thread(new Runnable() {
public void run() {
try {
messenger.sendMessageB(msg, null, null);
result.set(null);
} catch(IOException e) {
result.set(e);
}
latch.countDown();
};
});
sendThread.start();
Thread.sleep(10L);
sendThread.interrupt();
assertTrue(latch.await(100L, TimeUnit.MILLISECONDS));
IOException resultValue = result.get();
assertTrue(resultValue != null);
assertTrue(resultValue.getCause() instanceof InterruptedException);
}
@Test
public void testSendStateTransitions() {
messenger.addStateListener(mockListener);
final Sequence seq = mockContext.sequence("saturation-events");
mockContext.checking(new Expectations() {{
one(mockListener).messengerStateChanged(Messenger.SENDING); will(returnValue(true)); inSequence(seq);
one(mockListener).messengerStateChanged(Messenger.SENDINGSATURATED); will(returnValue(true)); inSequence(seq);
}});
saturateMessenger();
mockContext.assertIsSatisfied();
mockContext.checking(new Expectations() {{
one(mockListener).messengerStateChanged(Messenger.SENDING); will(returnValue(true)); inSequence(seq);
one(mockListener).messengerStateChanged(Messenger.CONNECTED); will(returnValue(true)); inSequence(seq);
}});
messenger.pullMessages();
mockContext.assertIsSatisfied();
}
@Test
public void testCloseWhenIdle() {
assertTrue(messenger.getState() == Messenger.CONNECTED);
messenger.addStateListener(mockListener);
mockContext.checking(new Expectations() {{
one(mockListener).messengerStateChanged(Messenger.CLOSED);
}});
messenger.close();
mockContext.assertIsSatisfied();
assertTrue(messenger.closeRequested.get());
}
@Test
public void testConnectionFailureWhenClosing() {
enqueueMessages(1);
messenger.addStateListener(mockListener);
final Sequence seq = mockContext.sequence("close-events");
mockContext.checking(new Expectations() {{
one(mockListener).messengerStateChanged(Messenger.CLOSING); will(returnValue(true)); inSequence(seq);
}});
messenger.close();
assertFalse(messenger.closeRequested.get());
mockContext.assertIsSatisfied();
// now emulate the connection failing before we have sent all pending messages
// this should result initially in a state change indicating an attempt to reconnect
// then an immediate failure (as AsynchMessenger does not yet support reconnecting)
mockContext.checking(new Expectations() {{
one(mockListener).messengerStateChanged(Messenger.RECONCLOSING); will(returnValue(true)); inSequence(seq);
one(mockListener).messengerStateChanged(Messenger.BROKEN); will(returnValue(true)); inSequence(seq);
}});
messenger.connectionFailed();
mockContext.assertIsSatisfied();
}
@Test
public void testCloseWithPendingMessages() {
enqueueMessages(2);
messenger.addStateListener(mockListener);
final Sequence seq = mockContext.sequence("close-events");
mockContext.checking(new Expectations() {{
one(mockListener).messengerStateChanged(Messenger.CLOSING); will(returnValue(true)); inSequence(seq);
}});
messenger.close();
assertFalse(messenger.closeRequested.get());
mockContext.assertIsSatisfied();
mockContext.checking(new Expectations() {{
one(mockListener).messengerStateChanged(Messenger.CLOSED); will(returnValue(true)); inSequence(seq);
}});
messenger.pullMessages();
mockContext.assertIsSatisfied();
assertTrue(messenger.closeRequested.get());
messenger.emulateConnectionGracefulClose();
assertEquals(2, messenger.sentMessages.size());
}
@Test
public void testSendMessageN_whenClosing() {
enqueueMessages(1);
messenger.close();
assertFalse(messenger.sendMessageN(msg, null, null));
assertTrue(TransportUtils.isMarkedWithFailure(msg));
assertEquals("Messenger is closed. It cannot be used to send messages", TransportUtils.getFailureCause(msg).getMessage());
}
@Test
public void testSendMessageB_whenClosing() {
enqueueMessages(1);
messenger.close();
try {
messenger.sendMessageB(msg, null, null);
fail("expected IOException to be thrown");
} catch (IOException e) {
assertEquals(AsynchronousMessenger.CLOSED_MESSAGE, e.getMessage());
}
}
@Test
public void testConnectionFailure_whenIdle() {
messenger.addStateListener(mockListener);
final Sequence seq = mockContext.sequence("event-seq");
mockContext.checking(new Expectations() {{
one(mockListener).messengerStateChanged(Messenger.DISCONNECTED); will(returnValue(true)); inSequence(seq);
}});
messenger.emulateConnectionDeath();
mockContext.assertIsSatisfied();
mockContext.checking(new Expectations() {{
one(mockListener).messengerStateChanged(Messenger.RECONNECTING); will(returnValue(true)); inSequence(seq);
one(mockListener).messengerStateChanged(Messenger.BROKEN); will(returnValue(true)); inSequence(seq);
}});
assertFalse(messenger.sendMessageN(msg, null, null));
mockContext.assertIsSatisfied();
}
@Test
public void testConnectionFailure_whenSending() {
LinkedList<Message> messages = enqueueMessages(5);
messenger.addStateListener(mockListener);
final Sequence seq = mockContext.sequence("event-seq");
mockContext.checking(new Expectations() {{
one(mockListener).messengerStateChanged(Messenger.RECONNECTING); will(returnValue(true)); inSequence(seq);
one(mockListener).messengerStateChanged(Messenger.BROKEN); will(returnValue(true)); inSequence(seq);
}});
messenger.emulateConnectionDeath();
mockContext.assertIsSatisfied();
for(Message msg : messages) {
assertTrue(TransportUtils.isMarkedWithFailure(msg));
assertEquals("Messenger unexpectedly closed", TransportUtils.getFailureCause(msg).getMessage());
}
}
private void markAsSent(int numMessages, long timeoutInMillis) throws InterruptedException {
int numMarked = 0;
long startTime = System.currentTimeMillis();
while(numMarked < numMessages && elapsedTime(startTime) < timeoutInMillis) {
QueuedMessage message = messenger.sentMessages.poll(timeoutInMillis, TimeUnit.MILLISECONDS);
if(message != null) {
message.getWriteListener().writeSuccess();
numMarked++;
}
}
assertEquals(numMessages, numMarked);
}
private LinkedList<Message> enqueueMessages(int numMessages) {
LinkedList<Message> enqueuedMessages = new LinkedList<Message>();
assertTrue(messenger.refuseToSend.compareAndSet(false, true));
for(int i=0; i < numMessages; i++) {
Message message = new Message();
enqueuedMessages.add(message);
assertTrue(messenger.sendMessageN(message, null, null));
}
messenger.refuseToSend.set(false);
return enqueuedMessages;
}
private long elapsedTime(long startTime) {
return System.currentTimeMillis() - startTime;
}
private class BlockingSender implements Callable<Void> {
private Messenger messengerToUse;
private Message message;
public BlockingSender(Messenger messenger, Message message) {
this.messengerToUse = messenger;
this.message = message;
}
public Void call() throws Exception {
messengerToUse.sendMessageB(message, null, null);
return null;
}
}
private class TestableAsynchronousMessenger extends AsynchronousMessenger {
public Exception sendException;
public EndpointAddress localAddress = new EndpointAddress("http", "remote", null, null);
public EndpointAddress logicalDestAddress = new EndpointAddress("jxta", IDFactory.newPeerID(PeerGroupID.defaultNetPeerGroupID).getUniqueValue().toString(), null, null);
public AtomicBoolean refuseToSend = new AtomicBoolean(false);
public BlockingQueue<QueuedMessage> sentMessages = new LinkedBlockingQueue<QueuedMessage>();
public AtomicBoolean closeRequested = new AtomicBoolean(false);
public AtomicBoolean connectionDead = new AtomicBoolean(false);
public TestableAsynchronousMessenger(PeerGroupID homeGroupID, EndpointAddress dest, int messageQueueSize) {
super(homeGroupID, dest, messageQueueSize);
}
@Override
public EndpointAddress getLocalAddress() {
return localAddress;
}
@Override
public EndpointAddress getLogicalDestinationAddress() {
return logicalDestAddress;
}
@Override
public boolean sendMessageImpl(QueuedMessage message) {
if(refuseToSend.get()) {
return false;
}
if(connectionDead.get()) {
message.getWriteListener().writeSubmitted();
message.getWriteListener().writeFailure(new IOException("Messenger unexpectedly closed"));
return false;
}
if(sendException != null) {
message.getWriteListener().writeSubmitted();
message.getWriteListener().writeFailure(sendException);
return false;
}
sentMessages.add(message);
message.getWriteListener().writeSubmitted();
return true;
}
@Override
public void requestClose() {
closeRequested.set(true);
}
public void emulateConnectionDeath() {
connectionDead.set(true);
connectionFailed();
}
public void emulateConnectionGracefulClose() {
connectionDead.set(true);
connectionCloseComplete();
}
}
}