Merge pull request #1 from ajanata/master

Repackage timer tasks. Add new timer task to broadcast game list upda…
This commit is contained in:
emckee2006 2015-05-11 11:17:30 -04:00
commit 3aea5d144e
11 changed files with 119 additions and 67 deletions

View File

@ -242,6 +242,7 @@ select {
<th>Delete</th>
<th>Edit</th>
<th>Weight</th>
<th>Active</th>
</tr>
</thead>
<tbody>
@ -253,6 +254,7 @@ select {
<td><a href="?delete=<%=cardSet.getId()%>" onclick="return confirm('Are you sure?')">Delete</a></td>
<td><a href="?edit=<%=cardSet.getId()%>">Edit</a></td>
<td><%=cardSet.getWeight()%></td>
<td><%=cardSet.isActive()%></td>
</tr>
<%
}

View File

@ -86,27 +86,16 @@ HttpSession hSession = request.getSession(true);
If this is your first time playing, you may wish to read <a href="/">the changelog and list of
known issues</a>.
</p>
<p tabindex="0">Most recent update: 21 February 2015:</p>
<p tabindex="0">Most recent update: 3 May 2015:</p>
<ul>
<li>Servers now run in Amazon Web Services. This is going to cost me more, but at least it
should be more stable and not take down my other stuff when it does go down...<ul>
<li>I am still tweaking server settings in AWS. It likely is going to be unstable for another
week or two while I fine-tune cost and performance.</li></ul></li>
<li>Card set filters are fixed.</li>
<li><pre>/removecardcast</pre> is fixed.</li>
<li>Connect and disconnect notices are disabled server-wide. This was a major source of
bandwidth and processing time.</li>
<li><strong>You can start a game without using any local card sets.</strong>You must have at
least 50 black cards and (20 times player limit) white cards to be able to start a game.</li>
<li>Several other back-end performance and code maintainability improvements.</li>
<li><strong>Custom card sets will be removed from local storage in the near future.</strong>You
will have to use Cardcast to use custom card sets. If a card set you want is not already in
Cardcast, you can attempt to extract it from
<a href='https://github.com/ajanata/PretendYoureXyzzy/blob/737b468/cah_cards.sql'>the last
version of the database dump which contains them</a> and add it to Cardcast yourself; I will be
unable to provide help in doing so.</li>
<li>At roughly the same time, all officially released Cards Against Humanity sets which are not
already in the system will be added as local decks.</li>
<li>The game list automatically updates once per minute now, instead of several times per
second. You can still click the Refresh Games button in the top left corner at any time.</li>
<li>Chat flood protection has been made more strict.</li>
<li>Other back-end changes to attempt to get the AWS bill in control.</li>
<li><strong>All locally-stored custom card sets have been removed.</strong> You must use
Cardcast for custom card sets now.</li>
<li>The 5th and 6th Expansions, PAX Prime 2014 Panel, Ten Days or Whatever of Kwanzaa,
and Science packs have all been added.</li>
<li>Remaining known issues and high priority features:<ul>
<li>Leaving a game as a spectator doesn't work right.</li>
<li>Game owners still can't kick players from their game.</li>

View File

@ -54,6 +54,16 @@ to, for instance, display the number of connected players.
</p>
<p>Recent Changes:</p>
<ul>
<li>3 May 2015:<ul>
<li>The game list automatically updates once per minute now, instead of several times per
second. You can still click the Refresh Games button in the top left corner at any time.</li>
<li>Chat flood protection has been made more strict.</li>
<li>Other back-end changes to attempt to get the AWS bill in control.</li>
<li><strong>All locally-stored custom card sets have been removed.</strong> You must use
Cardcast for custom card sets now.</li>
<li>The 5th and 6th Expansions, PAX Prime 2014 Panel, 10 Days or Whatever of Kwanzaa,
and Science packs have all been added.</li>
</ul></li>
<li>21 February 2015:<ul>
<li>Servers now run in Amazon Web Services. This is going to cost me more, but at least it
should be more stable and not take down my other stuff when it does go down...<ul>

View File

@ -44,8 +44,8 @@ import net.socialgamer.cah.data.Game;
*/
public class Constants {
public static final int CHAT_FLOOD_MESSAGE_COUNT = 5;
public static final int CHAT_FLOOD_TIME = 10 * 1000;
public static final int CHAT_FLOOD_MESSAGE_COUNT = 6;
public static final int CHAT_FLOOD_TIME = 30 * 1000;
public static final int CHAT_MAX_LENGTH = 200;
@SuppressWarnings("serial")

View File

@ -35,6 +35,8 @@ import javax.servlet.ServletContextEvent;
import net.socialgamer.cah.cardcast.CardcastModule;
import net.socialgamer.cah.cardcast.CardcastService;
import net.socialgamer.cah.task.BroadcastGameListUpdateTask;
import net.socialgamer.cah.task.UserPingTask;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
@ -70,6 +72,16 @@ public class StartupUtils extends GuiceServletContextListener {
*/
private static final long PING_CHECK_DELAY = 5 * 1000;
/**
* Delay before the "update game list" broadcast timer is started, in milliseconds.
*/
private static final long BROADCAST_UPDATE_START_DELAY = TimeUnit.SECONDS.toMillis(60);
/**
* Delay between invocations of the "update game list" broadcast timer, in milliseconds.
*/
private static final long BROADCAST_UPDATE_DELAY = TimeUnit.SECONDS.toMillis(60);
/**
* Context attribute key name for the time the server was started.
*/
@ -104,10 +116,18 @@ public class StartupUtils extends GuiceServletContextListener {
public void contextInitialized(final ServletContextEvent contextEvent) {
final ServletContext context = contextEvent.getServletContext();
final Injector injector = getInjector();
final UserPing ping = injector.getInstance(UserPing.class);
final ScheduledThreadPoolExecutor timer = injector
.getInstance(ScheduledThreadPoolExecutor.class);
final UserPingTask ping = injector.getInstance(UserPingTask.class);
timer.scheduleAtFixedRate(ping, PING_START_DELAY, PING_CHECK_DELAY, TimeUnit.MILLISECONDS);
final BroadcastGameListUpdateTask broadcastUpdate = injector
.getInstance(BroadcastGameListUpdateTask.class);
timer.scheduleAtFixedRate(broadcastUpdate, BROADCAST_UPDATE_START_DELAY,
BROADCAST_UPDATE_DELAY, TimeUnit.MILLISECONDS);
serverStarted = new Date();
context.setAttribute(INJECTOR, injector);
context.setAttribute(DATE_NAME, serverStarted);

View File

@ -50,11 +50,11 @@ import net.socialgamer.cah.Constants.LongPollEvent;
import net.socialgamer.cah.Constants.LongPollResponse;
import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.Constants.WhiteCardData;
import net.socialgamer.cah.SafeTimerTask;
import net.socialgamer.cah.cardcast.CardcastDeck;
import net.socialgamer.cah.cardcast.CardcastService;
import net.socialgamer.cah.data.GameManager.GameId;
import net.socialgamer.cah.data.QueuedMessage.MessageType;
import net.socialgamer.cah.task.SafeTimerTask;
import org.apache.log4j.Logger;
import org.hibernate.Session;

View File

@ -1,16 +1,16 @@
/**
* 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
@ -27,17 +27,13 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import net.socialgamer.cah.Constants.LongPollEvent;
import net.socialgamer.cah.Constants.LongPollResponse;
import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.data.Game.TooManyPlayersException;
import net.socialgamer.cah.data.GameManager.GameId;
import net.socialgamer.cah.data.QueuedMessage.MessageType;
import net.socialgamer.cah.task.BroadcastGameListUpdateTask;
import org.apache.log4j.Logger;
@ -49,9 +45,9 @@ import com.google.inject.Singleton;
/**
* Manage games for the server.
*
*
* This is also a Guice provider for game ids.
*
*
* @author Andy Janata (ajanata@socialgamer.net)
*/
@Singleton
@ -62,7 +58,8 @@ public class GameManager implements Provider<Integer> {
private final Provider<Integer> maxGamesProvider;
private final Map<Integer, Game> games = new TreeMap<Integer, Game>();
private final Provider<Game> gameProvider;
private final ConnectedUsers users;
private final BroadcastGameListUpdateTask broadcastUpdate;
/**
* Potential next game id.
*/
@ -70,7 +67,7 @@ public class GameManager implements Provider<Integer> {
/**
* Create a new game manager.
*
*
* @param gameProvider
* Provider for new {@code Game} instances.
* @param maxGamesProvider
@ -81,10 +78,10 @@ public class GameManager implements Provider<Integer> {
@Inject
public GameManager(final Provider<Game> gameProvider,
@MaxGames final Provider<Integer> maxGamesProvider,
final ConnectedUsers users) {
final BroadcastGameListUpdateTask broadcastUpdate) {
this.gameProvider = gameProvider;
this.maxGamesProvider = maxGamesProvider;
this.users = users;
this.broadcastUpdate = broadcastUpdate;
}
private int getMaxGames() {
@ -94,7 +91,7 @@ public class GameManager implements Provider<Integer> {
/**
* Creates a new game, if there are free game slots. Returns {@code null} if there are already the
* maximum number of games in progress.
*
*
* @return Newly created game, or {@code null} if the maximum number of games are in progress.
*/
private Game createGame() {
@ -114,11 +111,11 @@ public class GameManager implements Provider<Integer> {
/**
* Creates a new game and puts the specified user into the game, if there are free game slots.
* Returns {@code null} if there are already the maximum number of games in progress.
*
*
* Creating the game and adding the user are done atomically with respect to another game getting
* created, or even getting the list of active games. It is impossible for another user to join
* the game before the requesting user.
*
*
* @param user
* User to place into the game.
* @return Newly created game, or {@code null} if the maximum number of games are in progress.
@ -150,10 +147,10 @@ public class GameManager implements Provider<Integer> {
/**
* This probably will not be used very often in the server: Games should normally be deleted when
* all players leave it. I'm putting this in if only to help with testing.
*
*
* Destroys a game immediately. This will almost certainly cause errors on the client for any
* players left in the game. If {@code gameId} isn't valid, this method silently returns.
*
*
* @param gameId
* ID of game to destroy.
*/
@ -183,17 +180,15 @@ public class GameManager implements Provider<Integer> {
* Broadcast an event to all users that they should refresh the game list.
*/
public void broadcastGameListRefresh() {
final HashMap<ReturnableData, Object> broadcastData = new HashMap<ReturnableData, Object>();
broadcastData.put(LongPollResponse.EVENT, LongPollEvent.GAME_LIST_REFRESH.toString());
users.broadcastToAll(MessageType.GAME_EVENT, broadcastData);
broadcastUpdate.needsUpdate();
}
/**
* Get an unused game ID, or -1 if the maximum number of games are in progress. This should not be
* called in such a case, though!
*
*
* TODO: make this not suck
*
*
* @return Next game id, or {@code -1} if the maximum number of games are in progress.
*/
@Override
@ -220,7 +215,7 @@ public class GameManager implements Provider<Integer> {
/**
* Try to guess a good candidate for the next game id.
*
*
* @param skip
* An id to skip over.
* @return A guess for the next game id.
@ -255,7 +250,7 @@ public class GameManager implements Provider<Integer> {
/**
* Gets the game with the specified id, or {@code null} if there is no game with that id.
*
*
* @param id
* Id of game to retrieve.
* @return The Game, or {@code null} if there is no game with that id.

View File

@ -0,0 +1,39 @@
package net.socialgamer.cah.task;
import java.util.HashMap;
import net.socialgamer.cah.Constants.LongPollEvent;
import net.socialgamer.cah.Constants.LongPollResponse;
import net.socialgamer.cah.Constants.ReturnableData;
import net.socialgamer.cah.data.ConnectedUsers;
import net.socialgamer.cah.data.QueuedMessage.MessageType;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@Singleton
public class BroadcastGameListUpdateTask extends SafeTimerTask {
private volatile boolean needsUpdate = false;
private final ConnectedUsers users;
@Inject
public BroadcastGameListUpdateTask(final ConnectedUsers users) {
this.users = users;
}
public void needsUpdate() {
needsUpdate = true;
}
@Override
public void process() {
if (needsUpdate) {
final HashMap<ReturnableData, Object> broadcastData = new HashMap<ReturnableData, Object>();
broadcastData.put(LongPollResponse.EVENT, LongPollEvent.GAME_LIST_REFRESH.toString());
users.broadcastToAll(MessageType.GAME_EVENT, broadcastData);
needsUpdate = false;
}
}
}

View File

@ -1,4 +1,4 @@
package net.socialgamer.cah;
package net.socialgamer.cah.task;
import org.apache.log4j.Logger;

View File

@ -1,16 +1,16 @@
/**
* 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
@ -21,27 +21,29 @@
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.socialgamer.cah;
package net.socialgamer.cah.task;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import net.socialgamer.cah.data.ConnectedUsers;
import com.google.inject.Inject;
import com.google.inject.Singleton;
/**
* Timer task to check for disconnected and idle clients.
*
*
* @author Andy Janata (ajanata@gmail.com)
*/
public class UserPing extends SafeTimerTask {
@Singleton
public class UserPingTask extends SafeTimerTask {
private final ConnectedUsers users;
private final ScheduledThreadPoolExecutor globalTimer;
@Inject
public UserPing(final ConnectedUsers users, final ScheduledThreadPoolExecutor globalTimer) {
public UserPingTask(final ConnectedUsers users, final ScheduledThreadPoolExecutor globalTimer) {
this.users = users;
this.globalTimer = globalTimer;
}

View File

@ -1,16 +1,16 @@
/**
* 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
@ -58,7 +58,7 @@ import com.google.inject.Provides;
/**
* Tests for {@code GameManager}.
*
*
* @author Andy Janata (ajanata@socialgamer.net)
*/
public class GameManagerTest {
@ -129,11 +129,8 @@ public class GameManagerTest {
verify(userMock);
}
@SuppressWarnings("unchecked")
@Test
public void testGetAndDestroyGame() {
cuMock.broadcastToAll(eq(MessageType.GAME_EVENT), anyObject(HashMap.class));
expectLastCall().times(3);
replay(cuMock);
replay(userMock);
@ -168,8 +165,6 @@ public class GameManagerTest {
@SuppressWarnings("unchecked")
@Test
public void testCreateGame() {
cuMock.broadcastToAll(eq(MessageType.GAME_EVENT), anyObject(HashMap.class));
expectLastCall().times(3);
cuMock.broadcastToList(anyObject(Collection.class), eq(MessageType.GAME_PLAYER_EVENT),
anyObject(HashMap.class));
expectLastCall().times(3);