com.cubeia.firebase.server.lobby.systemstate.StateLobby.java Source code

Java tutorial

Introduction

Here is the source code for com.cubeia.firebase.server.lobby.systemstate.StateLobby.java

Source

/**
 * Copyright (C) 2011 Cubeia Ltd <info@cubeia.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.cubeia.firebase.server.lobby.systemstate;

import java.lang.management.ManagementFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.jboss.cache.Fqn;

import com.cubeia.core.space.jboss.FqnUtil;
import com.cubeia.firebase.api.lobby.LobbyPath;
import com.cubeia.firebase.api.lobby.LobbyPathType;
import com.cubeia.firebase.api.lobby.LobbyPathUtil;
import com.cubeia.firebase.api.server.conf.Namespace;
import com.cubeia.firebase.api.service.ServiceRegistry;
import com.cubeia.firebase.api.service.config.ClusterConfigProviderContract;
import com.cubeia.firebase.api.util.ConcurrentHashSet;
import com.cubeia.firebase.api.util.SecondCounter;
import com.cubeia.firebase.io.ProtocolObject;
import com.cubeia.firebase.io.StyxSerializer;
import com.cubeia.firebase.io.protocol.TableSnapshotListPacket;
import com.cubeia.firebase.io.protocol.TableSnapshotPacket;
import com.cubeia.firebase.io.protocol.TournamentSnapshotListPacket;
import com.cubeia.firebase.io.protocol.TournamentSnapshotPacket;
import com.cubeia.firebase.server.ClusterRole;
import com.cubeia.firebase.server.gateway.client.Client;
import com.cubeia.firebase.server.lobby.Lobby;
import com.cubeia.firebase.server.lobby.LobbyQueryRequest;
import com.cubeia.firebase.server.lobby.LobbySubscriptionRequest;
import com.cubeia.firebase.server.lobby.LobbyUnsubscriptionRequest;
import com.cubeia.firebase.server.lobby.model.TableInfo;
import com.cubeia.firebase.server.lobby.model.TournamentInfo;
import com.cubeia.firebase.server.lobby.snapshot.DeltaSnapshot;
import com.cubeia.firebase.server.lobby.snapshot.FullSnapshot;
import com.cubeia.firebase.server.lobby.snapshot.NodeChangeDTO;
import com.cubeia.firebase.server.lobby.snapshot.generator.MttSnapshotGenerator;
import com.cubeia.firebase.server.lobby.snapshot.generator.SnapshotGenerator;
import com.cubeia.firebase.server.lobby.snapshot.generator.TableSnapshotGenerator;
import com.cubeia.firebase.server.service.systemstate.SystemStateServiceContract;
import com.cubeia.firebase.server.service.systemstate.cache.SystemStateCacheHandler;
import com.cubeia.firebase.server.util.NodeRoles;
import com.cubeia.firebase.util.executor.JmxExecutor;
import com.cubeia.firebase.util.executor.JmxScheduler;

/**
 * The State Lobby uses the System State Service to get table
 * information.
 * 
 * The StateLobby holds a SnapshotGenerator that handles changes in the system state.
 * The SnapshotGenerator provides the StateLobby with sets of full packets for a node
 * or delta updates for a node. The packets within the generators should be cached so that
 * retrievals within a snapshot update-period are idempotent.
 * 
 * NOTE: This implementation is localized on the Gateway node and holds a direct reference
 * to the client session. This way we can optimize the lobby data sending.
 * 
 * @author Fredrik
 */
public class StateLobby implements Lobby, StateLobbyMBean, LobbyListener {

    static final StyxSerializer SERIALIZER = new StyxSerializer(null);
    private static final boolean USE_LIST_BROADCAST = System
            .getProperty("com.game.server.lobby.systemstate.listBroadcast", "true").equals("true");

    private Logger log = Logger.getLogger(this.getClass());

    /** Current service state */
    private State state = State.STOPPED;

    /** The service registry */
    private final ServiceRegistry registry;

    /** Lobby update period. Default = 2 seconds. */
    private long broadcastPeriod = 2000;

