Package com.betfair.cougar.transport.socket

Source Code of com.betfair.cougar.transport.socket.PooledServerConnectedObjectManager

/*
* Copyright 2013, The Sporting Exchange Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.betfair.cougar.transport.socket;

import com.betfair.cougar.api.ExecutionContextWithTokens;
import com.betfair.cougar.api.LogExtension;
import com.betfair.cougar.api.UUIDGenerator;
import com.betfair.cougar.core.api.ev.ConnectedResponse;
import com.betfair.cougar.core.api.ev.ExecutionResult;
import com.betfair.cougar.core.api.ev.OperationDefinition;
import com.betfair.cougar.core.api.ev.Subscription;
import com.betfair.cougar.core.api.exception.CougarFrameworkException;
import com.betfair.cougar.core.api.logging.EventLogger;
import com.betfair.cougar.core.impl.logging.ConnectedObjectLogEvent;
import com.betfair.cougar.logging.CougarLogger;
import com.betfair.cougar.logging.CougarLoggingUtils;
import com.betfair.cougar.netutil.nio.*;
import com.betfair.cougar.netutil.nio.connected.InitialUpdate;
import com.betfair.cougar.netutil.nio.connected.TerminateHeap;
import com.betfair.cougar.netutil.nio.connected.Update;
import com.betfair.cougar.netutil.nio.message.EventMessage;
import com.betfair.cougar.transport.api.protocol.CougarObjectIOFactory;
import com.betfair.cougar.transport.api.protocol.CougarObjectOutput;
import com.betfair.cougar.transport.api.protocol.socket.NewHeapSubscription;
import com.betfair.cougar.util.UUIDGeneratorImpl;
import com.betfair.platform.virtualheap.Heap;
import org.apache.mina.common.IoSession;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;

import java.io.ByteArrayOutputStream;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;

import static com.betfair.cougar.core.api.ev.Subscription.CloseReason.*;

@ManagedResource
public class PooledServerConnectedObjectManager implements ServerConnectedObjectManager {

    private static CougarLogger logger = CougarLoggingUtils.getLogger(PooledServerConnectedObjectManager.class);

    private static final AtomicLong heapStateInstanceIdSource = new AtomicLong();

    private EventLogger eventLogger;
    private NioLogger nioLogger;

    private BlockingDeque<String> heapsWaitingForUpdate = new LinkedBlockingDeque<String>();

    // mods of these 2 for writes were handled. removals are more problematic.. so let's into a r/w lock over their interactions
    private ReentrantLock subTermLock = new ReentrantLock();
    private Map<String, HeapState> heapStates = new HashMap<String, HeapState>();
    private Map<Long, String> heapUris = new HashMap<Long, String>();
    private Map<IoSession, Multiset<String>> heapsByClient = new HashMap<IoSession, Multiset<String>>();

    private AtomicLong heapIdGenerator = new AtomicLong(0);
    private CougarObjectIOFactory objectIOFactory;

    private int numProcessingThreads;
    private List<ConnectedObjectPusher> pushers = new ArrayList<ConnectedObjectPusher>();

    private int maxUpdateActionsPerMessage;

    private UUIDGenerator uuidGenerator = new UUIDGeneratorImpl();

    private Thread shutdownHook = new Thread(new Runnable() {
        @Override
        public void run() {
            logger.log(Level.INFO, "Terminating all push subscriptions due to application shutdown.");
            terminateAllSubscriptions(NODE_SHUTDOWN);
        }
    }, "PooledServerConnectedObjectManager-ShutdownHook");

    // used for testing
    Map<String, HeapState> getHeapStates() {
        return heapStates;
    }

    public Map<Long, String> getHeapUris() {
        return heapUris;
    }

    // used for testing
    Map<IoSession, Multiset<String>> getHeapsByClient() {
        return heapsByClient;
    }

    BlockingDeque<String> getHeapsWaitingForUpdate() {
        return heapsWaitingForUpdate;
    }

    // used for monitoring
    public List<String> getHeapsForSession(IoSession session) {
        List<String> ret = new ArrayList<String>();
        try {
            subTermLock.lock();
            Multiset<String> s = heapsByClient.get(session);
            if (s != null) {
                ret.addAll(s.keySet());
            }
        } finally {
            subTermLock.unlock();
        }
        return ret;
    }

    public HeapStateMonitoring getHeapStateForMonitoring(String uri) {
        return heapStates.get(uri);
    }

    @Override
    public void setNioLogger(NioLogger nioLogger) {
        this.nioLogger = nioLogger;
    }

    public void setObjectIOFactory(CougarObjectIOFactory objectIOFactory) {
        this.objectIOFactory = objectIOFactory;
    }

    public void setNumProcessingThreads(int i) {
        this.numProcessingThreads = i;
    }

    public void setEventLogger(EventLogger eventLogger) {
        this.eventLogger = eventLogger;
    }

    public void setMaxUpdateActionsPerMessage(int maxUpdateActionsPerMessage) {
        this.maxUpdateActionsPerMessage = maxUpdateActionsPerMessage;
    }

    public void start() {
        for (int i = 0; i < numProcessingThreads; i++) {
            ConnectedObjectPusher pusher = new ConnectedObjectPusher();
            pushers.add(pusher);
            new Thread(pusher, "ConnectedObjectPusher-" + (i + 1)).start();
        }
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }

    public void stop() {
        for (ConnectedObjectPusher pusher : pushers) {
            pusher.stop();
        }

        Runtime.getRuntime().removeShutdownHook(shutdownHook);
        shutdownHook.run();
    }

    private void terminateAllSubscriptions(Subscription.CloseReason reason) {
        if (heapsByClient != null) {
            List<IoSession> sessions;
            try {
                subTermLock.lock();
                // take a copy in case it's being modified as we shutdown
                sessions = new ArrayList<IoSession>(heapsByClient.keySet());
            } finally {
                subTermLock.unlock();
            }

            for (IoSession session : sessions) {
                terminateSubscriptions(session, reason);
            }
        }
    }

    // note, you must have the subterm lock before calling this method
    private HeapState processHeapStateCreation(final ConnectedResponse result, final String heapUri) {
        if (!subTermLock.isHeldByCurrentThread()) {
            throw new IllegalStateException("You must have the subTermLock before calling this method");
        }
        final HeapState newState = new HeapState(result.getHeap());
        // we're safe to lock this out of normal order as the HeapState isn't visible to other threads until the
        // end of this block. We have to have the lock before we make it visible..
        newState.getUpdateLock().lock();
        // new heap for this transport
        UpdateProducingHeapListener listener = new UpdateProducingHeapListener() {

            @Override
            protected void doUpdate(Update u) {
                if (u.getActions().size() > 0) {
                    newState.getQueuedChanges().add(new QueuedHeapChange(u));
                    // bad luck, we just added the heap and it's just about to get terminated...
                    if (u.getActions().contains(TerminateHeap.INSTANCE)) {
                        newState.getQueuedChanges().add(new HeapTermination());
                    }
                    heapsWaitingForUpdate.add(heapUri);
                }
            }
        };
        newState.setHeapListener(listener);
        result.getHeap().addListener(listener, false);
        heapStates.put(heapUri, newState);
        heapUris.put(newState.getHeapId(), heapUri);
        return newState;
    }

    @Override
    public void addSubscription(final SocketTransportCommandProcessor commandProcessor, final SocketTransportRPCCommand command, final ConnectedResponse result, final OperationDefinition operationDefinition, final ExecutionContextWithTokens context, final LogExtension connectedObjectLogExtension) {
        final String heapUri = result.getHeap().getUri();
        HeapState heapState = null;
        try {
            boolean readyToContinue = false;

            while (!readyToContinue) {
                // only need this lock to modify the heapStates map, we need the state lock to modify the contained state later..
                subTermLock.lock();
                heapState = heapStates.get(heapUri);
                boolean wasNewHeapState = heapState == null;
                if (wasNewHeapState) {
                    heapState = processHeapStateCreation(result, heapUri);
                    readyToContinue = true;
                }
                // for existing heaps we lock in the same way as usual
                else {
                    // we have to release this lock now so we can get them in the right order, otherwise we could deadlock
                    // this is safe since we know that at this moment in time there is a live heap state for this heapuri
                    subTermLock.unlock();

                    // between these 2 calls one of 3 things can happen:
                    // 1: nothing
                    // 2: the last subscriber to the heap state unsubscribes and the heap state is removed
                    // 3: the last subscriber goes away and someone else comes through at the right moment and recreates it (less likely)

                    // in the last case we need to just keep looping until we know for certain it hasn't happened...
                    // what we need is a unique heapstate instance id which we can compare to... (AtomicLong should be sufficient)

                    // now get them in the right order
                    heapState.getUpdateLock().lock();
                    subTermLock.lock();


                    // so, in case 2 above the heap is now not in the map.. in which case we're going to subscribe to something just as it goes..
                    // so, lets do that new state check once more
                    if (!heapStates.containsKey(heapUri)) {
                        heapState = processHeapStateCreation(result, heapUri);
                        readyToContinue = true;
                    }
                    // ok, so it's still there, now we need to check if it's the same one..
                    else {
                        HeapState latestState = heapStates.get(heapUri);
                        // case 1 above
                        if (latestState.getInstanceId() == heapState.getInstanceId()) {
                            readyToContinue = true;
                        }
                        // case 3 above
                        else {
                            // this shouldn't matter anymore as we've got a lock on a dead heap
                            heapState.getUpdateLock().unlock();
                            // reset our check state and go back round until we're happy
                            heapState = latestState;
                        }
                    }
                }
            }

            // right, now we've got both locks, in the right order and we've definitely got a heap state which everyone else can also get/has got

            final HeapState finalHeapState = heapState;
            // hmm,
            final Subscription subscription = result.getSubscription();
            result.getHeap().traverse(new UpdateProducingHeapListener() {
                @Override
                protected void doUpdate(Update u) {
                    boolean updateContainsTermination = u.getActions().contains(TerminateHeap.INSTANCE);
                    if (updateContainsTermination) {
                        // note this won't notify this sub, which never got started. the publisher code won't expect a call back for this since
                        // it's just terminated the heap, which implies it wants to disconnect all clients anyway
                        terminateSubscriptions(command.getSession(), heapUri, REQUESTED_BY_PUBLISHER);
                        commandProcessor.writeErrorResponse(command, context, new CougarFrameworkException("Subscription requested for terminated heap: " + heapUri));
                        return;
                    }

                    Multiset<String> heapsForThisClient = heapsByClient.get(command.getSession());
                    if (heapsForThisClient == null) {
                        heapsForThisClient = new Multiset<String>();
                        heapsByClient.put(command.getSession(), heapsForThisClient);
                    }

                    long heapId = finalHeapState.getHeapId();

                    final String subscriptionId = finalHeapState.addSubscription(connectedObjectLogExtension, subscription, command.getSession());
                    subscription.addListener(new Subscription.SubscriptionListener() {
                        @Override
                        public void subscriptionClosed(Subscription subscription, Subscription.CloseReason reason) {
                            if (reason == REQUESTED_BY_PUBLISHER) {
                                PooledServerConnectedObjectManager.this.terminateSubscription(command.getSession(), heapUri, subscriptionId, reason);
                            }
                            // log end regardless of the reason
                            finalHeapState.logSubscriptionEnd(subscriptionId, connectedObjectLogExtension, reason);
                        }
                    });
                    boolean newHeapDefinition = heapsForThisClient.count(heapUri) == 0;
                    heapsForThisClient.add(heapUri);

                    NewHeapSubscription response;
                    if (newHeapDefinition) {
                        response = new NewHeapSubscription(heapId, subscriptionId, heapUri);
                    } else {
                        response = new NewHeapSubscription(heapId, subscriptionId);
                    }

                    // first tell the client about the heap
                    ExecutionResult executionResult = new ExecutionResult(response);
                    boolean successfulResponse = commandProcessor.writeSuccessResponse(command, executionResult);
                    // so if we couldn't send the response it means we know the client isn't going to have a sub response, which means we need to clean up on our
                    // end so we don't get warnings on the client about receiving updates for something it knows nothing about..
                    if (!successfulResponse) {
                        terminateSubscriptions(command.getSession(), heapUri, INTERNAL_ERROR);
                    }

                    if (newHeapDefinition) {
                        // then add the sub initialisation to the update queue
                        finalHeapState.getQueuedChanges().add(new QueuedHeapChange(new QueuedSubscription(command.getSession(), new InitialUpdate(u))));
                        heapsWaitingForUpdate.add(heapUri);
                    }
                }
            });
        } finally {
            subTermLock.unlock();
            assert heapState != null;
            heapState.getUpdateLock().unlock();
        }
    }

    @Override
    public void terminateSubscription(IoSession session, TerminateSubscription payload) {
        Subscription.CloseReason reason = Subscription.CloseReason.REQUESTED_BY_PUBLISHER;
        try {
            reason = Subscription.CloseReason.valueOf(payload.getCloseReason());
        } catch (IllegalArgumentException iae) {
            // unrecognised reason
        }
        terminateSubscription(session, heapUris.get(payload.getHeapId()), payload.getSubscriptionId(), reason);
    }

    /**
     * Terminates a single subscription to a single heap
     */
    public void terminateSubscription(IoSession session, String heapUri, String subscriptionId, Subscription.CloseReason reason) {
        Lock heapUpdateLock = null;
        try {
            HeapState state = heapStates.get(heapUri);
            if (state != null) {
                heapUpdateLock = state.getUpdateLock();
                heapUpdateLock.lock();
            }
            subTermLock.lock();

            if (state != null) {
                if (!state.isTerminated()) {
                    state.terminateSubscription(session, subscriptionId, reason);
                    // notify client
                    if (reason == REQUESTED_BY_PUBLISHER || reason == Subscription.CloseReason.REQUESTED_BY_PUBLISHER_ADMINISTRATOR) {
                        try {
                            nioLogger.log(NioLogger.LoggingLevel.TRANSPORT, session, "Notifying client that publisher has terminated subscription %s", subscriptionId);
                            NioUtils.writeEventMessageToSession(session, new TerminateSubscription(state.getHeapId(), subscriptionId, reason.name()), objectIOFactory);
                        } catch (Exception e) {
                            // if we can't tell them about it then something more serious has just happened.
                            // the client will likely find out anyway since this will likely mean a dead session
                            // we'll just log some info to the log to aid any debugging. We won't change the closure reason.
                            logger.log(Level.INFO, "Error occurred whilst trying to inform client of subscription termination", e);
                            nioLogger.log(NioLogger.LoggingLevel.SESSION, session, "Error occurred whilst trying to inform client of subscription termination, closing session");
                            // we'll request a closure of the session too to make sure everything gets cleaned up, although chances are it's already closed
                            session.close();
                        }
                    }

                    if (state.hasSubscriptions()) {
                        terminateSubscriptions(heapUri, reason);
                    } else if (state.getSubscriptions(session).isEmpty()) {
                        terminateSubscriptions(session, heapUri, reason);
                    }
                }
            }

            Multiset<String> heapsForSession = heapsByClient.get(session);
            if (heapsForSession != null) {
                heapsForSession.remove(heapUri);
                if (heapsForSession.isEmpty()) {
                    terminateSubscriptions(session, reason);
                }
            }
        } finally {
            subTermLock.unlock();
            if (heapUpdateLock != null) {
                heapUpdateLock.unlock();
            }
        }
    }

    /**
     * Terminates all subscriptions to a given heap from a single session
     */
    private void terminateSubscriptions(IoSession session, String heapUri, Subscription.CloseReason reason) {
        terminateSubscriptions(session, heapStates.get(heapUri), heapUri, reason);
    }

    /**
     * Terminates all subscriptions to a given heap from a single session
     */
    private void terminateSubscriptions(IoSession session, HeapState state, String heapUri, Subscription.CloseReason reason) {
        Lock heapUpdateLock = null;
        try {
            if (state != null) {
                heapUpdateLock = state.getUpdateLock();
                heapUpdateLock.lock();
            }
            subTermLock.lock();

            if (state != null) {
                if (!state.isTerminated()) {
                    state.terminateSubscriptions(session, reason);
                    if (state.getSessions().isEmpty()) {
                        terminateSubscriptions(heapUri, reason);
                    }
                }
            }

            Multiset<String> heapsForSession = heapsByClient.get(session);
            if (heapsForSession != null) {
                nioLogger.log(NioLogger.LoggingLevel.TRANSPORT, session, "Terminating subscription on %s heaps", heapsForSession.keySet().size());
                heapsForSession.removeAll(heapUri);
                if (heapsForSession.isEmpty()) {
                    terminateSubscriptions(session, reason);
                }
            }
        } finally {
            subTermLock.unlock();
            if (heapUpdateLock != null) {
                heapUpdateLock.unlock();
            }
        }
    }

    /**
     * Terminates all subscriptions for a given client
     */
    private void terminateSubscriptions(IoSession session, Subscription.CloseReason reason) {
        Multiset<String> heapsForThisClient;
        try {
            subTermLock.lock();
            heapsForThisClient = heapsByClient.remove(session);
        } finally {
            subTermLock.unlock();
        }

        if (heapsForThisClient != null) {
            for (String s : heapsForThisClient.keySet()) {
                terminateSubscriptions(session, s, reason);
            }
        }
    }

    /**
     * Terminates all subscriptions for a given heap
     */
    private void terminateSubscriptions(String heapUri, Subscription.CloseReason reason) {
        HeapState state = heapStates.get(heapUri);
        if (state != null) {
            try {
                state.getUpdateLock().lock();
                subTermLock.lock();
                // if someone got here first, don't bother doing the work
                if (!state.isTerminated()) {
                    heapStates.remove(heapUri);
                    heapUris.remove(state.getHeapId());
                    List<IoSession> sessions = state.getSessions();
                    for (IoSession session : sessions) {
                        terminateSubscriptions(session, state, heapUri, reason);
                    }
                    logger.log(Level.SEVERE, "Terminating heap state '%s'", heapUri);
                    state.terminate();
                    state.removeListener();
                }
            } finally {
                subTermLock.unlock();
                state.getUpdateLock().unlock();
            }
        }
    }

    private class ConnectedObjectPusher implements Runnable {
        private volatile boolean running = true;

        public void run() {
            try {
                while (running) {
                    try {
                        String uri = heapsWaitingForUpdate.pollFirst(1000, TimeUnit.MILLISECONDS);
                        if (uri == null) {
                            continue;
                        }
                        HeapState heapState = heapStates.get(uri);
                        if (heapState == null) {
                            continue;
                        }
                        Lock lock = heapState.getUpdateLock();
                        // make sure noone else tries to send later updates while we're preparing this one..
                        lock.lock();
                        try {
                            if (heapState.isTerminated()) {
                                logger.log(Level.SEVERE, "heapState.isTerminated()");
                                continue;
                            }
                            // cleanly dequeue everything waiting in the queue
                            List<QueuedHeapChange> changes = new LinkedList<QueuedHeapChange>();
                            Iterator<QueuedHeapChange> changeIterator = heapState.getQueuedChanges().iterator();
                            while (changeIterator.hasNext()) {
                                changes.add(changeIterator.next());
                                changeIterator.remove();
                            }
                            // if someone already took all the updates (in a batch), then this could easily happen
                            while (!changes.isEmpty()) {
                                // any subs that occurred mid update stream need to be added to the list of sessions at the right place so they get updates from that point on.
                                Iterator<QueuedHeapChange> it = changes.iterator();
                                while (it.hasNext()) {
                                    QueuedHeapChange next = it.next();
                                    if (next.isSub()) {
                                        QueuedSubscription sub = next.getSub();
                                        IoSession session = sub.getSession();
                                        // send the initial update to this session only
                                        // because we write it with the last update id (because this session has never seen this
                                        //   heap before) the new client will just continue after with everyone else.

                                        long updateId = heapState.getLastUpdateId();
                                        nioLogger.log(NioLogger.LoggingLevel.TRANSPORT, session, "ConnectedObjectPusher: Sending initial heap state with updateId = %s for heapId = %s", updateId, heapState.getHeapId());
                                        NioUtils.writeEventMessageToSession(session, new HeapDelta(heapState.getHeapId(), updateId, Collections.<Update>singletonList(sub.getInitialState())), objectIOFactory);

                                        heapState.addSession(session);
                                        it.remove();
                                    } else {
                                        break;
                                    }
                                }

                                // loop through the changes, looking to see if we've got a heap sub in there, if so, send everything before in a batch
                                List<Update> updatesThisCycle = new ArrayList<Update>();
                                it = changes.iterator();
                                while (it.hasNext()) {
                                    QueuedHeapChange next = it.next();
                                    if (next.isUpdate()) {
                                        updatesThisCycle.add(next.getUpdate());
                                        it.remove();
                                    } else {
                                        break;
                                    }
                                }

                                if (!updatesThisCycle.isEmpty()) {
                                    // send all the updates in a set of batch messages to each session that is listening to this heap

                                    // so now we need to split this into the batches that will be sent and send each in turn, for this we need to split based on max batch size
                                    // each message must contain atomic QueuedHeapChanges, but may not contain more than maxUpdateActionsPerMessage, except where required to send an atomic QueuedHeapChange
                                    final int updatesToSendThisCycle = updatesThisCycle.size();
                                    int numQueuedHeapChangesSent = 0;
                                    while (numQueuedHeapChangesSent < updatesToSendThisCycle) {
                                        int numQueuedHeapChangesThisMessage = 0;
                                        int numActionsThisMessage = 0;
                                        List<Update> updatesThisBatch = new ArrayList<Update>();
                                        Iterator<Update> queuedIt = updatesThisCycle.iterator();
                                        while (queuedIt.hasNext()) {
                                            Update u = queuedIt.next();
                                            int actionsThisUpdate = u.getActions().size();
                                            if (numQueuedHeapChangesThisMessage > 0 && numActionsThisMessage + actionsThisUpdate > maxUpdateActionsPerMessage) {
                                                break;
                                            }
                                            updatesThisBatch.add(u);
                                            queuedIt.remove();
                                            numQueuedHeapChangesThisMessage++;
                                            numActionsThisMessage += actionsThisUpdate;
                                        }

                                        // we really only want to serialise this once per protocol version (given that serialisation can change by protocol version)
                                        Set<Byte> protocolVersions = new HashSet<Byte>();
                                        for (IoSession session : heapState.getSessions()) {
                                            protocolVersions.add(CougarProtocol.getProtocolVersion(session));
                                        }
                                        Map<Byte, EventMessage> serialisedUpdatesByProtocolVersion = new HashMap<Byte, EventMessage>();
                                        long updateId = heapState.getNextUpdateId();
                                        for (Byte version : protocolVersions) {
                                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
                                            CougarObjectOutput out = objectIOFactory.newCougarObjectOutput(baos, version);
                                            out.writeObject(new HeapDelta(heapState.getHeapId(), updateId, updatesThisBatch));
                                            out.flush();
                                            serialisedUpdatesByProtocolVersion.put(version, new EventMessage(baos.toByteArray()));
                                        }
                                        // now write these out for each session
                                        for (IoSession session : heapState.getSessions()) {
                                            nioLogger.log(NioLogger.LoggingLevel.TRANSPORT, session, "Sending heap delta of size %s and with updateId = %s for heapId = %s", updatesThisBatch.size(), updateId, heapState.getHeapId());
                                            session.write(serialisedUpdatesByProtocolVersion.get(CougarProtocol.getProtocolVersion(session)));
                                        }

                                        numQueuedHeapChangesSent += updatesThisBatch.size();
                                    }

                                }

                                // time to kill the heap..
                                if (!changes.isEmpty() && changes.get(0).isTermination()) {
                                    changes.remove(0);
                                    terminateSubscriptions(uri, REQUESTED_BY_PUBLISHER);
                                }
                            }
                        } catch (Exception e) {
                            logger.log(Level.SEVERE, "error sending updates", e);
                            terminateSubscriptions(uri, INTERNAL_ERROR);
                        } finally {
                            lock.unlock();
                        }
                    } catch (InterruptedException e) {
                        // oh well, prob just an opportunity to break out
                    }
                }
            } catch (Exception e) {
                logger.log(Level.SEVERE, "thread died", e);
            } catch (Error e) {
                logger.log(Level.SEVERE, "thread died", e);
                throw e;
            }
        }

        public void stop() {
            running = false;
        }
    }

    @Override
    public void sessionOpened(IoSession session) {
    }

    @Override
    public void sessionClosed(IoSession session) {
        nioLogger.log(NioLogger.LoggingLevel.TRANSPORT, session, "Session closed, terminating live subscriptions");
        terminateSubscriptions(session, CONNECTION_CLOSED);
    }

    private class QueuedHeapChange {
        private Update update;
        private QueuedSubscription sub;

        private QueuedHeapChange(Update update) {
            this.update = update;
        }

        private QueuedHeapChange(QueuedSubscription sub) {
            this.sub = sub;
        }

        protected QueuedHeapChange() {
        }

        public Update getUpdate() {
            return update;
        }

        public QueuedSubscription getSub() {
            return sub;
        }

        public boolean isUpdate() {
            return update != null;
        }

        public boolean isSub() {
            return sub != null;
        }

        public boolean isTermination() {
            return false;
        }
    }

    private class HeapTermination extends QueuedHeapChange {
        @Override
        public boolean isTermination() {
            return true;
        }
    }

    private class QueuedSubscription {
        private IoSession session;
        private InitialUpdate initialState;

        private QueuedSubscription(IoSession session, InitialUpdate initialState) {
            this.session = session;
            this.initialState = initialState;
        }

        public IoSession getSession() {
            return session;
        }

        public InitialUpdate getInitialState() {
            return initialState;
        }
    }

    public interface HeapStateMonitoring {

        SortedMap<String, List<String>> getSubscriptionIdsBySessionId();

        long getLastUpdateId();

        int getSubscriptionCount();

        int getSessionCount();
    }

    class HeapState implements HeapStateMonitoring {
        private Lock updateLock = new ReentrantLock();
        private final long heapId;
        private final List<IoSession> sessions = new CopyOnWriteArrayList<IoSession>();
        private final AtomicLong updateIdGenerator = new AtomicLong();
        private UpdateProducingHeapListener listener;
        private final Heap heap;
        private final Queue<QueuedHeapChange> queuedChanges = new ConcurrentLinkedQueue<QueuedHeapChange>();
        private volatile boolean terminated;
        private final Map<String, SubscriptionDetails> subscriptions = new HashMap<String, SubscriptionDetails>();
        private final Map<IoSession, List<String>> sessionSubscriptions = new HashMap<IoSession, List<String>>();
        private final long instanceId = heapStateInstanceIdSource.incrementAndGet();

        @Override
        public SortedMap<String, List<String>> getSubscriptionIdsBySessionId() {
            SortedMap<String, List<String>> ret = new TreeMap<String, List<String>>();
            try {
                updateLock.lock();
                subTermLock.lock();

                for (IoSession key : sessionSubscriptions.keySet()) {
                    String sessionId = NioUtils.getSessionId(key);
                    ret.put(sessionId, sessionSubscriptions.get(key));
                }
            } finally {
                // make damn certain both locks are unlocked
                try {
                    updateLock.unlock();
                }
                finally {
                    subTermLock.unlock();
                }
            }

            return ret;
        }

        @Override
        public long getLastUpdateId() {
            return updateIdGenerator.get();
        }

        @Override
        public int getSubscriptionCount() {
            return subscriptions.size();
        }

        @Override
        public int getSessionCount() {
            return sessions.size();
        }

        public HeapState(Heap heap) {
            this.heap = heap;
            heapId = heapIdGenerator.incrementAndGet();
        }

        public Queue<QueuedHeapChange> getQueuedChanges() {
            return queuedChanges;
        }

        public Lock getUpdateLock() {
            return updateLock;
        }

        public long getHeapId() {
            return heapId;
        }

        public List<IoSession> getSessions() {
            return sessions;
        }

        public long getNextUpdateId() {
            return updateIdGenerator.incrementAndGet();
        }

        public void addSession(IoSession session) {
            if (!sessions.contains(session)) {
                sessions.add(session);
            }
        }

        public void removeSession(IoSession session) {
            sessions.remove(session);
        }

        public void setHeapListener(UpdateProducingHeapListener listener) {
            this.listener = listener;
        }

        public void removeListener() {
            heap.removeListener(listener);
        }

        public void terminate() {
            terminated = true;
        }

        public boolean isTerminated() {
            return terminated;
        }

        public String addSubscription(LogExtension logExtension, Subscription subscription, IoSession session) {
            SubscriptionDetails details = new SubscriptionDetails();
            details.logExtension = logExtension;
            details.subscription = subscription;

            String id = uuidGenerator.getNextUUID();
            subscriptions.put(id, details);
            List<String> subscriptionIds = sessionSubscriptions.get(session);
            if (subscriptionIds == null) {
                subscriptionIds = new ArrayList<String>();
                sessionSubscriptions.put(session, subscriptionIds);
            }
            subscriptionIds.add(id);

            logSubscriptionStart(id, logExtension);

            return id;
        }

        private void logSubscriptionStart(String subscriptionId, LogExtension extension) {
            log(subscriptionId, heap.getUri(), "SUBSCRIPTION_START", extension);
        }

        public void logSubscriptionEnd(String subscriptionId, LogExtension extension, Subscription.CloseReason reason) {
            if (reason == null) {
                String message = "Close reason not provided for subscription " + subscriptionId + " to heap " + heap.getUri() + " defaulting to INTERNAL_ERROR";
                logger.log(Level.WARNING, message, new IllegalStateException()); // so we can trace the source later..
                reason = INTERNAL_ERROR;
            }
            log(subscriptionId, heap.getUri(), reason.name(), extension);
        }

        private void log(String subscriptionId, String heapUri, String closeReason, LogExtension logExtension) {
            Object[] fieldsToLog = logExtension != null ? logExtension.getFieldsToLog() : null;
            ConnectedObjectLogEvent connectedObjectLogEvent = new ConnectedObjectLogEvent(
                "PUSH_SUBSCRIPTION-LOG",
                new Date(),
                subscriptionId,
                heapUri,
                closeReason
            );
            eventLogger.logEvent(connectedObjectLogEvent, fieldsToLog);
        }

        public void terminateSubscriptions(IoSession session, Subscription.CloseReason reason) {
            sessions.remove(session);
            // find each Subscription object for this session and delete all the subs
            List<String> ids = sessionSubscriptions.remove(session);
            if (ids != null) {
                for (String id : ids) {
                    SubscriptionDetails sub = subscriptions.remove(id);
                    try {
                        sub.subscription.close(reason);
                    } catch (Exception e) {
                        logger.log(Level.WARNING, "Error trying to close subscription");
                    }
                }
            }
        }

        public boolean hasSubscriptions() {
            return subscriptions.isEmpty();
        }

        public List<String> getSubscriptions(IoSession session) {
            return sessionSubscriptions.get(session);
        }

        public void terminateSubscription(IoSession session, String subscriptionId, Subscription.CloseReason reason) {
            sessionSubscriptions.get(session).remove(subscriptionId);
            try {
                SubscriptionDetails sub = subscriptions.remove(subscriptionId);
                if (reason != REQUESTED_BY_PUBLISHER) {
                    sub.subscription.close(reason);
                }
            } catch (Exception e) {
                logger.log(Level.WARNING, "Error trying to close subscription");
            }
        }

        // for testing
        Map<String, SubscriptionDetails> getSubscriptions() {
            return subscriptions;
        }

        public long getInstanceId() {
            return instanceId;
        }

        public Heap getHeap() {
            return heap;
        }

        // package private for testing
        class SubscriptionDetails {
            Subscription subscription;
            LogExtension logExtension;
        }
    }

    @ManagedAttribute(description = "Number of active heaps")
    public int getNumberOfHeaps() {
        if (heapUris != null) {
            return heapUris.size();
        }
        return 0;
    }

    @ManagedOperation(description = "Number of subscriptions for the given heap URI")
    public int getHeapSubscriptionCount(String heapUri) {
        if (heapStates != null) {
            final HeapState heapState = heapStates.get(heapUri);
            if (heapState != null) {
                return heapState.getSubscriptionCount();
            }
        }
        return -1;
    }

    @ManagedOperation(description = "Number of sessions subscribed for the given heap URI")
    public int getHeapSessionCount(String heapUri) {
        if (heapStates != null) {
            final HeapState heapState = heapStates.get(heapUri);
            if (heapState != null) {
                return heapState.getSessionCount();
            }
        }
        return -1;
    }

    @ManagedOperation(description = "Has the specified heap terminated")
    public boolean hasHeapTerminated(String heapUri) {
        if (heapStates != null) {
            final HeapState heapState = heapStates.get(heapUri);
            if (heapState != null) {
                return heapState.isTerminated();
            }
        }
        return true;
    }

    @ManagedOperation(description = "Last received update Id")
    public long getLastUpdateId(String heapUri) {
        if (heapStates != null) {
            final HeapState heapState = heapStates.get(heapUri);
            if (heapState != null) {
                return heapState.getLastUpdateId();
            }
        }
        return -1;
    }

    @ManagedOperation(description = "Number of updates queued for the specified heap")
    public long showNumOfQueuedChanges(String heapUri) {
        if (heapStates != null) {
            final HeapState heapState = heapStates.get(heapUri);
            if (heapState != null) {
                return heapState.getQueuedChanges().size();
            }
        }
        return -1;
    }

    @ManagedAttribute(description = "Number of pusher threads")
    public int getNumProcessingThreads() {
        return numProcessingThreads;
    }

    static class Multiset<T> {

        private final Map<T, Integer> map = new HashMap<T, Integer>();

        public boolean add(T val) {
            Integer count = nullToZero(map.get(val));
            map.put(val, count + 1);
            return true;
        }

        public boolean remove(T val) {
            Integer count = map.get(val);
            if (count == null) {
                return false;
            } else {
                if (count == 1) {
                    map.remove(val);
                } else {
                    map.put(val, count - 1);
                }
                return true;
            }
        }

        public int removeAll(T val) {
            return nullToZero(map.remove(val));
        }

        public int count(T val) {
            return nullToZero(map.get(val));
        }

        public Set<T> keySet() {
            return map.keySet();
        }

        public boolean isEmpty() {
            return map.isEmpty();
        }

        private int nullToZero(Integer i) {
            return i == null ? 0 : i;
        }
    }
}
TOP

Related Classes of com.betfair.cougar.transport.socket.PooledServerConnectedObjectManager

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.