/*
* 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.client;
import java.net.HttpCookie;
import java.net.URI;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.cometd.bayeux.Channel;
import org.cometd.bayeux.Message;
import org.cometd.bayeux.client.ClientSessionChannel;
import org.cometd.common.JSONContext;
import org.cometd.common.JettyJSONContextClient;
import org.cometd.server.transport.AbstractHttpTransport;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Response;
import org.eclipse.jetty.client.api.Result;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
/**
* Simulates a browser opening multiple tabs to the same Bayeux server
*/
public class MultipleClientSessionsTest extends ClientServerTest
{
private final long timeout = 7000L;
@Before
public void init() throws Exception
{
Map<String, String> params = new HashMap<>();
params.put("timeout", String.valueOf(timeout));
startServer(params);
}
@Test
public void testMultipleClientSession_WithOneMaxSessionPerBrowser_WithNoMultiSessionInterval() throws Exception
{
AbstractHttpTransport transport = (AbstractHttpTransport)bayeux.getTransport("long-polling");
transport.setOption(AbstractHttpTransport.MAX_SESSIONS_PER_BROWSER_OPTION, 1);
transport.setOption(AbstractHttpTransport.MULTI_SESSION_INTERVAL_OPTION, 0);
// Force re-initialization
transport.init();
BayeuxClient client1 = newBayeuxClient();
final ConcurrentLinkedQueue<Message> connects1 = new ConcurrentLinkedQueue<>();
final CountDownLatch latch1 = new CountDownLatch(2);
client1.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
{
public void onMessage(ClientSessionChannel channel, Message message)
{
connects1.offer(message);
latch1.countDown();
}
});
client1.handshake();
assertTrue(client1.waitFor(5000, BayeuxClient.State.CONNECTED));
HttpCookie cookie = client1.getCookie("BAYEUX_BROWSER");
assertNotNull(cookie);
// Give some time to the first client to establish the long poll before the second client
Thread.sleep(1000);
BayeuxClient client2 = newBayeuxClient();
final ConcurrentLinkedQueue<Message> connects2 = new ConcurrentLinkedQueue<>();
final CountDownLatch latch2 = new CountDownLatch(1);
client2.putCookie(cookie);
client2.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
{
public void onMessage(ClientSessionChannel channel, Message message)
{
connects2.offer(message);
latch2.countDown();
}
});
client2.handshake();
assertTrue(latch2.await(5, TimeUnit.SECONDS));
assertEquals(1, connects2.size());
Message connect2 = connects2.peek();
Map<String, Object> advice2 = connect2.getAdvice();
assertEquals(Message.RECONNECT_NONE_VALUE, advice2.get(Message.RECONNECT_FIELD));
assertSame(Boolean.TRUE, advice2.get("multiple-clients"));
assertFalse(connect2.isSuccessful());
// Give some time to the second client to process the disconnect
Thread.sleep(1000);
assertFalse(client2.isConnected());
assertTrue(latch1.await(timeout, TimeUnit.MILLISECONDS));
assertEquals(2, connects1.size());
assertTrue(client1.isConnected());
disconnectBayeuxClient(client1);
}
@Test
public void testMultipleClientSession_WithOneMaxSessionPerBrowser_WithMultiSessionInterval() throws Exception
{
long multiSessionInterval = 1500;
AbstractHttpTransport transport = (AbstractHttpTransport)bayeux.getTransport("long-polling");
transport.setOption(AbstractHttpTransport.MAX_SESSIONS_PER_BROWSER_OPTION, 1);
transport.setOption(AbstractHttpTransport.MULTI_SESSION_INTERVAL_OPTION, multiSessionInterval);
// Force re-initialization
transport.init();
BayeuxClient client1 = newBayeuxClient();
final ConcurrentLinkedQueue<Message> connects1 = new ConcurrentLinkedQueue<>();
client1.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
{
public void onMessage(ClientSessionChannel channel, Message message)
{
if (message.isSuccessful())
connects1.offer(message);
}
});
client1.handshake();
assertTrue(client1.waitFor(5000, BayeuxClient.State.CONNECTED));
HttpCookie cookie = client1.getCookie("BAYEUX_BROWSER");
assertNotNull(cookie);
// Give some time to the first client to establish the long poll before the second client
Thread.sleep(1000);
BayeuxClient client2 = newBayeuxClient();
final ConcurrentLinkedQueue<Message> connects2 = new ConcurrentLinkedQueue<>();
client2.putCookie(cookie);
client2.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
{
public void onMessage(ClientSessionChannel channel, Message message)
{
connects2.offer(message);
}
});
client2.handshake();
assertTrue(client2.waitFor(5000, BayeuxClient.State.CONNECTED));
Thread.sleep(1000);
BayeuxClient client3 = newBayeuxClient();
final ConcurrentLinkedQueue<Message> connects3 = new ConcurrentLinkedQueue<>();
client3.putCookie(cookie);
client3.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
{
public void onMessage(ClientSessionChannel channel, Message message)
{
connects3.offer(message);
}
});
client3.handshake();
assertTrue(client3.waitFor(5000, BayeuxClient.State.CONNECTED));
// Sleep for a while
Thread.sleep(2 * multiSessionInterval);
// The first client must remain in long poll mode
assertEquals(1, connects1.size());
// Second client must be in normal poll mode
assertTrue(connects2.size() > 1);
Message lastConnect2 = new LinkedList<>(connects2).getLast();
Map<String,Object> advice2 = lastConnect2.getAdvice();
assertNotNull(advice2);
assertSame(Boolean.TRUE, advice2.get("multiple-clients"));
// Third client must be in normal poll mode
assertTrue(connects3.size() > 1);
Message lastConnect3 = new LinkedList<>(connects3).getLast();
Map<String,Object> advice3 = lastConnect3.getAdvice();
assertNotNull(advice3);
assertSame(Boolean.TRUE, advice3.get("multiple-clients"));
// Wait for the first client to re-issue a long poll
Thread.sleep(timeout);
// First client must still be in long poll mode
assertEquals(2, connects1.size());
// Abort abruptly the first client
// Another client must switch to long poll
client1.abort();
// Sleep another timeout to be sure client1 does not poll
Thread.sleep(timeout);
assertEquals(2, connects1.size());
// Loop until one of the other clients switched to long poll
BayeuxClient client4 = null;
BayeuxClient client5 = null;
for (int i = 0; i < 10; ++i)
{
lastConnect2 = new LinkedList<>(connects2).getLast();
advice2 = lastConnect2.getAdvice();
if (advice2 == null || !advice2.containsKey("multiple-clients"))
{
client4 = client2;
client5 = client3;
break;
}
lastConnect3 = new LinkedList<>(connects3).getLast();
advice3 = lastConnect3.getAdvice();
if (advice3 == null || !advice3.containsKey("multiple-clients"))
{
client4 = client3;
client5 = client2;
break;
}
Thread.sleep(timeout / 10);
}
assertNotNull(client4);
// Disconnect this client normally, the last client must switch to long poll
disconnectBayeuxClient(client4);
// Be sure the last client had the time to switch to long poll mode
Thread.sleep(timeout + 2 * multiSessionInterval);
Message lastConnect;
if (client5 == client2)
lastConnect = new LinkedList<>(connects2).getLast();
else
lastConnect = new LinkedList<>(connects3).getLast();
Map<String, Object> advice = lastConnect.getAdvice();
assertTrue(advice == null || !advice.containsKey("multiple-clients"));
disconnectBayeuxClient(client5);
}
@Test
public void testMultipleClientSession_WithTwoMaxSessionPerBrowser_WithMultiSessionInterval() throws Exception
{
long multiSessionInterval = 1500;
AbstractHttpTransport transport = (AbstractHttpTransport)bayeux.getTransport("long-polling");
transport.setOption(AbstractHttpTransport.MAX_SESSIONS_PER_BROWSER_OPTION, 2);
transport.setOption(AbstractHttpTransport.MULTI_SESSION_INTERVAL_OPTION, multiSessionInterval);
// Force re-initialization
transport.init();
BayeuxClient client1 = newBayeuxClient();
final ConcurrentLinkedQueue<Message> connects1 = new ConcurrentLinkedQueue<>();
client1.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
{
public void onMessage(ClientSessionChannel channel, Message message)
{
if (message.isSuccessful())
connects1.offer(message);
}
});
client1.handshake();
assertTrue(client1.waitFor(5000, BayeuxClient.State.CONNECTED));
HttpCookie cookie = client1.getCookie("BAYEUX_BROWSER");
assertNotNull(cookie);
// Give some time to the first client to establish the long poll before the second client
Thread.sleep(1000);
BayeuxClient client2 = newBayeuxClient();
final ConcurrentLinkedQueue<Message> connects2 = new ConcurrentLinkedQueue<>();
client2.putCookie(cookie);
client2.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
{
public void onMessage(ClientSessionChannel channel, Message message)
{
connects2.offer(message);
}
});
client2.handshake();
assertTrue(client2.waitFor(5000, BayeuxClient.State.CONNECTED));
Thread.sleep(1000);
BayeuxClient client3 = newBayeuxClient();
final ConcurrentLinkedQueue<Message> connects3 = new ConcurrentLinkedQueue<>();
client3.putCookie(cookie);
client3.getChannel(Channel.META_CONNECT).addListener(new ClientSessionChannel.MessageListener()
{
public void onMessage(ClientSessionChannel channel, Message message)
{
connects3.offer(message);
}
});
client3.handshake();
assertTrue(client3.waitFor(5000, BayeuxClient.State.CONNECTED));
// Sleep for a while
Thread.sleep(2 * multiSessionInterval);
// The first client must remain in long poll mode
assertEquals(1, connects1.size());
// Second client must remain in long poll mode
assertEquals(1, connects2.size());
// Third client must be in normal poll mode
assertTrue(connects3.size() > 1);
Message lastConnect3 = new LinkedList<>(connects3).getLast();
Map<String,Object> advice3 = lastConnect3.getAdvice();
assertNotNull(advice3);
assertSame(Boolean.TRUE, advice3.get("multiple-clients"));
// Wait for the first and second clients to re-issue a long poll
Thread.sleep(timeout);
// First and second clients must still be in long poll mode
assertEquals(2, connects1.size());
assertEquals(2, connects2.size());
// Abort abruptly the first client
// Third client must switch to long poll
client1.abort();
// Sleep another timeout to be sure client1 does not poll
Thread.sleep(timeout);
assertEquals(2, connects1.size());
// Loop until client3 switched to long poll
for (int i = 0; i < 10; ++i)
{
lastConnect3 = new LinkedList<>(connects3).getLast();
advice3 = lastConnect3.getAdvice();
if (advice3 == null || !advice3.containsKey("multiple-clients"))
break;
Thread.sleep(timeout / 10);
}
lastConnect3 = new LinkedList<>(connects3).getLast();
advice3 = lastConnect3.getAdvice();
assertTrue(advice3 == null || !advice3.containsKey("multiple-clients"));
disconnectBayeuxClient(client2);
disconnectBayeuxClient(client3);
}
@Test
public void testMultipleClientSession_WhenSameClientSendsTwoConnects() throws Exception
{
long multiSessionInterval = 1500;
AbstractHttpTransport transport = (AbstractHttpTransport)bayeux.getTransport("long-polling");
transport.setOption(AbstractHttpTransport.MAX_SESSIONS_PER_BROWSER_OPTION, 1);
transport.setOption(AbstractHttpTransport.MULTI_SESSION_INTERVAL_OPTION, multiSessionInterval);
// Force re-initialization
transport.init();
JSONContext.Client parser = new JettyJSONContextClient();
String handshakeContent = "[{" +
"\"id\":\"1\"," +
"\"channel\":\"/meta/handshake\"," +
"\"version\":\"1.0\"," +
"\"supportedConnectionTypes\":[\"long-polling\"]" +
"}]";
ContentResponse handshake = httpClient.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.path(cometdServletPath)
.content(new StringContentProvider(handshakeContent), "application/json;charset=UTF-8")
.timeout(5, TimeUnit.SECONDS)
.send();
assertEquals(200, handshake.getStatus());
HttpCookie browserCookie = httpClient.getCookieStore().get(URI.create(cometdURL)).get(0);
assertEquals("BAYEUX_BROWSER", browserCookie.getName());
Message.Mutable[] messages = parser.parse(handshake.getContentAsString());
assertEquals(1, messages.length);
String clientId = messages[0].getClientId();
String connectContent1 = "[{" +
"\"id\":\"2\"," +
"\"channel\":\"/meta/connect\"," +
"\"connectionType\":\"long-polling\"," +
"\"clientId\":\"" + clientId + "\"," +
"\"advice\": {\"timeout\":0}" +
"}]";
ContentResponse connect1 = httpClient.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.path(cometdServletPath)
.content(new StringContentProvider(connectContent1), "application/json;charset=UTF-8")
.timeout(5, TimeUnit.SECONDS)
.send();
assertEquals(200, connect1.getStatus());
// This /meta/connect is suspended.
final CountDownLatch abortedConnectLatch = new CountDownLatch(1);
String connectContent2 = "[{" +
"\"id\":\"3\"," +
"\"channel\":\"/meta/connect\"," +
"\"connectionType\":\"long-polling\"," +
"\"clientId\":\"" + clientId + "\"" +
"}]";
httpClient.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.path(cometdServletPath)
.content(new StringContentProvider(connectContent2), "application/json;charset=UTF-8")
.timeout(5, TimeUnit.SECONDS)
.send(new Response.CompleteListener()
{
@Override
public void onComplete(Result result)
{
assertTrue(result.isSucceeded());
assertEquals(408, result.getResponse().getStatus());
abortedConnectLatch.countDown();
}
});
// Give some time to the long poll to happen.
Thread.sleep(1000);
// Send the second /meta/connect before the previous returns.
String connectContent3 = "[{" +
"\"id\":\"4\"," +
"\"channel\":\"/meta/connect\"," +
"\"connectionType\":\"long-polling\"," +
"\"clientId\":\"" + clientId + "\"," +
"\"advice\": {\"timeout\":0}" +
"}]";
ContentResponse connect3 = httpClient.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.path(cometdServletPath)
.content(new StringContentProvider(connectContent3), "application/json;charset=UTF-8")
.timeout(5, TimeUnit.SECONDS)
.send();
assertEquals(200, connect3.getStatus());
assertTrue(abortedConnectLatch.await(5, TimeUnit.SECONDS));
// Make sure a subsequent connect does not have the multiple-clients advice.
String connectContent4 = "[{" +
"\"id\":\"5\"," +
"\"channel\":\"/meta/connect\"," +
"\"connectionType\":\"long-polling\"," +
"\"clientId\":\"" + clientId + "\"" +
"}]";
ContentResponse connect4 = httpClient.newRequest("localhost", connector.getLocalPort())
.method(HttpMethod.POST)
.path(cometdServletPath)
.content(new StringContentProvider(connectContent4), "application/json;charset=UTF-8")
.timeout(2 * timeout, TimeUnit.MILLISECONDS)
.send();
assertEquals(200, connect4.getStatus());
messages = parser.parse(connect4.getContentAsString());
assertEquals(1, messages.length);
Message.Mutable message = messages[0];
Map<String, Object> advice = message.getAdvice(true);
assertFalse(advice.containsKey("multiple-clients"));
}
}