diff --git a/WebContent/license.html b/WebContent/license.html index cbc5bac..baf6655 100644 --- a/WebContent/license.html +++ b/WebContent/license.html @@ -291,5 +291,8 @@ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +

+ UADetector +

diff --git a/build.properties.example b/build.properties.example index 565e352..348984c 100644 --- a/build.properties.example +++ b/build.properties.example @@ -36,9 +36,13 @@ hibernate.cache.use_query_cache=false hibernate.cache.provider_class=org.hibernate.cache.NoCacheProvider # Metrics implementation. -# FIXME: this isn't used. Change the binding in CahModule. pyx.metrics.impl=net.socialgamer.cah.metrics.NoOpMetrics +# for kafka metrics +kafka.host=kafka-host:9092 +kafka.topic=pyx-metrics + + # GeoIP database for analytics. If unset, will not be used. # Only used if the above is not NoOpMetrics. # See README.md for instructions. diff --git a/pom.xml b/pom.xml index 3357900..1108fcc 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ net.socialgamer pyx - 0.5.0-SNAPSHOT + 0.6.0-SNAPSHOT jar pyx @@ -268,9 +268,9 @@ 4.0 - commons-collections - commons-collections - 3.1 + org.apache.commons + commons-collections4 + 4.1 org.apache.commons @@ -346,7 +346,7 @@ org.apache.kafka kafka-clients - 0.10.1.1 + 0.10.2.0 com.maxmind.geoip2 diff --git a/src/main/filtered-resources/WEB-INF/pyx.properties b/src/main/filtered-resources/WEB-INF/pyx.properties index a1a7085..7ea290e 100644 --- a/src/main/filtered-resources/WEB-INF/pyx.properties +++ b/src/main/filtered-resources/WEB-INF/pyx.properties @@ -8,5 +8,9 @@ pyx.build=${buildNumber} # this is NOT allowed to be changed during a reload, as metrics depend on previous events pyx.metrics.impl=${pyx.metrics.impl} +# these also are NOT allowed to be changed during a reload +kafka.host=${kafka.host} +kafka.topic=${kafka.topic} + # TODO reload this file occasionally in case it changes? geoip.db=${geoip.db} diff --git a/src/main/java/net/socialgamer/cah/CahModule.java b/src/main/java/net/socialgamer/cah/CahModule.java index 61ed1d6..511ab03 100644 --- a/src/main/java/net/socialgamer/cah/CahModule.java +++ b/src/main/java/net/socialgamer/cah/CahModule.java @@ -35,12 +35,13 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; +import javax.servlet.ServletContext; + import net.socialgamer.cah.data.GameManager; import net.socialgamer.cah.data.GameManager.GameId; import net.socialgamer.cah.data.GameManager.MaxGames; import net.socialgamer.cah.data.User; import net.socialgamer.cah.metrics.Metrics; -import net.socialgamer.cah.metrics.NoOpMetrics; import net.socialgamer.cah.metrics.UniqueIds; import org.hibernate.Session; @@ -61,6 +62,12 @@ public class CahModule extends AbstractModule { private final static Properties properties = new Properties(); + private final ServletContext context; + + public CahModule(final ServletContext context) { + this.context = context; + } + @Override protected void configure() { bind(Integer.class) @@ -76,10 +83,21 @@ public class CahModule extends AbstractModule { .toInstance(Collections.synchronizedSet(new HashSet())); bind(Properties.class).toInstance(properties); - bind(Metrics.class).to(NoOpMetrics.class); + + // FIXME huge hack. + StartupUtils.reloadProperties(context, properties); + final String metricsClassName = properties.getProperty("pyx.metrics.impl"); + try { + @SuppressWarnings("unchecked") + final Class metricsClass = (Class) Class + .forName(metricsClassName); + bind(Metrics.class).to(metricsClass); + } catch (final ClassNotFoundException e) { + throw new RuntimeException(e); + } + bind(Date.class).annotatedWith(ServerStarted.class).toInstance(new Date()); bind(String.class).annotatedWith(UniqueId.class).toProvider(UniqueIds.class); - install(new FactoryModuleBuilder().build(User.Factory.class)); final ScheduledThreadPoolExecutor threadPool = diff --git a/src/main/java/net/socialgamer/cah/StartupUtils.java b/src/main/java/net/socialgamer/cah/StartupUtils.java index a95dea4..e547c8c 100644 --- a/src/main/java/net/socialgamer/cah/StartupUtils.java +++ b/src/main/java/net/socialgamer/cah/StartupUtils.java @@ -114,7 +114,7 @@ public class StartupUtils extends GuiceServletContextListener { @Override public void contextInitialized(final ServletContextEvent contextEvent) { final ServletContext context = contextEvent.getServletContext(); - final Injector injector = getInjector(); + final Injector injector = getInjector(context); final ScheduledThreadPoolExecutor timer = injector .getInstance(ScheduledThreadPoolExecutor.class); @@ -140,10 +140,17 @@ public class StartupUtils extends GuiceServletContextListener { } public static void reloadProperties(final ServletContext context) { - LOG.info("Reloading pyx.properties"); - final Injector injector = (Injector) context.getAttribute(INJECTOR); final Properties props = injector.getInstance(Properties.class); + reloadProperties(context, props); + } + + /** + * Hack method for calling inside CahModule before the injector is usable. + */ + public static void reloadProperties(final ServletContext context, final Properties props) { + LOG.info("Reloading pyx.properties"); + final File propsFile = new File(context.getRealPath("/WEB-INF/pyx.properties")); try { synchronized (props) { @@ -163,8 +170,12 @@ public class StartupUtils extends GuiceServletContextListener { "/WEB-INF/log4j.properties")); } + protected Injector getInjector(final ServletContext context) { + return Guice.createInjector(new CahModule(context), new CardcastModule()); + } + @Override protected Injector getInjector() { - return Guice.createInjector(new CahModule(), new CardcastModule()); + throw new RuntimeException("Not supported."); } } diff --git a/src/main/java/net/socialgamer/cah/data/ConnectedUsers.java b/src/main/java/net/socialgamer/cah/data/ConnectedUsers.java index b4236fb..5cf35bb 100644 --- a/src/main/java/net/socialgamer/cah/data/ConnectedUsers.java +++ b/src/main/java/net/socialgamer/cah/data/ConnectedUsers.java @@ -142,7 +142,7 @@ public class ConnectedUsers { logger.warn(String.format("Unable to get address for user %s (hostname: %s)", user.getNickname(), user.getHostname()), e); } - metrics.newUser(user.getPersistentId(), user.getSessionId(), geo, user.getAgentName(), + metrics.userConnect(user.getPersistentId(), user.getSessionId(), geo, user.getAgentName(), user.getAgentType(), user.getAgentOs(), user.getAgentLanguage()); return null; diff --git a/src/main/java/net/socialgamer/cah/data/Game.java b/src/main/java/net/socialgamer/cah/data/Game.java index 05b8a31..391b337 100644 --- a/src/main/java/net/socialgamer/cah/data/Game.java +++ b/src/main/java/net/socialgamer/cah/data/Game.java @@ -1446,29 +1446,29 @@ public class Game { } /** - * The judge has selected a card. The {@code cardId} passed in may be any white cards's ID for + * The judge has selected a card. The {@code cardId} passed in may be any white card's ID for * black cards that have multiple selection, however only the first card in the set's ID will be * passed around to clients. * - * @param user + * @param judge * Judge user. * @param cardId * Selected card ID. * @return Error code if there is an error, or null if success. */ - public ErrorCode judgeCard(final User user, final int cardId) { + public ErrorCode judgeCard(final User judge, final int cardId) { final Player cardPlayer; synchronized (judgeLock) { - final Player player = getPlayerForUser(user); - if (getJudge() != player) { + final Player judgePlayer = getPlayerForUser(judge); + if (getJudge() != judgePlayer) { return ErrorCode.NOT_JUDGE; } else if (state != GameState.JUDGING) { return ErrorCode.NOT_YOUR_TURN; } // shouldn't ever happen, but just in case... - if (null != player) { - player.resetSkipCount(); + if (null != judgePlayer) { + judgePlayer.resetSkipCount(); } cardPlayer = playedCards.getPlayerForId(cardId); @@ -1512,8 +1512,11 @@ public class Game { rescheduleTimer(task, ROUND_INTERMISSION); } - metrics.roundComplete(currentUniqueId, user.getSessionId(), cardPlayer.getUser().getSessionId(), - playedCards.cardsByUser()); + final Map> cardsBySessionId = new HashMap<>(); + playedCards.cardsByUser().forEach( + (key, value) -> cardsBySessionId.put(key.getSessionId(), value)); + metrics.roundComplete(currentUniqueId, uniqueIdProvider.get(), judge.getSessionId(), + cardPlayer.getUser().getSessionId(), blackCard, cardsBySessionId); return null; } diff --git a/src/main/java/net/socialgamer/cah/data/PlayerPlayedCardsTracker.java b/src/main/java/net/socialgamer/cah/data/PlayerPlayedCardsTracker.java index 3afe9b5..955647f 100644 --- a/src/main/java/net/socialgamer/cah/data/PlayerPlayedCardsTracker.java +++ b/src/main/java/net/socialgamer/cah/data/PlayerPlayedCardsTracker.java @@ -148,10 +148,7 @@ public class PlayerPlayedCardsTracker { */ public synchronized Map> cardsByUser() { final Map> cardsByUser = new HashMap<>(); - // TODO java8: streams - for (final Map.Entry> entry : playerCardMap.entrySet()) { - cardsByUser.put(entry.getKey().getUser(), entry.getValue()); - } + playerCardMap.forEach((key, value) -> cardsByUser.put(key.getUser(), value)); return Collections.unmodifiableMap(cardsByUser); } } diff --git a/src/main/java/net/socialgamer/cah/metrics/KafkaMetrics.java b/src/main/java/net/socialgamer/cah/metrics/KafkaMetrics.java index 466ebd0..a6ab232 100644 --- a/src/main/java/net/socialgamer/cah/metrics/KafkaMetrics.java +++ b/src/main/java/net/socialgamer/cah/metrics/KafkaMetrics.java @@ -23,60 +23,312 @@ package net.socialgamer.cah.metrics; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import javax.annotation.Nullable; + +import net.socialgamer.cah.data.BlackCard; import net.socialgamer.cah.data.CardSet; -import net.socialgamer.cah.data.User; import net.socialgamer.cah.data.WhiteCard; +import net.socialgamer.cah.db.PyxBlackCard; +import net.socialgamer.cah.db.PyxCardSet; +import net.socialgamer.cah.db.PyxWhiteCard; +import org.apache.kafka.clients.producer.Callback; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.clients.producer.RecordMetadata; +import org.apache.kafka.common.PartitionInfo; +import org.apache.kafka.common.serialization.StringSerializer; import org.apache.log4j.Logger; +import org.json.simple.JSONValue; +import com.google.inject.Inject; import com.google.inject.Singleton; import com.maxmind.geoip2.model.CityResponse; /** - * Metrics implementation that sends all data to a Kafka topic. + * Metrics implementation that sends all data to an Apache Kafka topic. * * @author Andy Janata (ajanata@socialgamer.net) */ @Singleton public class KafkaMetrics implements Metrics { + private static final String metricsVersion = "0.1"; private static final Logger LOG = Logger.getLogger(KafkaMetrics.class); - @Override - public void serverStart(final String startupId) { - LOG.trace(String.format("serverStarted(%s)", startupId)); + private final ProducerCallback callback = new ProducerCallback(); + private final String build; + private final String hosts; + private final String topic; + private volatile Producer producer; + private final Properties producerProps; + private final Lock makeProducerLock = new ReentrantLock(); + + @Inject + public KafkaMetrics(final Properties properties) { + build = properties.getProperty("pyx.build"); + hosts = properties.getProperty("kafka.host"); + topic = properties.getProperty("kafka.topic"); + LOG.info("Sending metrics to Kafka topic " + topic); + producerProps = getProducerProps(); + tryEnsureProducer(); + } + + private Properties getProducerProps() { + final Properties props = new Properties(); + props.put("bootstrap.servers", hosts); + props.put("key.serializer", StringSerializer.class.getName()); + props.put("value.serializer", StringSerializer.class.getName()); + props.put("acks", "0"); + props.put("compression.type", "gzip"); + props.put("retries", 1); + props.put("client.id", "pyx-" + build); + props.put("max.block.ms", TimeUnit.SECONDS.toMillis(5)); + // TODO TLS, authentication + return props; + } + + /** + * Helper method to log at TRACE level while only taking string format penalties if such logging + * is enabled. Includes the method name as well. + * @param format Format string to log + * @param params Parameters for format string + */ + private void trace(final String format, final Object... params) { + if (LOG.isTraceEnabled()) { + final StackTraceElement[] stack = Thread.currentThread().getStackTrace(); + final String message = String.format(format, params); + // skip getStackTrace and this method + LOG.trace(String.format("%s: %s", stack[2].getMethodName(), message)); + } + } + + /** + * Attempt to create a producer. {@link #producer} must still be checked against null after + * calling this method. + */ + private void tryEnsureProducer() { + if (null != producer) { + return; + } + + if (makeProducerLock.tryLock()) { + try { + LOG.info("Attempting to create producer."); + final Producer newProducer = new KafkaProducer<>(producerProps); + final List info = newProducer.partitionsFor(topic); + LOG.info(String.format("Topic %s has %d partitions", topic, info.size())); + final Producer oldProducer = producer; + producer = newProducer; + if (null != oldProducer) { + LOG.info("Old producer closed."); + oldProducer.close(); + } + LOG.info("Producer created."); + } catch (final Exception e) { + LOG.error("Unable to retrieve partition info for topic " + topic, e); + } finally { + makeProducerLock.unlock(); + } + } else { + LOG.warn("Another thread is creating a producer."); + } + } + + private void send(final Map map) { + send(JSONValue.toJSONString(map)); + } + + private void send(final String json) { + trace("%s", json); + tryEnsureProducer(); + if (null != producer) { + final ProducerRecord record = new ProducerRecord<>(topic, null, json); + producer.send(record, callback); + } else { + LOG.warn("Dropping event " + json); + } + } + + private class ProducerCallback implements Callback { + @Override + public void onCompletion(final RecordMetadata metadata, final Exception exception) { + if (null != exception) { + LOG.error("Unable to send event to Kafka", exception); + final Producer oldProducer = producer; + producer = null; + if (null != oldProducer) { + LOG.info("Closing producer after exception."); + oldProducer.close(); + } + } + } } @Override - public void newUser(final String guid, final String sessionId, final CityResponse geoIp, - final String agentName, final String agentType, final String agentOs, - final String agentLanguage) { - LOG.trace(String.format("newUser(%s, %s, %s, %s, %s, %s, %s)", guid, sessionId, geoIp, - agentName, agentType, agentOs, agentLanguage)); + public void shutdown() { + trace(""); + if (null != producer) { + producer.close(); + } + } + + private Map getEventMap(final String type, final Map data) { + final Map ret = new HashMap<>(); + ret.put("timestamp", System.currentTimeMillis()); + ret.put("build", build); + ret.put("type", type); + ret.put("data", data); + ret.put("version", metricsVersion); + return ret; + } + + @Override + public void serverStart(final String startupId) { + trace("%s", startupId); + final Map data = new HashMap<>(); + data.put("startupId", startupId); + send(getEventMap("serverStart", data)); + } + + @Override + public void userConnect(final String persistentId, final String sessionId, + @Nullable final CityResponse geoIp, final String agentName, final String agentType, + final String agentOs, final String agentLanguage) { + trace("%s, %s, %s, %s, %s, %s, %s", persistentId, sessionId, geoIp, agentName, agentType, + agentOs, agentLanguage); + + final Map data = new HashMap<>(); + data.put("persistentId", persistentId); + data.put("sessionId", sessionId); + + final Map browser = new HashMap<>(); + browser.put("name", agentName); + browser.put("type", agentType); + browser.put("os", agentOs); + browser.put("language", agentLanguage); + data.put("browser", browser); + + final Map geo = new HashMap<>(); + if (null != geoIp) { + // it appears these will never be null and will return null/blank data, but let's be sure + if (null != geoIp.getCity()) { + geo.put("city", geoIp.getCity().getName()); + } + if (null != geoIp.getCountry()) { + geo.put("country", geoIp.getCountry().getIsoCode()); + } + final List subdivCodes = new ArrayList<>(2); + geoIp.getSubdivisions().forEach(subdiv -> subdivCodes.add(subdiv.getIsoCode())); + if (!subdivCodes.isEmpty()) { + geo.put("subdivisions", subdivCodes); + } + if (null != geoIp.getRepresentedCountry()) { + geo.put("representedCountry", geoIp.getRepresentedCountry().getIsoCode()); + } + if (null != geoIp.getPostal()) { + geo.put("postal", geoIp.getPostal().getCode()); + } + } + data.put("geo", geo); + + send(getEventMap("userConnect", data)); } @Override public void userDisconnect(final String sessionId) { - LOG.trace(String.format("userDisconnect(%s)", sessionId)); + trace("%s", sessionId); + + final Map data = new HashMap<>(); + data.put("sessionId", sessionId); + send(getEventMap("userDisconnect", data)); } @Override public void gameStart(final String gameId, final Collection decks, final int blanks, final int maxPlayers, final int scoreGoal, final boolean hasPassword) { - LOG.trace(String.format("gameStart(%s, %s, %d, %d, %d, %s)", gameId, decks.toArray(), blanks, - maxPlayers, scoreGoal, hasPassword)); + trace("%s, %s, %d, %d, %d, %s", gameId, decks.toArray(), blanks, maxPlayers, scoreGoal, + hasPassword); + + final Map data = new HashMap<>(); + data.put("gameId", gameId); + data.put("blankCardsInDeck", blanks); + data.put("maxPlayers", maxPlayers); + data.put("scoreGoal", scoreGoal); + data.put("hasPassword", hasPassword); + + final List> deckInfos = new ArrayList<>(decks.size()); + for (final CardSet deck : decks) { + final Map deckInfo = new HashMap<>(); + // if we ever have more than cardcast for custom cards, this needs updated to indicate which + // custom deck source, but will still be correct for this specific flag + deckInfo.put("isCustom", !(deck instanceof PyxCardSet)); + deckInfo.put("id", deck.getId()); + // TODO(?) don't include these data for non-custom decks? + deckInfo.put("name", deck.getName()); + deckInfo.put("whiteCount", deck.getWhiteCards().size()); + deckInfo.put("blackCount", deck.getBlackCards().size()); + deckInfos.add(deckInfo); + } + data.put("decks", deckInfos); + + send(getEventMap("gameStart", data)); } @Override - public void roundComplete(final String gameId, final String judgeSessionId, - final String winnerSessionId, - final Map> cards) { - LOG.trace(String.format("roundJudged(%s, %s, %s, %s)", gameId, judgeSessionId, winnerSessionId, - cards)); + public void roundComplete(final String gameId, final String roundId, final String judgeSessionId, + final String winnerSessionId, final BlackCard blackCard, + final Map> cards) { + trace("%s, %s, %s, %s, %s, %s", gameId, roundId, judgeSessionId, winnerSessionId, blackCard, + cards); + + final Map data = new HashMap<>(); + data.put("gameId", gameId); + data.put("roundId", roundId); + data.put("judgeSessionId", judgeSessionId); + data.put("winnerSessionId", winnerSessionId); + + // ]> + final Map>> allCardMap = new HashMap<>(); + for (final Entry> cardsByUser : cards.entrySet()) { + final List> userCards = new ArrayList<>(cardsByUser.getValue().size()); + for (final WhiteCard card : cardsByUser.getValue()) { + final Map cardInfo = new HashMap<>(); + // same re: more custom deck sources + cardInfo.put("isCustom", !(card instanceof PyxWhiteCard)); + cardInfo.put("isWriteIn", card.isWriteIn()); + // negative IDs would be custom: either blank or cardcast. they are not stable. + cardInfo.put("id", card.getId()); + cardInfo.put("text", card.getText()); + userCards.add(cardInfo); + } + allCardMap.put(cardsByUser.getKey(), userCards); + } + data.put("cardsByUserId", allCardMap); + + final Map blackCardData = new HashMap<>(); + // same re: more custom deck sources + blackCardData.put("isCustom", !(blackCard instanceof PyxBlackCard)); + // negative IDs would be custom: either blank or cardcast. they are not stable. + blackCardData.put("id", blackCard.getId()); + blackCardData.put("text", blackCard.getText()); + blackCardData.put("draw", blackCard.getDraw()); + blackCardData.put("pick", blackCard.getPick()); + data.put("blackCard", blackCardData); + + send(getEventMap("roundComplete", data)); } } diff --git a/src/main/java/net/socialgamer/cah/metrics/Metrics.java b/src/main/java/net/socialgamer/cah/metrics/Metrics.java index 27282e4..03e8c9e 100644 --- a/src/main/java/net/socialgamer/cah/metrics/Metrics.java +++ b/src/main/java/net/socialgamer/cah/metrics/Metrics.java @@ -29,8 +29,8 @@ import java.util.Map; import javax.annotation.Nullable; +import net.socialgamer.cah.data.BlackCard; import net.socialgamer.cah.data.CardSet; -import net.socialgamer.cah.data.User; import net.socialgamer.cah.data.WhiteCard; import com.maxmind.geoip2.model.CityResponse; @@ -42,17 +42,19 @@ import com.maxmind.geoip2.model.CityResponse; * @author Andy Janata (ajanata@socialgamer.net) */ public interface Metrics { + void shutdown(); + void serverStart(String startupId); - void newUser(String persistentId, String sessionId, @Nullable CityResponse geoIp, + void userConnect(String persistentId, String sessionId, @Nullable CityResponse geoIp, String agentName, String agentType, String agentOs, String agentLanguage); void userDisconnect(String sessionId); // The card data is way too complicated to dictate the format it should be in, so let // implementations deal with the structured data. - void roundComplete(String gameId, String judgeSessionId, String winnerSessionId, - Map> cards); + void roundComplete(String gameId, String roundId, String judgeSessionId, String winnerSessionId, + BlackCard blackCard, Map> cards); void gameStart(String gameId, Collection decks, int blanks, int maxPlayers, int scoreGoal, boolean hasPassword); diff --git a/src/main/java/net/socialgamer/cah/metrics/NoOpMetrics.java b/src/main/java/net/socialgamer/cah/metrics/NoOpMetrics.java index 9d01891..bb2b6c6 100644 --- a/src/main/java/net/socialgamer/cah/metrics/NoOpMetrics.java +++ b/src/main/java/net/socialgamer/cah/metrics/NoOpMetrics.java @@ -27,8 +27,8 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import net.socialgamer.cah.data.BlackCard; import net.socialgamer.cah.data.CardSet; -import net.socialgamer.cah.data.User; import net.socialgamer.cah.data.WhiteCard; import org.apache.log4j.Logger; @@ -47,16 +47,21 @@ public class NoOpMetrics implements Metrics { private static final Logger LOG = Logger.getLogger(NoOpMetrics.class); + @Override + public void shutdown() { + // nothing to do + } + @Override public void serverStart(final String startupId) { LOG.trace(String.format("serverStarted(%s)", startupId)); } @Override - public void newUser(final String guid, final String sessionId, final CityResponse geoIp, + public void userConnect(final String persistentId, final String sessionId, final CityResponse geoIp, final String agentName, final String agentType, final String agentOs, final String agentLanguage) { - LOG.trace(String.format("newUser(%s, %s, %s, %s, %s, %s, %s)", guid, sessionId, geoIp, + LOG.trace(String.format("newUser(%s, %s, %s, %s, %s, %s, %s)", persistentId, sessionId, geoIp, agentName, agentType, agentOs, agentLanguage)); } @@ -73,10 +78,10 @@ public class NoOpMetrics implements Metrics { } @Override - public void roundComplete(final String gameId, final String judgeSessionId, - final String winnerSessionId, - final Map> cards) { - LOG.trace(String.format("roundJudged(%s, %s, %s, %s)", gameId, judgeSessionId, winnerSessionId, - cards)); + public void roundComplete(final String gameId, final String roundId, final String judgeSessionId, + final String winnerSessionId, final BlackCard blackCard, + final Map> cards) { + LOG.trace(String.format("roundJudged(%s, %s, %s, %s, %s, %s)", gameId, roundId, judgeSessionId, + winnerSessionId, blackCard, cards)); } } diff --git a/src/test/java/net/socialgamer/cah/data/GameManagerTest.java b/src/test/java/net/socialgamer/cah/data/GameManagerTest.java index ed71296..e2b1b78 100644 --- a/src/test/java/net/socialgamer/cah/data/GameManagerTest.java +++ b/src/test/java/net/socialgamer/cah/data/GameManagerTest.java @@ -46,6 +46,7 @@ import net.socialgamer.cah.data.GameManager.GameId; import net.socialgamer.cah.data.GameManager.MaxGames; import net.socialgamer.cah.data.QueuedMessage.MessageType; import net.socialgamer.cah.metrics.Metrics; +import net.socialgamer.cah.metrics.NoOpMetrics; import org.hibernate.Session; import org.junit.After; @@ -98,6 +99,7 @@ public class GameManagerTest { } }); bind(ScheduledThreadPoolExecutor.class).toInstance(threadPool); + bind(Metrics.class).to(NoOpMetrics.class); } @Provides