    /**
     * Handles the lobby data structure.
     * 
     * @return
     */
    private LobbyCacheHandler cacheHandler;

    /** A JMX-bound scheduler thread pool */
    private JmxScheduler scheduler = new JmxScheduler(1, "LobbyGenerator");

    /** A JMX-bound executor for executing change notifications */
    private JmxExecutor executor = new JmxExecutor(1, "LobbyUpdate");

    private ConcurrentMap<LobbyPathType, SnapshotGenerator> generators = new ConcurrentHashMap<LobbyPathType, SnapshotGenerator>();

    /**
     * Holds all current subscriptions on this node.
     */
    private ConcurrentMap<LobbyPath, Set<Client>> subscriptions = new ConcurrentHashMap<LobbyPath, Set<Client>>();

    /**
     * Holds object specific subscribers
     */
    private ObjectSubscriptions objectSubscriptions = new ObjectSubscriptions();

    /** Listeners for lobby changes */
    private Set<LobbyListener> listeners = new ConcurrentHashSet<LobbyListener>();

    /** The broadcast task. We keep a reference so we can stop and start it */
    private ScheduledFuture<?> bcastTask;

    private SecondCounter updateCounter = new SecondCounter();

    /**
     * If this flag is set, then we will run broadcast tasks as usual,
     * but we will not send out any data to the clients.
     */
    private AtomicBoolean paused = new AtomicBoolean(false);

    /**
     * Constructor.
     * We will use the registry to lookup the system state.
     * 
     * @param registry
     */
    public StateLobby(ServiceRegistry registry) {
        this.registry = registry;
        initJmx();
        initBroadcastInterval();
        // Create Generators
        SnapshotGenerator tableSnapshotGenerator = new TableSnapshotGenerator(this, LobbyPathType.TABLES);
        SnapshotGenerator mttSnapshotGenerator = new MttSnapshotGenerator(this, LobbyPathType.MTT);
        generators.put(LobbyPathType.TABLES, tableSnapshotGenerator);
        generators.put(LobbyPathType.MTT, mttSnapshotGenerator);
    }

    private void initBroadcastInterval() {
        if (registry != null) {
            ClusterConfigProviderContract contr = registry.getServiceInstance(ClusterConfigProviderContract.class);
            Namespace nameSpace = NodeRoles.getNodeNamespace(ClusterRole.CLIENT_NODE, "lobby");
            StateLobbyConfig conf = contr.getConfiguration(StateLobbyConfig.class, nameSpace);
            broadcastPeriod = conf.getLobbyBroadcastPeriod();
            log.info("Lobby broadcast period set to: " + broadcastPeriod);
        }
    }

    public String getStateDescription() {
        return state.toString();
    }

    // -------------- SERVICE METHODS  ------------------

    /**
     * Start the service and add the listener.
     * Fetches the cache handler from the System State service.
     */
    public void start() {
        state = State.STARTED;
        SystemStateCacheHandler cache = getCache();
        cacheHandler = new LobbyCacheHandler(cache, this);
        for (SnapshotGenerator gen : generators.values()) {
            gen.start();
        }

        scheduleBroadcast();
    }

    /**
     * Schedules lobby broadcast with fixed delay.
     */
    private void scheduleBroadcast() {
        Runnable bcast = null;
        if (USE_LIST_BROADCAST) {
            bcast = new StateLobbyListBroadcast(paused, generators, subscriptions, objectSubscriptions);
        } else {
            bcast = new StateLobbySingleBroadcast(paused, generators, subscriptions, objectSubscriptions);
        }
        bcastTask = scheduler.scheduleWithFixedDelay(bcast, broadcastPeriod, broadcastPeriod,
                TimeUnit.MILLISECONDS);
    }

    /**
     * Needed for mocking reasons
     * 
     * @return SystemStateCacheHandler
     */
    protected SystemStateCacheHandler getCache() {
        SystemStateCacheHandler cache = getSystemState().getCacheHandler();
        return cache;
    }

