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