/** * Copyright (c) 2012, Andy Janata * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are permitted * provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this list of conditions * and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, this list of * conditions and the following disclaimer in the documentation and/or other materials provided * with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package net.socialgamer.cah.servlets; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import net.socialgamer.cah.Constants.LongPollEvent; import net.socialgamer.cah.Constants.LongPollResponse; import net.socialgamer.cah.Constants.ReturnableData; import net.socialgamer.cah.Constants.SessionAttribute; import net.socialgamer.cah.data.QueuedMessage; import net.socialgamer.cah.data.User; /** * Servlet implementation class LongPollServlet. * * This servlet is used for client long polling requests. * * @author Andy Janata (ajanata@socialgamer.net) */ @WebServlet("/LongPollServlet") public class LongPollServlet extends CahServlet { private static final long serialVersionUID = 1L; /** * Minimum amount of time before timing out and returning a no-op, in nanoseconds. */ private static final long TIMEOUT_BASE = TimeUnit.SECONDS.toNanos(20); // private static final long TIMEOUT_BASE = 10 * 1000 * 1000; /** * Randomness factor added to minimum timeout duration, in nanoseconds. The maximum timeout delay * will be TIMEOUT_BASE + TIMEOUT_RANDOMNESS - 1. */ private static final double TIMEOUT_RANDOMNESS = TimeUnit.SECONDS.toNanos(5); // private static final double TIMEOUT_RANDOMNESS = 0; /** * The maximum number of messages which will be returned to a client during a single poll * operation. */ private static final int MAX_MESSAGES_PER_POLL = 20; /** * An amount of milliseconds to wait after being notified that the user has at least one message * to deliver, before we actually deliver messages. This will allow multiple messages that arrive * in close proximity to each other to actually be delivered in the same client request. */ private static final int WAIT_FOR_MORE_DELAY = 50; /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) */ @Override protected void handleRequest(final HttpServletRequest request, final HttpServletResponse response, final HttpSession hSession) throws ServletException, IOException { final PrintWriter out = response.getWriter(); final long start = System.nanoTime(); // Pick a random timeout point between [TIMEOUT_BASE, TIMEOUT_BASE + TIMEOUT_RANDOMNESS) // nanoseconds from now. final long end = start + TIMEOUT_BASE + (long) (Math.random() * TIMEOUT_RANDOMNESS); final User user = (User) hSession.getAttribute(SessionAttribute.USER); assert (user != null); user.contactedServer(); while (!(user.hasQueuedMessages()) && System.nanoTime() - end < 0) { try { user.waitForNewMessageNotification(TimeUnit.NANOSECONDS.toMillis(end - System.nanoTime())); } catch (final InterruptedException ie) { // pass } } if (user.hasQueuedMessages()) { try { // Delay for a short while in case there will be other messages queued to be delivered. // This will certainly happen in some game states. We want to deliver as much to the client // in as few round-trips as possible while not waiting too long. Thread.sleep(WAIT_FOR_MORE_DELAY); } catch (final InterruptedException ie) { // pass } final Collection<QueuedMessage> msgs = user.getNextQueuedMessages(MAX_MESSAGES_PER_POLL); // just in case... if (msgs.size() > 0) { final List<Map<ReturnableData, Object>> data = new ArrayList<Map<ReturnableData, Object>>(msgs.size()); for (final QueuedMessage qm : msgs) { data.add(qm.getData()); } returnArray(user, out, data); return; } } // otherwise, return that there is no new data final Map<ReturnableData, Object> data = new HashMap<ReturnableData, Object>(); data.put(LongPollResponse.EVENT, LongPollEvent.NOOP.toString()); data.put(LongPollResponse.TIMESTAMP, System.currentTimeMillis()); returnData(user, out, data); } }