    public void stop() {
        bcastTask.cancel(false);
        scheduler.stop();
        for (SnapshotGenerator gen : generators.values()) {
            gen.stop();
        }

        cacheHandler.destroy();
        state = State.STOPPED;
        executor.stop();
    }

    // ----------- END OF SERVICE METHODS  ---------------

    // -------------- LOBBY LISTENER METHODS ------------------

    public void nodeCreated(LobbyPath path) {
        TableCreatedTask task = new TableCreatedTask(path);
        executor.submit(task);
    }

    public void nodeAttributeChanged(NodeChangeDTO change) {
        TableChangedTask task = new TableChangedTask(change);
        executor.submit(task);
    }

    public void tableRemoved(LobbyPath path) {
        TableRemovedTask task = new TableRemovedTask(path);
        executor.submit(task);
    }

    public void nodeRemoved(String path) {
        PathRemovedTask task = new PathRemovedTask(path);
        executor.submit(task);
    }

    // ---------- END OF LOBBY LISTENER METHODS  ---------------

    public void addPath(String path) {
        cacheHandler.addPath(path);
        /*
         * Force generation of a full snapshot. This is done
         * in order prevent snapshots from being created later, when 
         * tables are actually picked up... Well, I have no
         * idea what that actually means, but it does seem
         * to work, at least for now /Larsan
         */
        LobbyPath lp = FqnUtil.parseFqn(Fqn.fromString(path));
        SnapshotGenerator gen = generators.get(lp.getType());
        gen.initSnapshot(lp);
    }

    public void addLobbyListener(LobbyListener listener) {
        listeners.add(listener);
    }

    public void removeLobbyListener(LobbyListener listener) {
        listeners.remove(listener);
    }

    public long getBroadcastPeriod() {
        return broadcastPeriod;
    }

    public void setBroadcastPeriod(long period) {
        broadcastPeriod = period;

        if (bcastTask != null) {
            bcastTask.cancel(false);
            scheduleBroadcast();
        }
    }

    /**
     * Acquire the system state.
     * The method is protected to it can be overridden by 
     * mock objects.
     * @return
     */
    protected SystemStateServiceContract getSystemState() {
        return registry.getServiceInstance(SystemStateServiceContract.class);
    }

    /**
     * One shot query
     */
    public List<ProtocolObject> getLobbyQuery(LobbyQueryRequest request) {
        SnapshotGenerator generator = getSnapshotGenerator(request.getType());
        List<ProtocolObject> packets = generator.getFullSnapshot(request.getPath());
        return packets;
    }

    public Collection<TableInfo> getTableInfos(LobbyPath path) {
        //Collection<TableInfo> tables = null; //new ArrayList<TableInfo>();
        String fqn = path.getNameSpace();
        if (cacheHandler.exists(fqn)) {
            return cacheHandler.getTables(fqn);
        } else {
            return Collections.emptySet();
        }
        // return tables;
    }

    public Collection<TournamentInfo> getMttInfos(LobbyPath path) {
        //Collection<TournamentInfo> tournaments = new ArrayList<TournamentInfo>();
        String fqn = path.getNameSpace();
        if (cacheHandler.exists(fqn)) {
            return cacheHandler.getTournaments(fqn);
        } else {
            return Collections.emptySet();
        }
        //return tournaments;
    }

    /**
     * Subscription request.
     * We get all leaves in the lobby tree and add the client as a subscriber to
     * all of those.
     * 
     * 
     * @param request
     * @param client, reference to the Client for direct communication
     */
    public void subscribe(LobbySubscriptionRequest request, Client client) {
        LobbyPath path = request.getPath();

        Collection<ProtocolObject> fullSnapshot = getSnapshotGenerator(request.getType()).getFullSnapshot(path);
        List<ProtocolObject> listSnapshot = toSnapshotLists(fullSnapshot);
        client.sendClientPackets(listSnapshot);

        // Get all nodes that we should register the player on
        Collection<LobbyPath> subNodes = cacheHandler.getSubNodes(path.getNameSpace());
        addSubscriberToNodes(client, subNodes);
    }

