@Override public boolean sendMeta(ServerSession session, ServerMessage.Mutable message) { if (Channel.META_CONNECT.equals(message.getChannel())) { int connects = this.connects.incrementAndGet(); if (connects == 1) { // Fake the removal of the session due to timeout bayeuxServer.removeServerSession(session, true); } } return true; }
@Override public boolean rcvMeta(ServerSession from, ServerMessage.Mutable message) { if (Channel.META_CONNECT.equals(message.getChannel())) { int connects = this.connects.incrementAndGet(); if (connects == 2) { try { Thread.sleep(delay); } catch (InterruptedException x) { return false; } } } return true; }
@Override public boolean sendMeta(ServerSession to, Mutable message) { if (Channel.META_HANDSHAKE.equals(message.getChannel()) || Channel.META_CONNECT.equals(message.getChannel()) || Channel.META_DISCONNECT.equals(message.getChannel())) { AbstractHttpTransport transport = (AbstractHttpTransport) bayeux.getCurrentTransport(); HttpServletRequest request = transport.getCurrentRequest(); if (request != null) { String uri = request.getRequestURI(); message.getExt(true).put("uri", uri); } } return true; }
@Override public boolean sendMeta(ClientSession session, Mutable message) { if (Channel.META_HANDSHAKE.equals(message.getChannel())) { _serverSupportsAcks = false; _transientBatch = 0; _batch = 0; _size = 0; message.getExt(true).put(ACK_FIELD, Boolean.TRUE); } else if (Channel.META_CONNECT.equals(message.getChannel())) { if (_serverSupportsAcks) { message.getExt(true).put(ACK_FIELD, _batch); } } return true; }
@Override public boolean rcvMeta(ClientSession session, Mutable message) { if (Channel.META_HANDSHAKE.equals(message.getChannel())) { Map<String, Object> ext = message.getExt(false); if (ext != null) { Object field = ext.get(ACK_FIELD); if (field instanceof Map) { // New format. @SuppressWarnings("unchecked") Map<String, Object> ack = (Map<String, Object>) field; _serverSupportsAcks = Boolean.TRUE.equals(ack.get("enabled")); // Check if there are messages. Object batch = ack.get("batch"); Object size = ack.get("size"); if (batch instanceof Number && size instanceof Number) { _transientBatch = ((Number) batch).longValue(); _size = ((Number) size).intValue(); } } else { // Old format. _serverSupportsAcks = Boolean.TRUE.equals(field); } } } else if (Channel.META_CONNECT.equals(message.getChannel()) && message.isSuccessful() && _serverSupportsAcks) { Map<String, Object> ext = message.getExt(false); if (ext != null) { Object ack = ext.get(ACK_FIELD); if (ack instanceof Number) { _batch = ((Number) ack).longValue(); } } } return true; }
@Override public void handle(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // Is this a resumed connect? LongPollScheduler scheduler = (LongPollScheduler) request.getAttribute(LongPollScheduler.ATTRIBUTE); if (scheduler == null) { // No - process messages // Remember if we start a batch boolean batch = false; // Don't know the session until first message or handshake response. ServerSessionImpl session = null; boolean connect = false; try { ServerMessage.Mutable[] messages = parseMessages(request); if (messages == null) return; PrintWriter writer = null; for (ServerMessage.Mutable message : messages) { // Is this a connect? connect = Channel.META_CONNECT.equals(message.getChannel()); // Get the session from the message String client_id = message.getClientId(); if (session == null || client_id != null && !client_id.equals(session.getId())) { session = (ServerSessionImpl) getBayeux().getSession(client_id); if (_autoBatch && !batch && session != null && !connect && !message.isMeta()) { // start a batch to group all resulting messages into a single response. batch = true; session.startBatch(); } } else if (!session.isHandshook()) { batch = false; session = null; } if (connect && session != null) { // cancel previous scheduler to cancel any prior waiting long poll // this should also dec the browser ID session.setScheduler(null); } boolean wasConnected = session != null && session.isConnected(); // Forward handling of the message. // The actual reply is return from the call, but other messages may // also be queued on the session. ServerMessage.Mutable reply = bayeuxServerHandle(session, message); // Do we have a reply ? if (reply != null) { if (session == null) { // This must be a handshake, extract a session from the reply session = (ServerSessionImpl) getBayeux().getSession(reply.getClientId()); // Get the user agent while we are at it, and add the browser ID cookie if (session != null) { String userAgent = request.getHeader("User-Agent"); session.setUserAgent(userAgent); String browserId = findBrowserId(request); if (browserId == null) setBrowserId(request, response); } } else { // Special handling for connect if (connect) { try { writer = sendQueue(request, response, session, writer); // If the writer is non null, we have already started sending a response, so we // should not suspend if (writer == null && reply.isSuccessful() && session.isQueueEmpty()) { // Detect if we have multiple sessions from the same browser // Note that CORS requests do not send cookies, so we need to handle them // specially // CORS requests always have the Origin header String browserId = findBrowserId(request); boolean allowSuspendConnect; if (browserId != null) allowSuspendConnect = incBrowserId(browserId); else allowSuspendConnect = _allowMultiSessionsNoBrowser || request.getHeader("Origin") != null; if (allowSuspendConnect) { long timeout = session.calculateTimeout(getTimeout()); // Support old clients that do not send advice:{timeout:0} on the first // connect if (timeout > 0 && wasConnected && session.isConnected()) { // Suspend and wait for messages Continuation continuation = ContinuationSupport.getContinuation(request); continuation.setTimeout(timeout); continuation.suspend(response); scheduler = new LongPollScheduler(session, continuation, reply, browserId); session.setScheduler(scheduler); request.setAttribute(LongPollScheduler.ATTRIBUTE, scheduler); reply = null; metaConnectSuspended(request, session, timeout); } else { decBrowserId(browserId); } } else { // There are multiple sessions from the same browser Map<String, Object> advice = reply.getAdvice(true); if (browserId != null) advice.put("multiple-clients", true); if (_multiSessionInterval > 0) { advice.put(Message.RECONNECT_FIELD, Message.RECONNECT_RETRY_VALUE); advice.put(Message.INTERVAL_FIELD, _multiSessionInterval); } else { advice.put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE); reply.setSuccessful(false); } session.reAdvise(); } } } finally { if (reply != null && session.isConnected()) session.startIntervalTimeout(getInterval()); } } else { if (!isMetaConnectDeliveryOnly() && !session.isMetaConnectDeliveryOnly()) { writer = sendQueue(request, response, session, writer); } } } // If the reply has not been otherwise handled, send it if (reply != null) { if (connect && session != null && !session.isConnected()) reply.getAdvice(true).put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE); reply = getBayeux().extendReply(session, session, reply); if (reply != null) { getBayeux().freeze(reply); writer = send(request, response, writer, reply); } } } // Disassociate the reply message.setAssociated(null); } if (writer != null) complete(writer); } catch (ParseException x) { handleJSONParseException(request, response, x.getMessage(), x.getCause()); } finally { // If we started a batch, end it now if (batch) { boolean ended = session.endBatch(); // Flush session if not done by the batch, since some browser order <script> requests if (!ended && isAlwaysFlushingAfterHandle()) session.flush(); } else if (session != null && !connect && isAlwaysFlushingAfterHandle()) { session.flush(); } } } else { // Get the resumed session ServerSessionImpl session = scheduler.getSession(); metaConnectResumed(request, session); PrintWriter writer; try { // Send the message queue writer = sendQueue(request, response, session, null); } finally { // We need to start the interval timeout before the connect reply // otherwise we open up a race condition where the client receives // the connect reply and sends a new connect request before we start // the interval timeout, which will be wrong. // We need to put this into a finally block in case sending the queue // throws an exception (for example because the client is gone), so that // we start the interval timeout that is important to sweep the session if (session.isConnected()) session.startIntervalTimeout(getInterval()); } // Send the connect reply ServerMessage.Mutable reply = scheduler.getReply(); if (!session.isConnected()) reply.getAdvice(true).put(Message.RECONNECT_FIELD, Message.RECONNECT_NONE_VALUE); reply = getBayeux().extendReply(session, session, reply); if (reply != null) { getBayeux().freeze(reply); writer = send(request, response, writer, reply); } complete(writer); } }
@Test public void testStateUnSubscribes() throws Exception { final BlockingArrayQueue<Object> results = new BlockingArrayQueue<>(); final AtomicBoolean failHandShake = new AtomicBoolean(true); LongPollingTransport transport = new LongPollingTransport(null, httpClient) { @Override protected void customize(Request request) { if (failHandShake.compareAndSet(true, false)) { request.listener( new Request.Listener.Adapter() { @Override public void onBegin(Request request) { request.header(HttpHeader.HOST, null); } }); } } }; BayeuxClient client = new BayeuxClient(cometdURL, transport); client .getChannel("/meta/*") .addListener( new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) { results.offer(message); } }); client.handshake(); // Subscribe without waiting client .getChannel("/foo/bar") .subscribe( new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) {} }); // First handshake fails Message message = (Message) results.poll(10, TimeUnit.SECONDS); Assert.assertNotNull(message); Assert.assertEquals(Channel.META_HANDSHAKE, message.getChannel()); Assert.assertFalse(message.isSuccessful()); // Second handshake works message = (Message) results.poll(10, TimeUnit.SECONDS); Assert.assertNotNull(message); Assert.assertEquals(Channel.META_HANDSHAKE, message.getChannel()); Assert.assertTrue(message.isSuccessful()); String id = client.getId(); Assert.assertNotNull(id); boolean subscribe = false; boolean connect = false; for (int i = 0; i < 2; ++i) { message = (Message) results.poll(10, TimeUnit.SECONDS); Assert.assertNotNull(message); subscribe |= Channel.META_SUBSCRIBE.equals(message.getChannel()); connect |= Channel.META_CONNECT.equals(message.getChannel()); } Assert.assertTrue(subscribe); Assert.assertTrue(connect); // Subscribe again client .getChannel("/foo/bar") .subscribe( new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) {} }); // No second subscribe sent, be sure to wait less than the timeout // otherwise we get a connect message message = (Message) results.poll(5, TimeUnit.SECONDS); Assert.assertNull(message); client.disconnect(); boolean disconnect = false; connect = false; for (int i = 0; i < 2; ++i) { message = (Message) results.poll(10, TimeUnit.SECONDS); Assert.assertNotNull(message); disconnect |= Channel.META_DISCONNECT.equals(message.getChannel()); connect |= Channel.META_CONNECT.equals(message.getChannel()); } Assert.assertTrue(disconnect); Assert.assertTrue(connect); Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.DISCONNECTED)); // Rehandshake client.handshake(); Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED)); results.clear(); // Subscribe again client .getChannel("/foo/bar") .subscribe( new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) {} }); // Subscribe is sent, skip the connect message if present message = (Message) results.poll(10, TimeUnit.SECONDS); Assert.assertNotNull(message); if (Channel.META_CONNECT.equals(message.getChannel())) message = (Message) results.poll(10, TimeUnit.SECONDS); Assert.assertEquals(Channel.META_SUBSCRIBE, message.getChannel()); // Restart server int port = connector.getLocalPort(); server.stop(); server.join(); Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.UNCONNECTED)); connector.setPort(port); server.start(); bayeux = (BayeuxServerImpl) context.getServletContext().getAttribute(BayeuxServer.ATTRIBUTE); Assert.assertTrue(client.waitFor(5000, BayeuxClient.State.CONNECTED)); results.clear(); // Subscribe again client .getChannel("/foo/bar") .subscribe( new ClientSessionChannel.MessageListener() { public void onMessage(ClientSessionChannel channel, Message message) {} }); // Subscribe is sent, skip the connect message if present message = (Message) results.poll(10, TimeUnit.SECONDS); Assert.assertNotNull(message); if (Channel.META_CONNECT.equals(message.getChannel())) message = (Message) results.poll(10, TimeUnit.SECONDS); Assert.assertEquals(Channel.META_SUBSCRIBE, message.getChannel()); disconnectBayeuxClient(client); }
@Override public void send(final TransportListener listener, final Message.Mutable... messages) { String url = getURL(); final URI uri = URI.create(url); if (_appendMessageType && messages.length == 1 && messages[0].isMeta()) { String type = messages[0].getChannel().substring(Channel.META.length()); if (url.endsWith("/")) url = url.substring(0, url.length() - 1); url += type; } final Request request = _httpClient.newRequest(url).method(HttpMethod.POST); request.header(HttpHeader.CONTENT_TYPE.asString(), "application/json;charset=UTF-8"); StringBuilder builder = new StringBuilder(); for (HttpCookie cookie : getCookieStore().get(uri)) { builder.setLength(0); builder.append(cookie.getName()).append("=").append(cookie.getValue()); request.header(HttpHeader.COOKIE.asString(), builder.toString()); } request.content(new StringContentProvider(generateJSON(messages))); customize(request); synchronized (this) { if (_aborted) throw new IllegalStateException("Aborted"); _requests.add(request); } request.listener( new Request.Listener.Empty() { @Override public void onHeaders(Request request) { listener.onSending(messages); } }); long maxNetworkDelay = _maxNetworkDelay; if (messages.length == 1 && Channel.META_CONNECT.equals(messages[0].getChannel())) { Map<String, Object> advice = messages[0].getAdvice(); if (advice == null) advice = _advice; if (advice != null) { Object timeout = advice.get("timeout"); if (timeout instanceof Number) maxNetworkDelay += ((Number) timeout).longValue(); else if (timeout != null) maxNetworkDelay += Long.parseLong(timeout.toString()); } } // Set the idle timeout for this request larger than the total timeout // so there are no races between the two timeouts request.idleTimeout(maxNetworkDelay * 2, TimeUnit.MILLISECONDS); request.timeout(maxNetworkDelay, TimeUnit.MILLISECONDS); request.send( new BufferingResponseListener() { @Override public boolean onHeader(Response response, HttpField field) { // We do not allow cookies to be handled by HttpClient, since one // HttpClient instance is shared by multiple BayeuxClient instances. // Instead, we store the cookies in the BayeuxClient instance. switch (field.getHeader()) { case SET_COOKIE: case SET_COOKIE2: Map<String, List<String>> cookies = new HashMap<>(1); cookies.put(field.getName(), Collections.singletonList(field.getValue())); storeCookies(uri, cookies); return false; default: return true; } } private void storeCookies(URI uri, Map<String, List<String>> cookies) { try { _cookieManager.put(uri, cookies); } catch (IOException x) { logger.debug("", x); } } @Override public void onComplete(Result result) { synchronized (LongPollingTransport.this) { _requests.remove(result.getRequest()); } if (result.isFailed()) { listener.onFailure(result.getFailure(), messages); return; } Response response = result.getResponse(); int status = response.getStatus(); if (status == HttpStatus.OK_200) { String content = getContentAsString(); if (content != null && content.length() > 0) { try { List<Message.Mutable> messages = parseMessages(content); debug("Received messages {}", messages); for (Message.Mutable message : messages) { if (message.isSuccessful() && Channel.META_CONNECT.equals(message.getChannel())) { Map<String, Object> advice = message.getAdvice(); if (advice != null && advice.get("timeout") != null) _advice = advice; } } listener.onMessages(messages); } catch (ParseException x) { listener.onFailure(x, messages); } } else { Map<String, Object> failure = new HashMap<>(2); // Convert the 200 into 204 (no content) failure.put("httpCode", 204); TransportException x = new TransportException(failure); listener.onFailure(x, messages); } } else { Map<String, Object> failure = new HashMap<>(2); failure.put("httpCode", status); TransportException x = new TransportException(failure); listener.onFailure(x, messages); } } }); }
public boolean sendMeta(ServerSession to, ServerMessage.Mutable message) { if (Channel.META_CONNECT.equals(message.getChannel())) { sendMetas.add(message); } return true; }