Package org.cometd.websocket.client

Source Code of org.cometd.websocket.client.BayeuxClientWebSocketTest

/*
* Copyright (c) 2008-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.cometd.websocket.client;

import java.io.IOException;
import java.net.ConnectException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import javax.websocket.ClientEndpointConfig;
import javax.websocket.WebSocketContainer;

import org.cometd.bayeux.Channel;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSession;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.bayeux.server.BayeuxServer;
import org.cometd.bayeux.server.LocalSession;
import org.cometd.bayeux.server.ServerChannel;
import org.cometd.bayeux.server.ServerMessage;
import org.cometd.bayeux.server.ServerSession;
import org.cometd.client.BayeuxClient;
import org.cometd.client.ext.AckExtension;
import org.cometd.client.transport.ClientTransport;
import org.cometd.server.AbstractServerTransport;
import org.cometd.server.BayeuxServerImpl;
import org.cometd.server.ServerSessionImpl;
import org.cometd.server.ext.AcknowledgedMessagesExtension;
import org.cometd.server.transport.JSONTransport;
import org.cometd.websocket.ClientServerWebSocketTest;
import org.cometd.websocket.server.WebSocketTransport;
import org.eclipse.jetty.util.BlockingArrayQueue;
import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
import org.eclipse.jetty.websocket.client.WebSocketClient;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import static org.cometd.bayeux.server.ConfigurableServerChannel.Initializer.Persistent;

public class BayeuxClientWebSocketTest extends ClientServerWebSocketTest
{
    public BayeuxClientWebSocketTest(String implementation)
    {
        super(implementation);
    }

    @Before
    public void init() throws Exception
    {
        prepareAndStart(null);
    }

    @Test
    public void testClientCanNegotiateTransportWithServerNotSupportingWebSocket() throws Exception
    {
        bayeux.setAllowedTransports("long-polling");

        final ClientTransport webSocketTransport = newWebSocketTransport(null);
        final ClientTransport longPollingTransport = newLongPollingTransport(null);
        final CountDownLatch failureLatch = new CountDownLatch(1);
        final BayeuxClient client = new BayeuxClient(cometdURL, webSocketTransport, longPollingTransport)
        {
            @Override
            protected void onTransportFailure(String oldTransportName, String newTransportName, Throwable failure)
            {
                Assert.assertEquals(webSocketTransport.getName(), oldTransportName);
                Assert.assertEquals(longPollingTransport.getName(), newTransportName);
                failureLatch.countDown();
            }
        };

        final CountDownLatch successLatch = new CountDownLatch(1);
        final CountDownLatch failedLatch = new CountDownLatch(1);
        client.getChannel(Channel.META_HANDSHAKE).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                if (message.isSuccessful())
                    successLatch.countDown();
                else
                    failedLatch.countDown();
            }
        });

        client.handshake();

        Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
        Assert.assertTrue(failedLatch.await(5, TimeUnit.SECONDS));
        Assert.assertTrue(successLatch.await(5, TimeUnit.SECONDS));

        disconnectBayeuxClient(client);
    }

    @Test
    public void testClientWithOnlyWebSocketCannotNegotiateWithServerNotSupportingWebSocket() throws Exception
    {
        bayeux.setAllowedTransports("long-polling");

        final ClientTransport webSocketTransport = newWebSocketTransport(null);
        final CountDownLatch failureLatch = new CountDownLatch(1);
        final BayeuxClient client = new BayeuxClient(cometdURL, webSocketTransport)
        {
            @Override
            protected void onTransportFailure(String oldTransportName, String newTransportName, Throwable failure)
            {
                Assert.assertEquals(webSocketTransport.getName(), oldTransportName);
                Assert.assertNull(newTransportName);
                failureLatch.countDown();
            }
        };

        final CountDownLatch failedLatch = new CountDownLatch(1);
        client.getChannel(Channel.META_HANDSHAKE).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                if (!message.isSuccessful())
                    failedLatch.countDown();
            }
        });

        client.handshake();

        Assert.assertTrue(failureLatch.await(5, TimeUnit.SECONDS));
        Assert.assertTrue(failedLatch.await(5, TimeUnit.SECONDS));
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.DISCONNECTED));
    }

    @Test
    public void testClientRetriesWebSocketTransportIfCannotConnect() throws Exception
    {
        final CountDownLatch connectLatch = new CountDownLatch(2);
        ClientTransport webSocketTransport = newWebSocketTransport(null);
        webSocketTransport.setOption(JettyWebSocketTransport.CONNECT_TIMEOUT_OPTION, 1000L);
        ClientTransport longPollingTransport = newLongPollingTransport(null);
        final BayeuxClient client = new BayeuxClient(cometdURL, webSocketTransport, longPollingTransport)
        {
            @Override
            protected boolean sendConnect()
            {
                if ("websocket".equals(getTransport().getName()))
                    connectLatch.countDown();
                return super.sendConnect();
            }
        };

        final CountDownLatch failedLatch = new CountDownLatch(1);
        client.getChannel(Channel.META_HANDSHAKE).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                if (!message.isSuccessful())
                    failedLatch.countDown();
            }
        });

        int port = connector.getLocalPort();
        stopServer();

        client.handshake();
        Assert.assertTrue(failedLatch.await(5, TimeUnit.SECONDS));

        prepareServer(port, null, true);
        startServer();

        Assert.assertTrue(connectLatch.await(5, TimeUnit.SECONDS));

        disconnectBayeuxClient(client);
    }

    @Test
    public void testAbortThenRestart() throws Exception
    {
        BayeuxClient client = newBayeuxClient();
        client.handshake();

        // Need to be sure that the second connect is sent otherwise
        // the abort and rehandshake may happen before the second
        // connect and the test will fail.
        Thread.sleep(1000);

        client.abort();
        Assert.assertFalse(client.isConnected());

        // Restart
        final CountDownLatch connectLatch = new CountDownLatch(1);
        client.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                if (message.isSuccessful())
                    connectLatch.countDown();
            }
        });
        client.handshake();
        Assert.assertTrue(connectLatch.await(5, TimeUnit.SECONDS));
        Assert.assertTrue(client.isConnected());

        disconnectBayeuxClient(client);
    }

    @Test
    public void testRestart() throws Exception
    {
        ClientTransport webSocketTransport = newWebSocketTransport(null);
        ClientTransport longPollingTransport = newLongPollingTransport(null);
        final BayeuxClient client = new BayeuxClient(cometdURL, webSocketTransport, longPollingTransport);

        final AtomicReference<CountDownLatch> connectedLatch = new AtomicReference<>(new CountDownLatch(1));
        final AtomicReference<CountDownLatch> disconnectedLatch = new AtomicReference<>(new CountDownLatch(2));
        client.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                if (message.isSuccessful() && "websocket".equals(client.getTransport().getName()))
                    connectedLatch.get().countDown();
                else
                    disconnectedLatch.get().countDown();
            }
        });
        client.handshake();

        // Wait for connect
        Assert.assertTrue(connectedLatch.get().await(10, TimeUnit.SECONDS));
        Assert.assertTrue(client.isConnected());
        Thread.sleep(1000);

        // Stop server
        int port = connector.getLocalPort();
        stopServer();
        Assert.assertTrue(disconnectedLatch.get().await(10, TimeUnit.SECONDS));
        Assert.assertTrue(!client.isConnected());

        // restart server
        connectedLatch.set(new CountDownLatch(1));
        prepareServer(port, null, true);
        startServer();

        // Wait for connect
        Assert.assertTrue(connectedLatch.get().await(10, TimeUnit.SECONDS));
        Assert.assertTrue(client.isConnected());

        disconnectBayeuxClient(client);
    }

    @Test
    public void testRestartAfterConnectWithFatalException() throws Exception
    {
        // ConnectException is a recoverable exception that does not disable the transport.
        // Convert it to a fatal exception so the transport would be disabled.
        // However, since it connected before this fatal exception, the transport is not disabled.
        ClientTransport webSocketTransport;
        switch (wsTransportType)
        {
            case WEBSOCKET_JSR_356:
                webSocketTransport = new org.cometd.websocket.client.WebSocketTransport(null, null, wsClientContainer)
                {
                    @Override
                    protected Delegate connect(WebSocketContainer container, ClientEndpointConfig configuration, String uri) throws IOException
                    {
                        try
                        {
                            return super.connect(container, configuration, uri);
                        }
                        catch (ConnectException x)
                        {
                            // Convert recoverable exception to unrecoverable.
                            throw new IOException(x);
                        }
                    }
                };
                break;
            case WEBSOCKET_JETTY:
                webSocketTransport = new JettyWebSocketTransport(null, null, wsClient)
                {
                    @Override
                    protected Delegate connect(WebSocketClient client, ClientUpgradeRequest request, String uri) throws IOException, InterruptedException
                    {
                        try
                        {
                            return super.connect(client, request, uri);
                        }
                        catch (ConnectException x)
                        {
                            // Convert recoverable exception to unrecoverable.
                            throw new IOException(x);
                        }
                    }
                };
                break;
            default:
                throw new IllegalArgumentException();
        }

        final BayeuxClient client = new BayeuxClient(cometdURL, webSocketTransport);

        final AtomicReference<CountDownLatch> connectedLatch = new AtomicReference<>(new CountDownLatch(1));
        final AtomicReference<CountDownLatch> disconnectedLatch = new AtomicReference<>(new CountDownLatch(2));
        client.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                if (message.isSuccessful() && "websocket".equals(client.getTransport().getName()))
                    connectedLatch.get().countDown();
                else
                    disconnectedLatch.get().countDown();
            }
        });
        client.handshake();

        // Wait for connect
        Assert.assertTrue(connectedLatch.get().await(10, TimeUnit.SECONDS));
        Assert.assertTrue(client.isConnected());
        Thread.sleep(1000);

        // Stop server
        int port = connector.getLocalPort();
        server.stop();
        Assert.assertTrue(disconnectedLatch.get().await(10, TimeUnit.SECONDS));
        Assert.assertTrue(!client.isConnected());

        // restart server
        connector.setPort(port);
        connectedLatch.set(new CountDownLatch(1));
        server.start();

        // Wait for connect
        Assert.assertTrue(connectedLatch.get().await(10, TimeUnit.SECONDS));
        Assert.assertTrue(client.isConnected());

        disconnectBayeuxClient(client);
    }

    @Test
    public void testHandshakeExpiration() throws Exception
    {
        final long maxNetworkDelay = 2000;

        bayeux.getChannel(Channel.META_HANDSHAKE).addListener(new ServerChannel.MessageListener()
        {
            public boolean onMessage(ServerSession from, ServerChannel channel, ServerMessage.Mutable message)
            {
                try
                {
                    Thread.sleep(maxNetworkDelay + maxNetworkDelay / 2);
                    return true;
                }
                catch (InterruptedException x)
                {
                    return false;
                }
            }
        });

        Map<String,Object> options = new HashMap<>();
        options.put(ClientTransport.MAX_NETWORK_DELAY_OPTION, maxNetworkDelay);
        ClientTransport webSocketTransport = newWebSocketTransport(options);
        final BayeuxClient client = new BayeuxClient(cometdURL, webSocketTransport);

        // Expect 2 failed messages because the client backoffs and retries
        // This way we are sure that the late response from the first
        // expired handshake is not delivered to listeners
        final CountDownLatch latch = new CountDownLatch(2);
        client.getChannel(Channel.META_HANDSHAKE).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                Assert.assertFalse(message.isSuccessful());
                if (!message.isSuccessful())
                    latch.countDown();
            }
        });

        client.handshake();

        Assert.assertTrue(latch.await(maxNetworkDelay * 2 + client.getBackoffIncrement() * 2, TimeUnit.MILLISECONDS));

        disconnectBayeuxClient(client);
    }

    @Test
    public void testMetaConnectNotRespondedOnServerSidePublish() throws Exception
    {
        final BayeuxClient client = newBayeuxClient();

        final String channelName = "/test";
        final AtomicReference<CountDownLatch> publishLatch = new AtomicReference<>(new CountDownLatch(1));
        client.getChannel(Channel.META_HANDSHAKE).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel metaHandshake, Message handshake)
            {
                if (handshake.isSuccessful())
                {
                    client.getChannel(channelName).subscribe(new ClientSessionChannel.MessageListener()
                    {
                        public void onMessage(ClientSessionChannel channel, Message message)
                        {
                            publishLatch.get().countDown();
                        }
                    });
                }
            }
        });
        final AtomicReference<CountDownLatch> connectLatch = new AtomicReference<>(new CountDownLatch(2));
        client.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                connectLatch.get().countDown();
            }
        });
        client.handshake();

        // Wait for the long poll to establish
        Thread.sleep(1000);

        // Test publish triggered by an external event
        final LocalSession emitter = bayeux.newLocalSession("test_emitter");
        emitter.handshake();
        final String data = "test_data";
        bayeux.getChannel(channelName).publish(emitter, data);

        Assert.assertTrue(publishLatch.get().await(5, TimeUnit.SECONDS));
        // Make sure long poll is not responded
        Assert.assertFalse(connectLatch.get().await(1, TimeUnit.SECONDS));

        // Test publish triggered by a message sent by the client
        // There will be a response pending so the case is different
        publishLatch.set(new CountDownLatch(1));
        connectLatch.set(new CountDownLatch(1));
        String serviceChannelName = "/service/test";
        final ServerChannel serviceChannel = bayeux.createChannelIfAbsent(serviceChannelName, new Persistent()).getReference();
        serviceChannel.addListener(new ServerChannel.MessageListener()
        {
            public boolean onMessage(ServerSession from, ServerChannel channel, ServerMessage.Mutable message)
            {
                bayeux.getChannel(channelName).publish(emitter, data);
                return true;
            }
        });
        client.getChannel(serviceChannelName).publish(new HashMap<>());

        Assert.assertTrue(publishLatch.get().await(5, TimeUnit.SECONDS));
        // Make sure long poll is not responded
        Assert.assertFalse(connectLatch.get().await(1, TimeUnit.SECONDS));

        disconnectBayeuxClient(client);
    }

    @Test
    public void testMetaConnectDeliveryOnlyTransport() throws Exception
    {
        stopAndDispose();

        Map<String, String> options = new HashMap<>();
        options.put(AbstractServerTransport.META_CONNECT_DELIVERY_OPTION, "true");
        options.put("ws." + org.cometd.websocket.server.JettyWebSocketTransport.THREAD_POOL_MAX_SIZE, "8");
        prepareAndStart(options);

        final BayeuxClient client = newBayeuxClient();

        final String channelName = "/test";
        final AtomicReference<CountDownLatch> publishLatch = new AtomicReference<>(new CountDownLatch(1));
        client.getChannel(Channel.META_HANDSHAKE).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel metaHandshake, Message handshake)
            {
                if (handshake.isSuccessful())
                {
                    client.getChannel(channelName).subscribe(new ClientSessionChannel.MessageListener()
                    {
                        public void onMessage(ClientSessionChannel channel, Message message)
                        {
                            publishLatch.get().countDown();
                        }
                    });
                }
            }
        });
        final AtomicReference<CountDownLatch> connectLatch = new AtomicReference<>(new CountDownLatch(2));
        client.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                connectLatch.get().countDown();
            }
        });
        client.handshake();

        // Wait for the long poll to establish
        Thread.sleep(1000);

        // Test publish triggered by an external event
        final LocalSession emitter = bayeux.newLocalSession("test_emitter");
        emitter.handshake();
        final String data = "test_data";
        bayeux.getChannel(channelName).publish(emitter, data);

        Assert.assertTrue(publishLatch.get().await(5, TimeUnit.SECONDS));
        // Make sure long poll is responded
        Assert.assertTrue(connectLatch.get().await(5, TimeUnit.SECONDS));

        // Test publish triggered by a message sent by the client
        // There will be a response pending so the case is different
        // from the server-side publish
        publishLatch.set(new CountDownLatch(1));
        connectLatch.set(new CountDownLatch(1));
        String serviceChannelName = "/service/test";
        ServerChannel serviceChannel = bayeux.createChannelIfAbsent(serviceChannelName, new Persistent()).getReference();
        serviceChannel.addListener(new ServerChannel.MessageListener()
        {
            public boolean onMessage(ServerSession from, ServerChannel channel, ServerMessage.Mutable message)
            {
                bayeux.getChannel(channelName).publish(emitter, data);
                return true;
            }
        });
        client.getChannel(serviceChannelName).publish(new HashMap<>());

        Assert.assertTrue(publishLatch.get().await(5, TimeUnit.SECONDS));
        // Make sure long poll is responded
        Assert.assertTrue(connectLatch.get().await(5, TimeUnit.SECONDS));

        disconnectBayeuxClient(client);
    }

    @Test
    public void testMetaConnectDeliveryOnlySession() throws Exception
    {
        bayeux.addExtension(new BayeuxServer.Extension.Adapter()
        {
            @Override
            public boolean sendMeta(ServerSession to, ServerMessage.Mutable message)
            {
                if (Channel.META_HANDSHAKE.equals(message.getChannel()))
                {
                    if (to != null && !to.isLocalSession())
                        ((ServerSessionImpl)to).setMetaConnectDeliveryOnly(true);
                }
                return true;
            }
        });

        final BayeuxClient client = newBayeuxClient();

        final String channelName = "/test";
        final AtomicReference<CountDownLatch> publishLatch = new AtomicReference<>(new CountDownLatch(1));
        client.getChannel(Channel.META_HANDSHAKE).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel metaHandshake, Message handshake)
            {
                if (handshake.isSuccessful())
                {
                    client.getChannel(channelName).subscribe(new ClientSessionChannel.MessageListener()
                    {
                        public void onMessage(ClientSessionChannel channel, Message message)
                        {
                            publishLatch.get().countDown();
                        }
                    });
                }
            }
        });
        final AtomicReference<CountDownLatch> connectLatch = new AtomicReference<>(new CountDownLatch(2));
        client.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                connectLatch.get().countDown();
            }
        });
        client.handshake();

        // Wait for the long poll to establish
        Thread.sleep(1000);

        // Test publish triggered by an external event
        final LocalSession emitter = bayeux.newLocalSession("test_emitter");
        emitter.handshake();
        final String data = "test_data";
        bayeux.getChannel(channelName).publish(emitter, data);

        Assert.assertTrue(publishLatch.get().await(5, TimeUnit.SECONDS));
        // Make sure long poll is responded
        Assert.assertTrue(connectLatch.get().await(5, TimeUnit.SECONDS));

        // Test publish triggered by a message sent by the client
        // There will be a response pending so the case is different
        publishLatch.set(new CountDownLatch(1));
        connectLatch.set(new CountDownLatch(1));
        String serviceChannelName = "/service/test";
        final ServerChannel serviceChannel = bayeux.createChannelIfAbsent(serviceChannelName, new Persistent()).getReference();
        serviceChannel.addListener(new ServerChannel.MessageListener()
        {
            public boolean onMessage(ServerSession from, ServerChannel channel, ServerMessage.Mutable message)
            {
                bayeux.getChannel(channelName).publish(emitter, data);
                return true;
            }
        });
        client.getChannel(serviceChannelName).publish(new HashMap<>());

        Assert.assertTrue(publishLatch.get().await(5, TimeUnit.SECONDS));
        // Make sure long poll is responded
        Assert.assertTrue(connectLatch.get().await(5, TimeUnit.SECONDS));

        disconnectBayeuxClient(client);
    }

    @Test
    public void testMetaConnectExpires() throws Exception
    {
        stopAndDispose();
        long timeout = 2000;
        Map<String, String> options = new HashMap<>();
        options.put(AbstractServerTransport.TIMEOUT_OPTION, String.valueOf(timeout));
        prepareAndStart(options);

        final BayeuxClient client = newBayeuxClient();
        final CountDownLatch connectLatch = new CountDownLatch(2);
        client.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                connectLatch.countDown();
                if (connectLatch.getCount() == 0)
                    client.disconnect();
            }
        });
        final CountDownLatch disconnectLatch = new CountDownLatch(1);
        client.getChannel(Channel.META_DISCONNECT).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                disconnectLatch.countDown();
            }
        });
        client.handshake();

        Assert.assertTrue(connectLatch.await(timeout + timeout / 2, TimeUnit.MILLISECONDS));
        Assert.assertTrue(disconnectLatch.await(5, TimeUnit.SECONDS));
    }

    @Test
    public void testWebSocketWithAckExtension() throws Exception
    {
        final BayeuxClient client = newBayeuxClient();

        bayeux.addExtension(new AcknowledgedMessagesExtension());
        client.addExtension(new AckExtension());

        final String channelName = "/chat/demo";
        final BlockingQueue<Message> messages = new BlockingArrayQueue<>();
        client.getChannel(Channel.META_HANDSHAKE).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                if (message.isSuccessful())
                {
                    client.getChannel(channelName).subscribe(new ClientSessionChannel.MessageListener()
                    {
                        public void onMessage(ClientSessionChannel channel, Message message)
                        {
                            messages.add(message);
                        }
                    });
                }
            }
        });
        final CountDownLatch subscribed = new CountDownLatch(1);
        client.getChannel(Channel.META_SUBSCRIBE).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                if (message.isSuccessful() && channelName.equals(message.get(Message.SUBSCRIPTION_FIELD)))
                    subscribed.countDown();
            }
        });
        client.handshake();

        Assert.assertTrue(subscribed.await(5, TimeUnit.SECONDS));
        Assert.assertEquals(0, messages.size());

        final ServerChannel chatChannel = bayeux.getChannel(channelName);
        Assert.assertNotNull(chatChannel);

        final int count = 5;
        client.batch(new Runnable()
        {
            public void run()
            {
                for (int i = 0; i < count; ++i)
                    client.getChannel(channelName).publish("hello_" + i);
            }
        });

        for (int i = 0; i < count; ++i)
            Assert.assertEquals("hello_" + i, messages.poll(5, TimeUnit.SECONDS).getData());

        // Give time to the /meta/connect to tell the server what is the current ack number
        Thread.sleep(1000);

        int port = connector.getLocalPort();
        connector.stop();
        Thread.sleep(1000);
        Assert.assertTrue(connector.isStopped());
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.UNCONNECTED));

        // Send messages while client is offline
        for (int i = count; i < 2 * count; ++i)
            chatChannel.publish(null, "hello_" + i);

        Thread.sleep(1000);
        Assert.assertEquals(0, messages.size());

        connector.setPort(port);
        connector.start();
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));

        // Check that the offline messages are received
        for (int i = count; i < 2 * count; ++i)
            Assert.assertEquals("hello_" + i, messages.poll(5, TimeUnit.SECONDS).getData());

        // Send messages while client is online
        client.batch(new Runnable()
        {
            public void run()
            {
                for (int i = 2 * count; i < 3 * count; ++i)
                    client.getChannel(channelName).publish("hello_" + i);
            }
        });

        // Check if messages after reconnect are received
        for (int i = 2 * count; i < 3 * count; ++i)
            Assert.assertEquals("hello_" + i, messages.poll(5, TimeUnit.SECONDS).getData());

        disconnectBayeuxClient(client);
    }

    @Test
    public void testMetaConnectDelayedOnServerRespondedBeforeRetry() throws Exception
    {
        final long maxNetworkDelay = 2000;
        final long backoffIncrement = 2000;
        testMetaConnectDelayedOnServer(maxNetworkDelay, backoffIncrement, maxNetworkDelay + backoffIncrement / 2);
    }

    @Test
    public void testMetaConnectDelayedOnServerRespondedAfterRetry() throws Exception
    {
        final long maxNetworkDelay = 2000;
        final long backoffIncrement = 1000;
        testMetaConnectDelayedOnServer(maxNetworkDelay, backoffIncrement, maxNetworkDelay + backoffIncrement * 2);
    }

    private void testMetaConnectDelayedOnServer(final long maxNetworkDelay, final long backoffIncrement, final long delay) throws Exception
    {
        stopAndDispose();

        Map<String, String> initParams = new HashMap<>();
        long timeout = 5000;
        initParams.put("timeout", String.valueOf(timeout));
        switch (wsTransportType)
        {
            case WEBSOCKET_JSR_356:
                initParams.put("transports", CloseLatchWebSocketTransport.class.getName() + "," + JSONTransport.class.getName());
                break;
            case WEBSOCKET_JETTY:
                initParams.put("transports", CloseLatchJettyWebSocketTransport.class.getName() + "," + JSONTransport.class.getName());
                break;
            default:
                throw new IllegalArgumentException();
        }
        prepareAndStart(initParams);

        Map<String, Object> options = new HashMap<>();
        options.put("ws.maxNetworkDelay", maxNetworkDelay);
        ClientTransport webSocketTransport = newWebSocketTransport(options);
        BayeuxClient client = new BayeuxClient(cometdURL, webSocketTransport);
        client.setOption(BayeuxClient.BACKOFF_INCREMENT_OPTION, backoffIncrement);

        bayeux.getChannel(Channel.META_CONNECT).addListener(new ServerChannel.MessageListener()
        {
            private final AtomicInteger connects = new AtomicInteger();

            public boolean onMessage(ServerSession from, ServerChannel channel, ServerMessage.Mutable message)
            {
                int connects = this.connects.incrementAndGet();
                if (connects == 2)
                {
                    try
                    {
                        // We delay the second connect, so the client can expire it
                        Thread.sleep(delay);
                    }
                    catch (InterruptedException x)
                    {
                        x.printStackTrace();
                        return false;
                    }
                }
                return true;
            }
        });

        // The second connect must fail, and should never be notified on client
        final CountDownLatch connectLatch1 = new CountDownLatch(2);
        final CountDownLatch connectLatch2 = new CountDownLatch(1);
        client.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
        {
            private final AtomicInteger connects = new AtomicInteger();
            public String failedId;

            public void onMessage(ClientSessionChannel channel, Message message)
            {
                int connects = this.connects.incrementAndGet();
                if (connects == 1 && message.isSuccessful())
                {
                    connectLatch1.countDown();
                }
                else if (connects == 2 && !message.isSuccessful())
                {
                    connectLatch1.countDown();
                    failedId = message.getId();
                }
                else if (connects > 2 && !failedId.equals(message.getId()))
                {
                    connectLatch2.countDown();
                }
            }
        });

        client.handshake();

        Assert.assertTrue(connectLatch1.await(timeout + 2 * maxNetworkDelay, TimeUnit.MILLISECONDS));
        Assert.assertTrue(connectLatch2.await(client.getBackoffIncrement() * 2, TimeUnit.MILLISECONDS));

        disconnectBayeuxClient(client);
    }

    @Test
    public void testClientSendsAndReceivesBigMessage() throws Exception
    {
        stopAndDispose();

        int maxMessageSize = 128 * 1024;
        Map<String, String> serverOptions = new HashMap<>();
        serverOptions.put("ws.maxMessageSize", String.valueOf(maxMessageSize));
        prepareAndStart(serverOptions);

        Map<String, Object> clientOptions = new HashMap<>();
        clientOptions.put("ws.maxMessageSize", maxMessageSize);
        ClientTransport webSocketTransport = newWebSocketTransport(clientOptions);
        BayeuxClient client = new BayeuxClient(cometdURL, webSocketTransport);

        client.handshake();
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));

        ClientSessionChannel channel = client.getChannel("/test");
        final CountDownLatch latch = new CountDownLatch(1);
        channel.subscribe(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                latch.countDown();
            }
        });

        char[] data = new char[maxMessageSize * 3 / 4];
        Arrays.fill(data, 'x');
        channel.publish(new String(data));

        Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));

        disconnectBayeuxClient(client);
    }

    @Test
    public void testClientDisconnectingClosesTheConnection() throws Exception
    {
        stopAndDispose();

        Map<String, String> initParams = new HashMap<>();
        switch (wsTransportType)
        {
            case WEBSOCKET_JSR_356:
                initParams.put("transports", CloseLatchWebSocketTransport.class.getName() + "," + JSONTransport.class.getName());
                break;
            case WEBSOCKET_JETTY:
                initParams.put("transports", CloseLatchJettyWebSocketTransport.class.getName() + "," + JSONTransport.class.getName());
                break;
            default:
                throw new IllegalArgumentException();
        }
        prepareAndStart(initParams);

        BayeuxClient client = newBayeuxClient();
        client.handshake();
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));
        Thread.sleep(1000);

        client.disconnect();

        switch (wsTransportType)
        {
            case WEBSOCKET_JSR_356:
                CloseLatchWebSocketTransport jsrTransport = (CloseLatchWebSocketTransport)bayeux.getTransport("websocket");
                Assert.assertTrue(jsrTransport.latch.await(5, TimeUnit.SECONDS));
                break;
            case WEBSOCKET_JETTY:
                CloseLatchJettyWebSocketTransport jettyTransport = (CloseLatchJettyWebSocketTransport)bayeux.getTransport("websocket");
                Assert.assertTrue(jettyTransport.latch.await(5, TimeUnit.SECONDS));
                break;
            default:
                throw new IllegalArgumentException();
        }
    }

    @Test
    public void testClientDisconnectingSynchronouslyClosesTheConnection() throws Exception
    {
        stopAndDispose();

        Map<String, String> initParams = new HashMap<>();
        switch (wsTransportType)
        {
            case WEBSOCKET_JSR_356:
                initParams.put("transports", CloseLatchWebSocketTransport.class.getName() + "," + JSONTransport.class.getName());
                break;
            case WEBSOCKET_JETTY:
                initParams.put("transports", CloseLatchJettyWebSocketTransport.class.getName() + "," + JSONTransport.class.getName());
                break;
            default:
                throw new IllegalArgumentException();
        }
        prepareAndStart(initParams);

        BayeuxClient client = newBayeuxClient();
        client.handshake();
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));
        Thread.sleep(1000);

        client.disconnect(1000);

        switch (wsTransportType)
        {
            case WEBSOCKET_JSR_356:
                CloseLatchWebSocketTransport jsrTransport = (CloseLatchWebSocketTransport)bayeux.getTransport("websocket");
                Assert.assertTrue(jsrTransport.latch.await(5, TimeUnit.SECONDS));
                break;
            case WEBSOCKET_JETTY:
                CloseLatchJettyWebSocketTransport jettyTransport = (CloseLatchJettyWebSocketTransport)bayeux.getTransport("websocket");
                Assert.assertTrue(jettyTransport.latch.await(5, TimeUnit.SECONDS));
                break;
            default:
                throw new IllegalArgumentException();
        }
    }

    public static class CloseLatchWebSocketTransport extends WebSocketTransport
    {
        private final CountDownLatch latch = new CountDownLatch(1);

        public CloseLatchWebSocketTransport(BayeuxServerImpl bayeux)
        {
            super(bayeux);
        }

        @Override
        protected void onClose(int code, String reason)
        {
            latch.countDown();
        }
    }

    public static class CloseLatchJettyWebSocketTransport extends org.cometd.websocket.server.JettyWebSocketTransport
    {
        private final CountDownLatch latch = new CountDownLatch(1);

        public CloseLatchJettyWebSocketTransport(BayeuxServerImpl bayeux)
        {
            super(bayeux);
        }

        @Override
        protected void onClose(int code, String reason)
        {
            latch.countDown();
        }
    }

    @Test
    public void testWhenClientAbortsServerSessionIsSwept() throws Exception
    {
        stopAndDispose();

        Map<String, String> options = new HashMap<>();
        long maxInterval = 1000;
        options.put(AbstractServerTransport.MAX_INTERVAL_OPTION, String.valueOf(maxInterval));
        prepareAndStart(options);

        BayeuxClient client = newBayeuxClient();
        client.handshake();
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));

        // Allow long poll to establish
        Thread.sleep(1000);

        final CountDownLatch latch = new CountDownLatch(1);
        ServerSession session = bayeux.getSession(client.getId());
        session.addListener(new ServerSession.RemoveListener()
        {
            @Override
            public void removed(ServerSession session, boolean timeout)
            {
                latch.countDown();
            }
        });

        client.abort();

        Assert.assertTrue(latch.await(2 * maxInterval, TimeUnit.MILLISECONDS));
    }

    @Test
    public void testDisconnectWithPendingMetaConnectWithoutResponseIsFailedOnClient() throws Exception
    {
        stopAndDispose();

        final long timeout = 2000L;
        Map<String, String> serverOptions = new HashMap<>();
        serverOptions.put("timeout", String.valueOf(timeout));
        prepareAndStart(serverOptions);

        bayeux.addExtension(new BayeuxServer.Extension.Adapter()
        {
            @Override
            public boolean sendMeta(ServerSession to, ServerMessage.Mutable message)
            {
                if (Channel.META_CONNECT.equals(message.getChannel()))
                {
                    Map<String, Object> advice = message.getAdvice();
                    if (advice != null)
                    {
                        if (Message.RECONNECT_NONE_VALUE.equals(advice.get(Message.RECONNECT_FIELD)))
                        {
                            // Pretend this message could not be sent
                            return false;
                        }
                    }
                }
                return true;
            }
        });

        final long maxNetworkDelay = 2000L;
        Map<String, Object> clientOptions = new HashMap<>();
        clientOptions.put("maxNetworkDelay", maxNetworkDelay);
        ClientTransport webSocketTransport = newWebSocketTransport(clientOptions);
        BayeuxClient client = new BayeuxClient(cometdURL, webSocketTransport);

        client.handshake();
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));

        // Wait for the /meta/connect to be held on server
        TimeUnit.MILLISECONDS.sleep(1000);

        final CountDownLatch latch = new CountDownLatch(1);
        client.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                if (!message.isSuccessful())
                    latch.countDown();
            }
        });

        client.disconnect();

        // Only wait for the maxNetworkDelay: when the /meta/disconnect response arrives,
        // the connection is closed, so the /meta/connect is failed on the client side.
        Assert.assertTrue(latch.await(2 * maxNetworkDelay, TimeUnit.MILLISECONDS));
    }

    @Test
    public void testDeliverDuringHandshakeProcessing() throws Exception
    {
        final String channelName = "/service/test";
        BayeuxClient client = newBayeuxClient();

        final CountDownLatch latch = new CountDownLatch(1);
        client.getChannel(channelName).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                if (!message.isPublishReply())
                    latch.countDown();
            }
        });

        // SessionListener is the first listener notified after the ServerSession is created.
        bayeux.addListener(new BayeuxServer.SessionListener()
        {
            public void sessionAdded(ServerSession session, ServerMessage message)
            {
                session.deliver(null, channelName, "data");
            }

            public void sessionRemoved(ServerSession session, boolean timedout)
            {
            }
        });

        client.handshake();
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));

        Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));

        disconnectBayeuxClient(client);
    }

    @Test
    public void testDeliverDuringHandshakeProcessingWithAckExtension() throws Exception
    {
        bayeux.addExtension(new AcknowledgedMessagesExtension());

        final String channelName = "/service/test";
        BayeuxClient client = newBayeuxClient();
        client.addExtension(new AckExtension());

        final CountDownLatch latch = new CountDownLatch(1);
        client.getChannel(channelName).addListener(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                if (!message.isPublishReply())
                    latch.countDown();
            }
        });

        // SessionListener is the first listener notified after the ServerSession is created.
        bayeux.addListener(new BayeuxServer.SessionListener()
        {
            public void sessionAdded(ServerSession session, ServerMessage message)
            {
                session.deliver(null, channelName, "data");
            }

            public void sessionRemoved(ServerSession session, boolean timedout)
            {
            }
        });

        client.handshake();
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));

        Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));

        disconnectBayeuxClient(client);
    }

    @Test
    public void testExtensionIsInvokedAfterNetworkFailure() throws Exception
    {
        bayeux.addExtension(new AcknowledgedMessagesExtension());

        final BayeuxClient client = newBayeuxClient();
        final String channelName = "/test";
        final AtomicReference<CountDownLatch> rcv = new AtomicReference<CountDownLatch>(new CountDownLatch(1));
        client.addExtension(new AckExtension());
        client.addExtension(new ClientSession.Extension.Adapter()
        {
            @Override
            public boolean rcv(ClientSession session, Message.Mutable message)
            {
                if (channelName.equals(message.getChannel()))
                    rcv.get().countDown();
                return true;
            }

            @Override
            public boolean rcvMeta(ClientSession session, Message.Mutable message)
            {
                return true;
            }
        });
        client.handshake(new ClientSessionChannel.MessageListener()
        {
            public void onMessage(ClientSessionChannel channel, Message message)
            {
                client.getChannel(channelName).subscribe(new ClientSessionChannel.MessageListener()
                {
                    public void onMessage(ClientSessionChannel channel, Message message)
                    {
                    }
                });
            }
        });
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));

        // This message will be delivered via /meta/connect.
        bayeux.createChannelIfAbsent(channelName).getReference().publish(null, "data1");
        Assert.assertTrue(rcv.get().await(5, TimeUnit.SECONDS));
        // Wait for the /meta/connect to be established again.
        Thread.sleep(1000);

        // Stopping HttpClient will also stop the WebSocketClients (both Jetty's and JSR's).
        httpClient.stop();
        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.UNCONNECTED));

        // Send a message while disconnected.
        bayeux.createChannelIfAbsent(channelName).getReference().publish(null, "data2");

        rcv.set(new CountDownLatch(1));
        httpClient.start();

        Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED));
        Assert.assertTrue(rcv.get().await(5, TimeUnit.SECONDS));

        disconnectBayeuxClient(client);
    }
}
TOP

Related Classes of org.cometd.websocket.client.BayeuxClientWebSocketTest

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.