    private List<ProtocolObject> toSnapshotLists(Collection<ProtocolObject> fullSnapshot) {
        List<TableSnapshotPacket> tables = new LinkedList<TableSnapshotPacket>();
        List<TournamentSnapshotPacket> mtts = new LinkedList<TournamentSnapshotPacket>();

        for (ProtocolObject o : fullSnapshot) {
            if (o instanceof TableSnapshotPacket) {
                tables.add((TableSnapshotPacket) o);
            } else if (o instanceof TournamentSnapshotPacket) {
                mtts.add((TournamentSnapshotPacket) o);
            } else {
                log.error("Unknown packet type: " + o.getClass().getName());
            }
        }

        List<ProtocolObject> ret = new ArrayList<ProtocolObject>(2);
        if (tables.size() > 0) {
            ret.add(new TableSnapshotListPacket(tables));
        }
        if (mtts.size() > 0) {
            ret.add(new TournamentSnapshotListPacket(mtts));
        }
        return ret;
    }

    /* (non-Javadoc)
     * @see com.game.server.lobby.Lobby#unsubscribeAll(com.cubeia.firebase.server.gateway.client.Client)
     */
    public void unsubscribeAll(Client client) {
        if (client == null) {
            log.debug("Client was null when trying to unsubscribe all. Doing nothing.");
            return;
        }

        for (Set<Client> subscribers : subscriptions.values()) {
            subscribers.remove(client);
        }
        objectSubscriptions.removeAllSubscriptionsForClient(client);
    }

    /* (non-Javadoc)
     * @see com.game.server.lobby.Lobby#unsubscribe(com.game.server.lobby.LobbyUnsubscriptionRequest, com.cubeia.firebase.server.gateway.client.Client)
     */
    public void unsubscribe(LobbyUnsubscriptionRequest request, Client client) {
        Collection<LobbyPath> subNodes = cacheHandler.getSubNodes(request.getPath().getNameSpace());

        for (LobbyPath key : subNodes) {
            Set<Client> subscribers = getSubscribers(key);
            subscribers.remove(client);
        }
    }

    public void subscribeToLobbyObject(LobbySubscriptionRequest request, Client client) {
        log.debug("Add lobby object subscription: " + request);
        // Get applicable full snapshot
        Collection<ProtocolObject> fullSnapshot = getSnapshotGenerator(request.getType())
                .getFullSnapshot(request.getPath());
        ProtocolObject snapshotPacket = null;

        if (request.getType() == LobbyPathType.TABLES) {
            snapshotPacket = findTableSnapshotPacket(request.getPath().getObjectId(), fullSnapshot);
        } else if (request.getType() == LobbyPathType.MTT) {
            snapshotPacket = findTournamentSnapshotPacket(request.getPath().getObjectId(), fullSnapshot);
        }

        if (snapshotPacket != null) {
            objectSubscriptions.addSubscription(request, client);
            client.sendClientPacket(snapshotPacket);
        }
    }

    public void unsubscribeToLobbyObject(LobbyUnsubscriptionRequest request, Client client) {
        log.debug("Remove lobby object subscription: " + request);
        objectSubscriptions.removeSubscription(request, client);
    }

    public void addSubscribersToNewFQN(LobbyPath path) {
        // Check if we are missing snapshot for the path
        if (subscriptions.get(path) == null) {
            List<LobbyPath> newLobbyPaths = new LinkedList<LobbyPath>();
            LobbyPath base = new LobbyPath(path.getType(), path.getArea(), path.getDomain(), -1);
            newLobbyPaths.add(base);

            // Either we have no subscribers for this branch or we have a newly created FQN.
            // We will traverse upwards until we find an FQN that exists in subscription map or we hit the root
            LobbyPath ancestor = LobbyPathUtil.getAncestor(path);
            while (!ancestor.getDomain().equals("") && subscriptions.get(ancestor) == null) {
                newLobbyPaths.add(ancestor);
                ancestor = LobbyPathUtil.getAncestor(ancestor);
            }

            // FIXME: Correct?
            if (subscriptions.get(ancestor) != null) {
                addSubscribersToNodes(subscriptions.get(ancestor), newLobbyPaths);
                for (Client client : subscriptions.get(ancestor)) {
                    List<ProtocolObject> fullSnapshot = getSnapshotGenerator(path.getType()).getFullSnapshot(path);
                    client.sendClientPackets(fullSnapshot);
                }
            }
        }

    }

