/*
* Copyright 2011 JBoss, by Red Hat, Inc
*
* 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 org.jboss.errai.bus.server;
import static org.jboss.errai.bus.client.api.base.MessageBuilder.createConversation;
import static org.jboss.errai.bus.client.protocols.SecurityCommands.MessageNotDelivered;
import static org.jboss.errai.bus.client.util.ErrorHelper.handleMessageDeliveryFailure;
import static org.jboss.errai.bus.server.io.websockets.WebSocketTokenManager.verifyOneTimeToken;
import static org.jboss.errai.common.client.protocols.MessageParts.ReplyTo;
import static org.slf4j.LoggerFactory.getLogger;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.jboss.errai.bus.client.api.Message;
import org.jboss.errai.bus.client.api.MessageCallback;
import org.jboss.errai.bus.client.api.MessageListener;
import org.jboss.errai.bus.client.api.QueueSession;
import org.jboss.errai.bus.client.api.SubscribeListener;
import org.jboss.errai.bus.client.api.UnsubscribeListener;
import org.jboss.errai.bus.client.api.base.Capabilities;
import org.jboss.errai.bus.client.api.base.CommandMessage;
import org.jboss.errai.bus.client.api.base.ConversationMessage;
import org.jboss.errai.bus.client.api.base.MessageBuilder;
import org.jboss.errai.bus.client.api.base.NoSubscribersToDeliverTo;
import org.jboss.errai.bus.client.api.base.RuleDelegateMessageCallback;
import org.jboss.errai.bus.client.framework.BooleanRoutingRule;
import org.jboss.errai.bus.client.framework.BuiltInServices;
import org.jboss.errai.bus.client.framework.BusMonitor;
import org.jboss.errai.bus.client.framework.DeliveryPlan;
import org.jboss.errai.bus.client.framework.RoutingFlag;
import org.jboss.errai.bus.client.framework.Subscription;
import org.jboss.errai.bus.client.framework.SubscriptionEvent;
import org.jboss.errai.bus.client.protocols.BusCommands;
import org.jboss.errai.bus.server.api.MessageQueue;
import org.jboss.errai.bus.server.api.QueueCloseEvent;
import org.jboss.errai.bus.server.api.QueueClosedListener;
import org.jboss.errai.bus.server.api.ServerMessageBus;
import org.jboss.errai.bus.server.io.BufferHelper;
import org.jboss.errai.bus.server.io.buffers.BufferColor;
import org.jboss.errai.bus.server.io.buffers.TransmissionBuffer;
import org.jboss.errai.bus.server.io.websockets.WebSocketServer;
import org.jboss.errai.bus.server.io.websockets.WebSocketServerHandler;
import org.jboss.errai.bus.server.io.websockets.WebSocketTokenManager;
import org.jboss.errai.bus.server.service.ErraiConfigAttribs;
import org.jboss.errai.bus.server.service.ErraiServiceConfigurator;
import org.jboss.errai.bus.server.util.LocalContext;
import org.jboss.errai.common.client.protocols.MessageParts;
import org.jboss.errai.common.client.protocols.Resources;
import org.slf4j.Logger;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* The <tt>ServerMessageBusImpl</tt> implements the <tt>ServerMessageBus</tt>, making it possible for the server to
* send and receive messages
*
* @author Mike Brock
*/
@Singleton
public class ServerMessageBusImpl implements ServerMessageBus {
private final List<MessageListener> listeners = new ArrayList<MessageListener>();
private final TransmissionBuffer transmissionbuffer;
private final Map<String, DeliveryPlan> subscriptions = new ConcurrentHashMap<String, DeliveryPlan>();
private final Set<String> globalSubscriptions = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>());
private final Map<String, RemoteMessageCallback> remoteSubscriptions = new ConcurrentHashMap<String, RemoteMessageCallback>();
private final Map<QueueSession, MessageQueue> messageQueues = new ConcurrentHashMap<QueueSession, MessageQueue>();
private final Map<MessageQueue, List<Message>> deferredQueue = new ConcurrentHashMap<MessageQueue, List<Message>>();
private final Map<String, QueueSession> sessionLookup = new ConcurrentHashMap<String, QueueSession>();
private final List<SubscribeListener> subscribeListeners = new LinkedList<SubscribeListener>();
private final List<UnsubscribeListener> unsubscribeListeners = new LinkedList<UnsubscribeListener>();
private final List<QueueClosedListener> queueClosedListeners = new LinkedList<QueueClosedListener>();
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private static final Logger log = getLogger(ServerMessageBus.class);
private BusMonitor busMonitor;
private final Set<String> reservedNames = new HashSet<String>();
private final boolean webSocketServlet;
private final boolean webSocketServer;
/**
* Sets up the <tt>ServerMessageBusImpl</tt> with the configuration supplied. Also, initializes the bus' callback
* functions, scheduler, and monitor
* <p/>
* When deploying services on the server-side, it is possible to obtain references to the
* <tt>ErraiServiceConfigurator</tt> by declaring it as injection dependencies
*/
@Inject
public ServerMessageBusImpl(final ErraiServiceConfigurator config) {
this.webSocketServer = config
.getBooleanProperty(ErraiServiceConfigurator.ENABLE_WEB_SOCKET_SERVER);
final int webSocketPort;
final String webSocketPath;
webSocketServlet = ErraiConfigAttribs.WEBSOCKET_SERVLET_ENABLED.getBoolean(config);
if (webSocketServlet) {
webSocketPath = ErraiConfigAttribs.WEBSOCKET_SERVLET_CONTEXT_PATH.get(config);
webSocketPort = -1;
}
else {
webSocketPath = config.hasProperty(ErraiServiceConfigurator.WEB_SOCKET_URL) ?
config.getProperty(ErraiServiceConfigurator.WEB_SOCKET_URL) :
WebSocketServerHandler.WEBSOCKET_PATH;
webSocketPort = WebSocketServer.getWebSocketPort(config);
}
final Integer bufferSize = ErraiConfigAttribs.BUS_BUFFER_SIZE.getInt(config);
Integer segmentSize = ErraiConfigAttribs.BUS_BUFFER_SEGMENT_SIZE.getInt(config);
Integer segmentCount = ErraiConfigAttribs.BUS_BUFFER_SEGMENT_COUNT.getInt(config);
final String allocMode = ErraiConfigAttribs.BUS_BUFFER_ALLOCATION_MODE.get(config);
if (segmentSize == null) {
segmentSize = 8 * 1024;
}
else {
segmentSize = segmentSize * 1024;
}
if (bufferSize != null) {
segmentCount = (bufferSize * 1024 * 1024) / segmentSize;
}
else if (segmentCount == null) {
segmentCount = 4096;
}
final boolean directAlloc;
if (allocMode != null) {
if ("direct".equals(allocMode)) {
directAlloc = true;
}
else if ("heap".equals(allocMode)) {
directAlloc = false;
}
else {
throw new ErraiBootstrapFailure("unrecognized option for property: "
+ ErraiConfigAttribs.BUS_BUFFER_ALLOCATION_MODE.get(config));
}
}
else {
directAlloc = false;
}
TransmissionBuffer buffer;
if (directAlloc) {
try {
buffer = TransmissionBuffer.createDirect(segmentSize, segmentCount);
}
catch (OutOfMemoryError e) {
log.warn("could not allocate direct memory buffer. insufficient direct memory. increase the direct memory " +
"buffer size with the JVM argument: -XX:MaxDirectMemorySize=<size>");
log.warn("falling back to a heap allocated buffer.");
buffer = TransmissionBuffer.create(segmentSize, segmentCount);
}
}
else {
buffer = TransmissionBuffer.create(segmentSize, segmentCount);
}
transmissionbuffer = buffer;
/**
* Define the default ServerBus service used for intrabus communication.
*/
subscribe(BuiltInServices.ServerBus.name(), new MessageCallback() {
@Override
@SuppressWarnings({"unchecked", "SynchronizationOnLocalVariableOrMethodParameter"})
public void callback(final Message message) {
try {
final QueueSession session = getSession(message);
MessageQueueImpl queue = (MessageQueueImpl) messageQueues.get(session);
switch (BusCommands.valueOf(message.getCommandType())) {
case Heartbeat:
if (queue != null) {
queue.heartBeat();
}
break;
case RemoteSubscribe:
if (queue == null) return;
if (message.hasPart(MessageParts.SubjectsList)) {
for (final String subject : (List<String>) message.get(List.class, MessageParts.SubjectsList)) {
remoteSubscribe(session, queue, subject);
}
}
else {
remoteSubscribe(session, messageQueues.get(session),
message.get(String.class, MessageParts.Subject));
}
break;
case RemoteUnsubscribe:
if (queue == null) return;
remoteUnsubscribe(session, queue,
message.get(String.class, MessageParts.Subject));
break;
case FinishStateSync:
if (queue == null) return;
queue.finishInit();
drainDeferredDeliveryQueue(queue);
break;
case Disconnect:
if (queue == null) return;
synchronized (messageQueues) {
queue.stopQueue();
closeQueue(queue);
session.endSession();
}
break;
case Resend:
if (queue == null) return;
case ConnectToQueue: {
List<Message> deferred = null;
synchronized (messageQueues) {
if (messageQueues.containsKey(session)) {
final MessageQueue q = messageQueues.get(session);
synchronized (q) {
if (deferredQueue.containsKey(q)) {
deferred = deferredQueue.remove(q);
}
}
messageQueues.get(session).stopQueue();
}
queue = new MessageQueueImpl(transmissionbuffer, session);
addQueue(session, queue);
if (deferred != null) {
deferredQueue.put(queue, deferred);
}
remoteSubscribe(session, queue, BuiltInServices.ClientBus.name());
}
if (isMonitor()) {
busMonitor.notifyQueueAttached(session.getSessionId(), queue);
}
createConversation(message)
.toSubject(BuiltInServices.ClientBus.name())
.command(BusCommands.RemoteSubscribe)
.with(MessageParts.SubjectsList, new HashSet(globalSubscriptions))
.with(MessageParts.PriorityProcessing, "1")
.noErrorHandling().sendNowWith(ServerMessageBusImpl.this, false);
final Message msg = ConversationMessage.create(message)
.toSubject(BuiltInServices.ClientBus.name())
.command(BusCommands.CapabilitiesNotice);
final StringBuilder capabilitiesBuffer = new StringBuilder(25);
final boolean first;
if (ErraiServiceConfigurator.LONG_POLLING) {
capabilitiesBuffer.append(Capabilities.LongPollAvailable.name());
first = false;
}
else {
capabilitiesBuffer.append(Capabilities.NoLongPollAvailable.name());
first = false;
msg.set(MessageParts.PollFrequency, ErraiServiceConfigurator.HOSTED_MODE_TESTING ? 50 : 250);
}
if (webSocketServer || webSocketServlet) {
if (!first) {
capabilitiesBuffer.append(',');
}
capabilitiesBuffer.append(Capabilities.WebSockets.name());
/**
* Advertise where the client can find a websocket.
*/
final String webSocketURL;
final HttpServletRequest request
= message.getResource(HttpServletRequest.class, HttpServletRequest.class.getName());
if (webSocketServlet) {
webSocketURL = "ws://" + request.getHeader("Host") + webSocketPath;
}
else {
webSocketURL = "ws://" + request.getServerName() + ":" + webSocketPort + webSocketPath;
}
msg.set(MessageParts.WebSocketURL, webSocketURL);
msg.set(MessageParts.WebSocketToken, WebSocketTokenManager.getNewOneTimeToken(session));
}
msg.set(MessageParts.CapabilitiesFlags, capabilitiesBuffer.toString());
send(msg, false);
createConversation(message)
.toSubject(BuiltInServices.ClientBus.name())
.command(BusCommands.FinishStateSync)
.with(MessageParts.ConnectionSessionKey, queue.getSession().getSessionId())
.noErrorHandling().sendNowWith(ServerMessageBusImpl.this, false);
break;
}
case WebsocketChannelVerify:
if (message.hasPart(MessageParts.WebSocketToken)) {
if (verifyOneTimeToken(session, message.get(String.class, MessageParts.WebSocketToken))) {
final LocalContext localContext = LocalContext.get(session);
localContext.setAttribute(WebSocketServerHandler.SESSION_ATTR_WS_STATUS,
WebSocketServerHandler.WEBSOCKET_ACTIVE);
createConversation(message)
.toSubject(BuiltInServices.ClientBus.name())
.command(BusCommands.WebsocketChannelOpen)
.done().sendNowWith(ServerMessageBusImpl.this, false);
}
}
break;
}
}
catch (Throwable t) {
t.printStackTrace();
}
}
});
addSubscribeListener(new SubscribeListener() {
@Override
public void onSubscribe(final SubscriptionEvent event) {
if (event.isLocalOnly() || event.isRemote() || event.getSubject().startsWith("local:")) return;
MessageBuilder.createMessage()
.toSubject(BuiltInServices.ClientBus.name())
.command(BusCommands.RemoteSubscribe)
.with(MessageParts.Subject, event.getSubject())
.noErrorHandling().sendGlobalWith(ServerMessageBusImpl.this);
}
});
addUnsubscribeListener(new UnsubscribeListener() {
@Override
public void onUnsubscribe(final SubscriptionEvent event) {
if (event.isLocalOnly() || event.isRemote() || event.getSubject().startsWith("local:")) return;
if (messageQueues.isEmpty()) return;
MessageBuilder.createMessage()
.toSubject(BuiltInServices.ClientBus.name())
.command(BusCommands.RemoteUnsubscribe)
.with(MessageParts.Subject, event.getSubject())
.noErrorHandling().sendGlobalWith(ServerMessageBusImpl.this);
}
});
scheduler.scheduleAtFixedRate(new Runnable() {
int runCount = 0;
boolean lastWasEmpty = false;
@Override
public void run() {
runCount++;
boolean houseKeepingPerformed = false;
final List<MessageQueue> endSessions = new LinkedList<MessageQueue>();
int paged = 0, killed = 0;
while (!houseKeepingPerformed) {
try {
final Iterator<MessageQueue> iter = ServerMessageBusImpl.this.messageQueues.values().iterator();
MessageQueue q;
while (iter.hasNext()) {
if ((q = iter.next()).isStale()) {
iter.remove();
endSessions.add(q);
killed++;
}
else if (q.isDowngradeCandidate()) {
if (!q.pageWaitingToDisk()) {
paged++;
}
}
}
houseKeepingPerformed = true;
}
catch (ConcurrentModificationException cme) {
// fall-through and try again.
}
}
if (paged > 0 || killed > 0) {
log.debug("[bus] killed " + killed + " sessions and paged out " + paged + " queues");
}
for (final MessageQueue ref : endSessions) {
for (final String subject : new HashSet<String>(ServerMessageBusImpl.this.remoteSubscriptions.keySet())) {
ServerMessageBusImpl.this.remoteUnsubscribe(ref.getSession(), ref, subject);
}
ServerMessageBusImpl.this.closeQueue(ref);
ref.getSession().endSession();
deferredQueue.remove(ref);
ref.discard();
}
final BufferStatus stat = bufferStatus();
if (stat.getFree() == 1.0f) {
if (lastWasEmpty) {
return;
}
else {
lastWasEmpty = true;
}
}
else {
lastWasEmpty = false;
}
log.debug("[bus] buffer status [freebytes: " + stat.getFreeBytes()
+ " (" + (stat.getFree() * 100) + "%) tail rng: " + stat.getTailRange() + "; actv tails: "
+ stat.getActiveTails() + "]");
if (stat.getFree() < 0.50f) {
log.debug("[bus] high load condition detected!");
}
}
@Override
public String toString() {
return "Bus Housekeeper";
}
}, 8, 8, TimeUnit.SECONDS);
}
private static class BufferStatus {
private final int freeBytes;
private final int tailRange;
private final int activeTails;
private final float free;
private BufferStatus(final int freeBytes, final int tailRange, final int activeTails, final float free) {
this.freeBytes = freeBytes;
this.tailRange = tailRange;
this.activeTails = activeTails;
this.free = free;
}
public int getFreeBytes() {
return freeBytes;
}
public int getTailRange() {
return tailRange;
}
public int getActiveTails() {
return activeTails;
}
public float getFree() {
return free;
}
}
private BufferStatus bufferStatus() {
final int headBytes = transmissionbuffer.getHeadPositionBytes();
final int bufSize = transmissionbuffer.getBufferSize();
long lowTail = -1;
long highTail = -1;
int activeTails = 0;
final int free;
long lowSegBytes = 0;
long highSegBytes = 0;
for (final MessageQueue q : messageQueues.values()) {
activeTails++;
final long seq = q.getCurrentBufferSequenceNumber();
if (lowTail == -1) {
lowTail = highTail = seq;
}
else {
if (seq > highTail) highTail = seq;
if (seq < lowTail) lowTail = seq;
}
}
if (activeTails > 0) {
lowSegBytes = (lowTail % transmissionbuffer.getBufferSize()) * transmissionbuffer.getSegmentSize();
highSegBytes = (highTail % transmissionbuffer.getBufferSize()) * transmissionbuffer.getSegmentSize();
if (lowSegBytes < headBytes) {
free = (int) ((bufSize - headBytes) + lowSegBytes);
}
else if (lowSegBytes > headBytes) {
free = (int) (lowSegBytes - bufSize);
}
else {
free = bufSize;
}
}
else {
free = bufSize;
}
return new BufferStatus(free, (int) (highSegBytes - lowSegBytes), activeTails, ((float) free) / bufSize);
}
private void addQueue(final QueueSession session, final MessageQueue queue) {
messageQueues.put(session, queue);
sessionLookup.put(session.getSessionId(), session);
}
/**
* Configures the server message bus with the specified <tt>ErraiServiceConfigurator</tt>.
* Presently there are no configurable parameters.
*/
@Override
public void configure(final ErraiServiceConfigurator config) {
// no configuration in current implementation
}
/**
* Sends a message globally to all subscriptions containing the same subject as the specified message.
*
* @param message
* - The message to be sent.
*/
@Override
public void sendGlobal(final Message message) {
message.commit();
final String subject = message.getSubject();
if (!subscriptions.containsKey(subject) && !remoteSubscriptions.containsKey(subject)) {
delayOrFail(message, new Runnable() {
@Override
public void run() {
sendGlobal(message);
}
});
return;
}
if (!fireGlobalMessageListeners(message)) {
if (message.hasPart(ReplyTo) && message.hasResource(Resources.Session.name())) {
/**
* Inform the sender that we did not dispatchGlobal the message.
*/
final Map<String, Object> rawMsg = new HashMap<String, Object>();
rawMsg.put(MessageParts.CommandType.name(), MessageNotDelivered.name());
try {
enqueueForDelivery(getQueueByMessage(message), CommandMessage.createWithParts(rawMsg));
}
catch (NoSubscribersToDeliverTo nstdt) {
handleMessageDeliveryFailure(this, message, "No subscribers to deliver to", nstdt, false);
}
}
return;
}
if (isMonitor()) {
if (message.isFlagSet(RoutingFlag.FromRemote)) {
busMonitor.notifyIncomingMessageFromRemote(
message.getResource(QueueSession.class, Resources.Session.name()).getSessionId(), message);
}
else {
if (subscriptions.containsKey(subject)) {
busMonitor.notifyInBusMessage(message);
}
}
}
if (subscriptions.containsKey(subject)) {
subscriptions.get(subject).deliver(message);
}
else if (subscriptions.containsKey("local:".concat(subject))) {
subscriptions.get("local:".concat(subject)).deliver(message);
}
}
private void delayOrFail(final Message message, final Runnable deliveryTaskRunnable) {
if (message.isFlagSet(RoutingFlag.RetryDelivery)
&& message.getResource(Integer.class, Resources.RetryAttempts.name()) > 3) {
final NoSubscribersToDeliverTo ntdt = new NoSubscribersToDeliverTo(message.getSubject());
if (message.getErrorCallback() != null) {
message.getErrorCallback().error(message, ntdt);
}
throw ntdt;
}
message.setFlag(RoutingFlag.RetryDelivery);
if (!message.hasResource(Resources.RetryAttempts.name())) {
message.setResource(Resources.RetryAttempts.name(), 0);
}
message.setResource(Resources.RetryAttempts.name(),
message.getResource(Integer.class, Resources.RetryAttempts.name()) + 1);
getScheduler().schedule(new Runnable() {
@Override
public void run() {
deliveryTaskRunnable.run();
}
}, 250, TimeUnit.MILLISECONDS);
}
/**
* Sends the <tt>message</tt>
*
* @param message
* - the message to send
*/
@Override
public void send(final Message message) {
message.commit();
if (message.hasResource(Resources.Session.name())) {
message.setFlag(RoutingFlag.NonGlobalRouting);
send(getQueueByMessage(message), message, true);
}
else if (message.hasPart(MessageParts.SessionID)) {
message.setFlag(RoutingFlag.NonGlobalRouting);
send(getQueueBySession(message.get(String.class, MessageParts.SessionID)), message, true);
}
else {
sendGlobal(message);
}
}
/**
* Parses the message appropriately and enqueues it for delivery
*
* @param message
* - the message to be sent
* @param fireListeners
* - true if all listeners attached should be notified of delivery
*/
@Override
public void send(final Message message, final boolean fireListeners) {
message.commit();
if (!message.hasResource(Resources.Session.name())) {
handleMessageDeliveryFailure(this, message,
"cannot automatically route message. no session contained in message.", null, false);
}
final MessageQueue queue = getQueue(getSession(message));
if (queue == null) {
handleMessageDeliveryFailure(this, message,
"cannot automatically route message. no session contained in message.", null, false);
}
send(message.hasPart(MessageParts.SessionID) ? getQueueBySession(message.get(String.class, MessageParts.SessionID)) :
getQueueByMessage(message), message, fireListeners);
}
private void send(final MessageQueue queue, final Message message, final boolean fireListeners) {
try {
if (fireListeners && !fireGlobalMessageListeners(message)) {
if (message.hasPart(ReplyTo)) {
final Map<String, Object> rawMsg = new HashMap<String, Object>();
rawMsg.put(MessageParts.CommandType.name(), MessageNotDelivered.name());
enqueueForDelivery(queue, CommandMessage.createWithParts(rawMsg));
}
return;
}
if (isMonitor()) {
busMonitor.notifyOutgoingMessageToRemote(queue.getSession().getSessionId(), message);
}
enqueueForDelivery(queue, message);
}
catch (NoSubscribersToDeliverTo nstdt) {
// catch this so we can get a full trace
handleMessageDeliveryFailure(this, message, "No subscribers to deliver to", nstdt, false);
}
}
private void enqueueForDelivery(final MessageQueue queue, final Message message) {
try {
if (queue != null && isAnyoneListening(queue, message.getSubject())) {
queue.offer(message);
}
else {
if (queue != null && !queue.isInitialized()) {
deferDelivery(queue, message);
}
else {
delayOrFail(message, new Runnable() {
@Override
public void run() {
enqueueForDelivery(queue, message);
}
});
}
}
}
catch (QueueUnavailableException e) {
closeQueue(queue);
}
catch (IOException e) {
throw new RuntimeException("failed to enqueue message for delivery", e);
}
}
@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter"})
private void deferDelivery(final MessageQueue queue, final Message message) {
synchronized (queue) {
if (!deferredQueue.containsKey(queue)) deferredQueue.put(queue, new ArrayList<Message>());
deferredQueue.get(queue).add(message);
}
}
@SuppressWarnings({"SynchronizationOnLocalVariableOrMethodParameter"})
private void drainDeferredDeliveryQueue(final MessageQueue queue) {
try {
synchronized (queue) {
if (deferredQueue.containsKey(queue)) {
final List<Message> deferredMessages = deferredQueue.get(queue);
final Iterator<Message> dmIter = deferredMessages.iterator();
Message m;
while (dmIter.hasNext()) {
if ((m = dmIter.next()).hasPart(MessageParts.PriorityProcessing.toString())) {
queue.offer(m);
dmIter.remove();
}
}
for (final Message message : deferredQueue.get(queue)) {
queue.offer(message);
}
deferredQueue.remove(queue);
}
}
}
catch (IOException e) {
throw new RuntimeException("error draining deferred delivery queue", e);
}
}
/**
* Gets the queue corresponding to the session id given
*
* @param session
* - the session id of the queue
*
* @return the message queue
*/
@Override
public MessageQueue getQueue(final QueueSession session) {
return messageQueues.get(session);
}
/**
* Closes the queue with <tt>sessionId</tt>
*
* @param sessionId
* - the session context of the queue to close
*/
@Override
public void closeQueue(final String sessionId) {
closeQueue(getQueueBySession(sessionId));
}
/**
* Closes the message queue
*
* @param queue
* - the message queue to close
*/
@Override
public void closeQueue(final MessageQueue queue) {
messageQueues.values().remove(queue);
sessionLookup.values().remove(queue.getSession());
for (Iterator<RemoteMessageCallback> iterator = remoteSubscriptions.values().iterator(); iterator.hasNext(); ) {
final RemoteMessageCallback cb = iterator.next();
cb.removeQueue(queue);
if (cb.getQueueCount() == 0) {
iterator.remove();
}
}
fireQueueCloseListeners(new QueueCloseEvent(queue));
}
/**
* Adds a rule for a specific subscription. The <tt>BooleanRoutingRule</tt> determines if a message should
* be routed based on the already specified rules or not.
*
* @param subject
* - the subject of the subscription
* @param rule
* - the <tt>BooleanRoutingRule</tt> instance specifying the routing rules
*/
@Override
public void addRule(final String subject, final BooleanRoutingRule rule) {
final DeliveryPlan plan = subscriptions.get(subject);
if (plan == null) {
throw new RuntimeException("no such subject: " + subject);
}
subscriptions.put(subject, new RuleDelegateMessageCallback(plan, rule));
}
/**
* Adds a subscription
*
* @param subject
* - the subject to subscribe to
* @param receiver
* - the callback function called when a message is dispatched
*/
@Override
public Subscription subscribe(final String subject, final MessageCallback receiver) {
if (reservedNames.contains(subject))
throw new IllegalArgumentException("cannot modify or subscribe to reserved service: " + subject);
final DeliveryPlan plan = createOrAddDeliveryPlan(subject, receiver);
globalSubscriptions.add(subject);
fireSubscribeListeners(new SubscriptionEvent(false, null, plan.getTotalReceivers(), true, subject));
return new Subscription() {
@Override
public void remove() {
if (removeFromDeliveryPlan(subject, receiver).getTotalReceivers() == 0) {
globalSubscriptions.remove(subject);
subscriptions.remove(subject);
}
else {
boolean nonRemote = true;
for (final MessageCallback callback : plan.getDeliverTo()) {
if (!(callback instanceof RemoteMessageCallback)) {
nonRemote = false;
break;
}
}
if (nonRemote) {
globalSubscriptions.remove(subject);
subscriptions.remove(subject);
}
}
}
};
}
@Override
public Subscription subscribeLocal(final String subject, final MessageCallback receiver) {
if (reservedNames.contains(subject))
throw new IllegalArgumentException("cannot modify or subscribe to reserved service: " + subject);
final String toSubscribe = "local:".concat(subject);
final DeliveryPlan plan = createOrAddDeliveryPlan(toSubscribe, receiver);
fireSubscribeListeners(
new SubscriptionEvent(false, false, true, true, plan.getTotalReceivers(), "InBus", toSubscribe)
);
return new Subscription() {
@Override
public void remove() {
removeFromDeliveryPlan(toSubscribe, receiver);
}
};
}
private DeliveryPlan createOrAddDeliveryPlan(final String subject, final MessageCallback receiver) {
if (receiver == null) {
throw new NullPointerException("message callback cannot but null");
}
DeliveryPlan plan = subscriptions.get(subject);
if (plan == null) {
subscriptions.put(subject, plan = DeliveryPlan.newDeliveryPlan(receiver));
}
else {
subscriptions.put(subject, plan.newDeliveryPlanWith(receiver));
}
return plan;
}
private DeliveryPlan removeFromDeliveryPlan(final String subject, final MessageCallback receiver) {
DeliveryPlan plan = subscriptions.get(subject);
if (plan != null) {
subscriptions.put(subject, plan = plan.newDeliveryPlanWithOut(receiver));
fireUnsubscribeListeners(
new SubscriptionEvent(false, "InBus", plan.getTotalReceivers(), false, subject));
}
return plan;
}
private static final Set<String> broadcastExclusionSet = new HashSet<String>() {
{
add(BuiltInServices.ClientBus.name());
add(BuiltInServices.ClientBusErrors.name());
}
};
/**
* Adds a new remote subscription and fires subscription listeners
*
* @param sessionContext
* - session context of queue
* @param queue
* - the message queue
* @param subject
* - the subject to subscribe to
*/
public void remoteSubscribe(final QueueSession sessionContext, final MessageQueue queue, final String subject) {
if (subject == null) return;
boolean isNew = false;
RemoteMessageCallback rmc;
synchronized (remoteSubscriptions) {
rmc = remoteSubscriptions.get(subject);
if (rmc == null) {
rmc = new RemoteMessageCallback(!broadcastExclusionSet.contains(subject), subject);
rmc.addQueue(queue);
isNew = true;
remoteSubscriptions.put(subject, rmc);
createOrAddDeliveryPlan(subject, rmc);
}
else if (!rmc.contains(queue)) {
rmc.addQueue(queue);
}
}
fireSubscribeListeners(
new SubscriptionEvent(true, sessionContext.getSessionId(), rmc.getQueueCount(), isNew, subject)
);
}
public class RemoteMessageCallback implements MessageCallback {
private final String svc;
private final Set<MessageQueue> queues = Collections.newSetFromMap(new ConcurrentHashMap<MessageQueue, Boolean>());
private final boolean broadcastable;
private final AtomicInteger totalBroadcasted = new AtomicInteger();
public RemoteMessageCallback(final boolean broadcastable, final String svc) {
this.broadcastable = broadcastable;
this.svc = svc;
}
@Override
public void callback(final Message message) {
// do not pipeline if this message is addressed to a specified session.
if (broadcastable && !message.isFlagSet(RoutingFlag.NonGlobalRouting)) {
// all queues are listening to this subject. therefore we can save memory and time by
// writing to the broadcast color on the buffer
try {
if (queues.isEmpty()) return;
BufferHelper.encodeAndWrite(transmissionbuffer, BufferColor.getAllBuffersColor(), message);
for (final MessageQueue q : queues) {
q.wake();
}
if (log.isDebugEnabled() && totalBroadcasted.incrementAndGet() % 1000 == 0) {
log.debug(totalBroadcasted.get() + " messages have been broadcasted to service: " + svc);
}
}
catch (IOException e) {
throw new RuntimeException("transmission error", e);
}
}
else {
for (final MessageQueue q : queues) {
send(q, message, true);
}
}
}
public void addQueue(final MessageQueue queue) {
queues.add(queue);
}
public void removeQueue(final MessageQueue queue) {
queues.remove(queue);
}
public Collection<MessageQueue> getQueues() {
return queues;
}
public int getQueueCount() {
return queues.size();
}
public boolean contains(final MessageQueue queue) {
return queues.contains(queue);
}
}
/**
* Unsubscribes a remote subscription and fires the appropriate listeners
*
* @param sessionContext
* - session context of queue
* @param queue
* - the message queue
* @param subject
* - the subject to unsubscribe from
*/
public void remoteUnsubscribe(final QueueSession sessionContext, final MessageQueue queue, final String subject) {
if (!remoteSubscriptions.containsKey(subject)) {
return;
}
final RemoteMessageCallback rmc = remoteSubscriptions.get(subject);
rmc.removeQueue(queue);
try {
fireUnsubscribeListeners(new SubscriptionEvent(true, rmc.getQueueCount() == 0, false, false, rmc.getQueueCount(),
sessionContext.getSessionId(), subject));
}
catch (Exception e) {
e.printStackTrace();
}
}
/**
* Unsubscribe all subscriptions attached to <tt>subject</tt>
*
* @param subject
* - the subject to unsubscribe from
*/
@Override
public void unsubscribeAll(final String subject) {
if (reservedNames.contains(subject))
throw new IllegalArgumentException("Attempt to modify lockdown service: " + subject);
subscriptions.remove(subject);
globalSubscriptions.remove(subject);
fireUnsubscribeListeners(new SubscriptionEvent(false, null, 0, false, subject));
}
/**
* Checks if a subscription exists for <tt>subject</tt>
*
* @param subject
* - the subject to search the subscriptions for
*
* @return true if a subscription exists
*/
@Override
public boolean isSubscribed(final String subject) {
return subscriptions.containsKey(subject);
}
private boolean isAnyoneListening(final MessageQueue queue, final String subject) {
return subscriptions.containsKey(subject) ||
(remoteSubscriptions.containsKey(subject) && remoteSubscriptions.get(subject).contains(queue));
}
@Override
public boolean hasRemoteSubscriptions(final String subject) {
return remoteSubscriptions.containsKey(subject);
}
@Override
public boolean hasRemoteSubscription(final String sessionId, final String subject) {
return remoteSubscriptions.containsKey(subject) && remoteSubscriptions.get(subject)
.contains(getQueueBySession(sessionId));
}
private boolean fireGlobalMessageListeners(final Message message) {
boolean allowContinue = true;
for (final MessageListener listener : listeners) {
if (!listener.handleMessage(message)) {
allowContinue = false;
}
}
return allowContinue;
}
private void fireSubscribeListeners(final SubscriptionEvent event) {
if (isMonitor()) {
busMonitor.notifyNewSubscriptionEvent(event);
}
synchronized (subscribeListeners) {
event.setDisposeListener(false);
for (Iterator<SubscribeListener> iter = subscribeListeners.iterator(); iter.hasNext(); ) {
iter.next().onSubscribe(event);
if (event.isDisposeListener()) {
iter.remove();
event.setDisposeListener(false);
}
}
}
}
private void fireUnsubscribeListeners(final SubscriptionEvent event) {
if (isMonitor()) {
busMonitor.notifyUnSubcriptionEvent(event);
}
synchronized (unsubscribeListeners) {
event.setDisposeListener(false);
for (Iterator<UnsubscribeListener> iter = unsubscribeListeners.iterator(); iter.hasNext(); ) {
iter.next().onUnsubscribe(event);
if (event.isDisposeListener()) {
iter.remove();
event.setDisposeListener(false);
}
}
}
}
private void fireQueueCloseListeners(final QueueCloseEvent event) {
if (isMonitor()) {
busMonitor.notifyQueueDetached(event.getQueue().getSession().getSessionId(), event.getQueue());
}
synchronized (queueClosedListeners) {
event.setDisposeListener(false);
for (Iterator<QueueClosedListener> iter = queueClosedListeners.iterator(); iter.hasNext(); ) {
iter.next().onQueueClosed(event);
if (event.isDisposeListener()) {
iter.remove();
event.setDisposeListener(false);
}
}
}
}
/**
* Adds a global listener
*
* @param listener
* - global listener to add
*/
@Override
public void addGlobalListener(final MessageListener listener) {
synchronized (listeners) {
listeners.add(listener);
}
}
/**
* Adds subscription listener
*
* @param listener
* - subscription listener to add
*/
@Override
public void addSubscribeListener(final SubscribeListener listener) {
synchronized (subscribeListeners) {
subscribeListeners.add(listener);
}
}
/**
* Adds unsubscription listener
*
* @param listener
* - adds an unsubscription listener
*/
@Override
public void addUnsubscribeListener(final UnsubscribeListener listener) {
synchronized (unsubscribeListeners) {
unsubscribeListeners.add(listener);
}
}
private static QueueSession getSession(final Message message) {
return message.getResource(QueueSession.class, Resources.Session.name());
}
private MessageQueue getQueueByMessage(final Message message) {
final MessageQueue queue = getQueue(getSession(message));
if (queue == null) {
throw new QueueUnavailableException("no queue available to send. (queue or session may have expired): " +
"(session id: " + getSession(message).getSessionId() + ")");
}
else {
return queue;
}
}
@Override
public void associateNewQueue(final QueueSession oldSession, final QueueSession newSession) {
sessionLookup.put(newSession.getSessionId(), oldSession);
messageQueues.put(newSession, getQueue(oldSession));
}
@Override
public MessageQueue getQueueBySession(final String sessionId) {
final QueueSession session = sessionLookup.get(sessionId);
if (session == null) {
throw new QueueUnavailableException("no queue for sessionId=" + sessionId);
}
return getQueue(session);
}
@Override
public QueueSession getSessionBySessionId(final String id) {
return sessionLookup.get(id);
}
/**
* Gets all the message queues
*
* @return a map of the message queues that exist
*/
@Override
public Map<QueueSession, MessageQueue> getMessageQueues() {
return messageQueues;
}
/**
* Gets the scheduler being used within this message bus for housekeeping and
* other periodic or deferred tasks.
*
* @return the scheduler
*/
@Override
public ScheduledExecutorService getScheduler() {
return scheduler;
}
@Override
public void addQueueClosedListener(final QueueClosedListener listener) {
synchronized (queueClosedListeners) {
queueClosedListeners.add(listener);
}
}
@Override
public List<MessageCallback> getReceivers(final String subject) {
return Collections.unmodifiableList(Arrays.asList(subscriptions.get(subject).getDeliverTo()));
}
private boolean isMonitor() {
return this.busMonitor != null;
}
@Override
public void attachMonitor(final BusMonitor monitor) {
if (this.busMonitor != null) {
log.warn("new monitor attached, but a monitor was already attached: old monitor has been detached.");
}
this.busMonitor = monitor;
for (final Map.Entry<QueueSession, MessageQueue> entry : messageQueues.entrySet()) {
busMonitor.notifyQueueAttached(entry.getKey().getSessionId(), entry.getValue());
}
for (final String subject : subscriptions.keySet()) {
busMonitor.notifyNewSubscriptionEvent(new SubscriptionEvent(false, "None", 1, false, subject));
}
for (final Map.Entry<String, RemoteMessageCallback> entry : remoteSubscriptions.entrySet()) {
for (final MessageQueue queue : entry.getValue().getQueues()) {
busMonitor.notifyNewSubscriptionEvent(
new SubscriptionEvent(true, queue.getSession().getSessionId(), 1, false, entry.getKey())
);
}
}
monitor.attach(this);
}
@Override
public void stop() {
for (final MessageQueue queue : messageQueues.values()) {
queue.stopQueue();
}
scheduler.shutdown();
transmissionbuffer.clear();
subscriptions.clear();
remoteSubscriptions.clear();
deferredQueue.clear();
sessionLookup.clear();
}
public void finishInit() {
reservedNames.addAll(subscriptions.keySet());
}
}