/**
* 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);
}
}