    public List<LobbyPath> getAllLobbyLeaves(LobbyPathType type) {
        List<LobbyPath> leaves = new ArrayList<LobbyPath>();
        List<LobbyPath> games = cacheHandler.getAreas(type);
        for (LobbyPath path : games) {
            leaves.addAll(cacheHandler.getEndNodes(path));
        }
        return leaves;
    }

    public Collection<LobbyPath> getLeaves(LobbyPath path) {
        Collection<LobbyPath> leaves = cacheHandler.getEndNodes(path);
        return leaves;
    }

    public ProtocolObject getSnapshot(LobbyPathType type, int objectId) {
        return getSnapshotGenerator(type).getSingleSnapshot(getPath(type, objectId), objectId);
    }

    private LobbyPath getPath(LobbyPathType type, int objectId) {
        return getSnapshotGenerator(type).getPath(objectId);
    }

    /**
     * Add this client as a subscriber to all provided nodes.
     * 
     * @param client
     * @param subNodes
     */
    private void addSubscriberToNodes(Client client, Collection<LobbyPath> subNodes) {
        for (LobbyPath key : subNodes) {
            Set<Client> subscribers = getSubscribers(key);
            subscribers.add(client);
            subscriptions.put(key, subscribers);
        }
    }

    private void addSubscribersToNodes(Set<Client> subscribers, Collection<LobbyPath> subNodes) {
        for (LobbyPath key : subNodes) {
            subscriptions.put(key, subscribers);
        }
    }

    /**
     * Get snapshot generator depending on lobby type
     * 
     * @param type
     * @return
     */
    protected SnapshotGenerator getSnapshotGenerator(LobbyPathType type) {
        return generators.get(type);
    }

    /**
     * Get or Create subscriber list for a path key 
     * 
     * @param key
     * @return
     */
    public Set<Client> getSubscribers(LobbyPath path) {
        Set<Client> clients = subscriptions.get(path);
        if (clients == null) {
            ConcurrentHashSet<Client> newSet = new ConcurrentHashSet<Client>();
            clients = subscriptions.putIfAbsent(path, newSet);
            if (clients == null) {
                clients = newSet;
            }
        }
        return clients;
    }

    @Override
    public int countSubscribersForPath(String type, int area, String domain) {
        Set<Client> clients = subscriptions.get(new LobbyPath(LobbyPathType.valueOf(type), area, domain, -1));
        return (clients == null ? 0 : clients.size());
    }

    /**
     * Add MBean info to JMX.
     * Will be called from the constructor.
     *
     */
    private void initJmx() {
        try {
            MBeanServer mbs = getMBeanServer();
            ObjectName monitorName = new ObjectName("com.cubeia.firebase.lobby:type=SysLobby");
            if (!mbs.isRegistered(monitorName)) {
                mbs.registerMBean(this, monitorName);
            }
        } catch (Exception e) {
            log.error("failed to start JMX for State Lobby", e);
        }
    }

    private MBeanServer getMBeanServer() {
        return ManagementFactory.getPlatformMBeanServer();
    }

    /**
     * Iterate all packets and find the matching table snapshot packet.
     * 
     * @param tableId
     * @param fullSnapshot
     * @return
     */
    private ProtocolObject findTableSnapshotPacket(int tableId, Collection<ProtocolObject> fullSnapshot) {
        ProtocolObject snapshotPacket = null;
        for (ProtocolObject protocol : fullSnapshot) {
            if (protocol instanceof TableSnapshotPacket) {
                TableSnapshotPacket packet = (TableSnapshotPacket) protocol;
                if (packet.tableid == tableId) {
                    snapshotPacket = packet;
                    break;
                }
            }
        }
        return snapshotPacket;
    }

    /**
     * Iterate all packets and find the matching tournament snapshot packet.
     * 
     * @param tableId
     * @param fullSnapshot
     * @return
     */
    private ProtocolObject findTournamentSnapshotPacket(int mttId, Collection<ProtocolObject> fullSnapshot) {
        ProtocolObject snapshotPacket = null;
        for (ProtocolObject protocol : fullSnapshot) {
            if (protocol instanceof TournamentSnapshotPacket) {
                TournamentSnapshotPacket packet = (TournamentSnapshotPacket) protocol;
                if (packet.mttid == mttId) {
                    snapshotPacket = packet;
                    break;
                }
            }
        }
        return snapshotPacket;
    }

    /**
    * Task for executing a table removed notification.
    * We will notify all users.
    * 
    * @author Fredrik
    */
    private class TableRemovedTask implements Runnable {
        private final LobbyPath path;

        public TableRemovedTask(LobbyPath path) {
            this.path = path;
        }

        public void run() {
            try {
                // Code below reports to both MTT and Table generators = no good!
                for (LobbyListener listener : listeners) {
                    listener.tableRemoved(path);
                }

                // Get the correct generator
                LobbyListener listener = generators.get(path.getType());
                listener.tableRemoved(path);

                // Make sure object subscriptions can clean up
                objectSubscriptions.removeObject(path, path.getObjectId());

            } catch (Exception e) {
                log.error("Failed to remove object from listener. Path: " + path, e);
            }
        }
    }

    private class PathRemovedTask implements Runnable {
        private final String path;

        public PathRemovedTask(String path) {
            this.path = path;
        }

        public void run() {
            try {
                // Code below reports to both MTT and Table generators = no good!
                for (LobbyListener listener : listeners) {
                    listener.nodeRemoved(path);
                }
                // Check un-subscriptions
                subscriptions.remove(path);
                Collection<LobbyPath> subNodes = cacheHandler.getSubNodes(path);
                for (LobbyPath key : subNodes) {
                    subscriptions.remove(key);
                }
            } catch (Exception e) {
                log.error("Failed to remove lobby path from listener. Path: " + path, e);
            }
        }
    }

    /**
     * Task for executing a table changed notification
     * 
     * @author Fredrik
     */
    private class TableChangedTask implements Runnable {
        private final NodeChangeDTO change;

        public TableChangedTask(NodeChangeDTO change) {
            this.change = change;
        }

        public void run() {
            try {
                updateCounter.register();

                for (LobbyListener listener : listeners) {
                    listener.nodeAttributeChanged(change);
                }

                // Get the correct generator
                LobbyListener listener = generators.get(change.getPath().getType());
                listener.nodeAttributeChanged(change);

                // If a new FQN we might need to include subscribers
                addSubscribersToNewFQN(change.getPath());
            } catch (Throwable e) {
                log.error("Failed to handle lobby node change", e);
            }
        }
    }

    /**
     * Task for executing a table created notification
     * 
     * @author Fredrik
     */
    private class TableCreatedTask implements Runnable {
        private final LobbyPath path;

        public TableCreatedTask(LobbyPath path) {
            this.path = path;
        }

        public void run() {
            for (LobbyListener listener : listeners) {
                listener.nodeCreated(path);
            }
        }
    }

    /* --------------------------------------------------
     * 
     *   JMX METHODS
     * 
     * -------------------------------------------------- */

    /**
     * Write all subscriptions to the log file.
     * This can be very extensive and should not be used frequently.
     * 
     */
    public void dumpSubscriptionInfoToLog() {
        log.info(printSubscriptionInfo());
    }

    public String printSubscriptionInfo() {
        String info = "Current Lobby Subscriptions: \n";
        for (LobbyPath path : subscriptions.keySet()) {
            Set<Client> clients = new HashSet<Client>(subscriptions.get(path));
            info += "\n" + path + " (" + clients.size() + ")\n";
            for (Client c : clients) {
                info += "\t" + c + "\n";
            }
        }
        return info;
    }

    public String printPaths() {
        SnapshotGenerator tableGenerator = generators.get(LobbyPathType.TABLES);
        SnapshotGenerator tournamentGenerator = generators.get(LobbyPathType.MTT);

        String info = "TABLE PATHS:\n";
        ConcurrentMap<LobbyPath, FullSnapshot> tableSnapshots = tableGenerator.getFullSnapshots();
        for (LobbyPath path : tableSnapshots.keySet()) {
            info += path + "\n";
        }

        info += "\nTOURNAMENT PATHS\n";
        ConcurrentMap<LobbyPath, FullSnapshot> tournamentSnapshots = tournamentGenerator.getFullSnapshots();
        for (LobbyPath path : tournamentSnapshots.keySet()) {
            info += path + "\n";
        }

        return info;
    }

    public String printTableData(int tableId) {
        ProtocolObject snapshot = getSnapshot(LobbyPathType.TABLES, tableId);
        return String.valueOf(snapshot);
    }

    public String printTournamentData(int mttId) {
        ProtocolObject snapshot = getSnapshot(LobbyPathType.MTT, mttId);
        return String.valueOf(snapshot);
    }

    public int getTableUpdatesPerSecond() {
        return updateCounter.current();
    }

    public boolean isPaused() {
        return paused.get();
    }

    public void pause() {
        paused.set(true);
    }

    public void unpause() {
        paused.set(false);
    }

    public int getSnapshotNodeCount() {
        int count = 0;
        for (SnapshotGenerator generator : generators.values()) {
            ConcurrentMap<LobbyPath, FullSnapshot> snapshots = generator.getFullSnapshots();
            count += snapshots.size();
        }
        return count;
    }

    public int getDeltaSnapshotNodeCount() {
        int count = 0;
        for (SnapshotGenerator generator : generators.values()) {
            ConcurrentMap<LobbyPath, DeltaSnapshot> snapshots = generator.getDeltaSnapshots();
            count += snapshots.size();
        }
        return count;
    }

    public String printPacketCounts() {
        ConcurrentMap<LobbyPath, Integer> fullPacketCount = new ConcurrentHashMap<LobbyPath, Integer>();
        ConcurrentMap<LobbyPath, Integer> deltaPacketCount = new ConcurrentHashMap<LobbyPath, Integer>();

        for (SnapshotGenerator generator : generators.values()) {
            ConcurrentMap<LobbyPath, FullSnapshot> fullSnapshots = generator.getFullSnapshots();
            ConcurrentMap<LobbyPath, DeltaSnapshot> deltaSnapshots = generator.getDeltaSnapshots();

            for (LobbyPath path : fullSnapshots.keySet()) {
                FullSnapshot fullSnapshot = fullSnapshots.get(path);
                fullPacketCount.put(path, fullSnapshot.getLobbyData().size());
            }

            for (LobbyPath path : deltaSnapshots.keySet()) {
                DeltaSnapshot deltaSnapshot = deltaSnapshots.get(path);
                deltaPacketCount.put(path, deltaSnapshot.getLobbyData().size());
            }
        }

        String info = "Lobby Snapshots Per Chached Lobby Path\n";
        info += "-----------------------\n";
        info += "Full Packet Path Count:  " + fullPacketCount.size() + "\n";
        info += "Delta Packet Path Count: " + deltaPacketCount.size() + "\n";
        info += "-----------------------\n";
        info += "FULL PACKET COUNT PER PATH\n";
        for (LobbyPath path : fullPacketCount.keySet()) {
            info += StringUtils.rightPad(path.toString(), 60) + "\t : " + fullPacketCount.get(path) + "\n";
        }
        info += "-----------------------\n";
        info += "DELTA PACKET COUNT PER PATH\n";
        for (LobbyPath path : deltaPacketCount.keySet()) {
            info += StringUtils.rightPad(path.toString(), 60) + "\t : " + deltaPacketCount.get(path) + "\n";
            ;
        }

        return info;
    }

    public int getSubscriptionCount() {
        int count = 0;
        for (LobbyPath path : subscriptions.keySet()) {
            count += subscriptions.get(path).size();
        }
        return count;
    }

}