/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* 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.intellij.util.messages.impl;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.Disposer;
import com.intellij.util.ConcurrencyUtil;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.lang.CompoundRuntimeException;
import com.intellij.util.messages.MessageBus;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.messages.Topic;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author max
*/
public class MessageBusImpl implements MessageBus {
private static final Logger LOG = Logger.getInstance("#com.intellij.util.messages.impl.MessageBusImpl");
private static final Comparator<MessageBusImpl> MESSAGE_BUS_COMPARATOR = new Comparator<MessageBusImpl>() {
@Override
public int compare(MessageBusImpl bus1, MessageBusImpl bus2) {
return ContainerUtil.compareLexicographically(bus1.myOrderRef.get(), bus2.myOrderRef.get());
}
};
@SuppressWarnings("SSBasedInspection") private final ThreadLocal<Queue<DeliveryJob>> myMessageQueue = createThreadLocalQueue();
/**
* Root's order is empty
* Child bus's order is its parent order plus one more element, an int that's bigger than that of all sibling buses that come before
* Sorting by these vectors lexicographically gives DFS order
*/
private final AtomicReference<List<Integer>> myOrderRef = new AtomicReference<List<Integer>>();
private final ConcurrentMap<Topic, Object> mySyncPublishers = ContainerUtil.newConcurrentMap();
private final ConcurrentMap<Topic, Object> myAsyncPublishers = ContainerUtil.newConcurrentMap();
/**
* This bus's subscribers
*/
private final ConcurrentMap<Topic, List<MessageBusConnectionImpl>> mySubscribers = ContainerUtil.newConcurrentMap();
/**
* Caches subscribers for this bus and its children or parent, depending on the topic's broadcast policy
*/
private final ConcurrentMap<Topic, List<MessageBusConnectionImpl>> mySubscriberCache = ContainerUtil.newConcurrentMap();
private final Deque<MessageBusImpl> myChildBuses = new LinkedBlockingDeque<MessageBusImpl>();
private final ConcurrentMap<List<Integer>, Boolean> myChildOrders = ContainerUtil.newConcurrentMap();
private static final Object NA = new Object();
private MessageBusImpl myParentBus;
//is used for debugging purposes
private final String myOwner;
private boolean myDisposed;
private final Disposable myConnectionDisposable;
public MessageBusImpl(@NotNull Object owner, @NotNull MessageBus parentBus) {
myOwner = owner + " of " + owner.getClass();
myConnectionDisposable = Disposer.newDisposable(myOwner);
myParentBus = (MessageBusImpl)parentBus;
myParentBus.onChildBusCreated(this);
LOG.assertTrue(myParentBus.myChildBuses.contains(this));
LOG.assertTrue(myOrderRef.get() != null);
}
private MessageBusImpl(Object owner) {
myOwner = owner + " of " + owner.getClass();
myConnectionDisposable = Disposer.newDisposable(myOwner);
myOrderRef.set(Collections.<Integer>emptyList());
}
@Override
public MessageBus getParent() {
return myParentBus;
}
@NotNull
private RootBus getRootBus() {
return myParentBus != null ? myParentBus.getRootBus() : asRoot();
}
private MessageBusImpl rootBus() { // return MessageBusImpl instead of RootBus to save one cast when accessing MessageBusImpl's private members
return getRootBus();
}
private RootBus asRoot() {
if (this instanceof RootBus) {
return (RootBus)this;
}
throw new AssertionError("Accessing disposed message bus " + this);
}
@Override
public String toString() {
return super.toString() + "; owner=" + myOwner + (myDisposed ? "; disposed" : "");
}
/**
* Notifies current bus that a child bus is created. Has two responsibilities:
* <ul>
* <li>stores given child bus in {@link #myChildBuses} collection</li>
* <li>
* calculates {@link #myOrderRef} for the given child bus
* </li>
* </ul>
* <p/>
* Thread-safe.
*
* @param childBus newly created child bus
*/
private void onChildBusCreated(final MessageBusImpl childBus) {
LOG.assertTrue(childBus.myParentBus == this);
// It's possible that new child bus objects are created concurrently, i.e. current method is called at the same
// time from different threads for different child bus objects. We had a race condition with that which resulted
// in NPE - https://youtrack.jetbrains.com/issue/UP-4322.
//
// The general idea is that we keep child buses orders in a concurrent set (myChildOrders) and use it as a synchronization
// point on new child registration, i.e. the algorithm is as follows:
// 1. Calculate an order for the given child bus on the currently registered buses basis;
// 2. Store given order in the myChildOrders if it doesn't contain such order yet;
// 3.1. Failure (such order is already there) - another child is being registered at the same time and the same order
// was calculated for it. Retry (go to 1.);
// 3.2. Success - store given bus at child buses collection.
// Note: it's important to respect that order on bus de-registration (onChildBusDisposed()) - first remove child bus
// from the buses collection, second remove its order from child orders.
List<Integer> childOrder = new ArrayList<Integer>(myOrderRef.get().size() + 1);
childOrder.addAll(myOrderRef.get());
childOrder.add(1); // Dummy holder, just to be able to call set(index) later
while (true) {
final MessageBusImpl lastChild = myChildBuses.peekLast();
final int lastChildIndex;
if (lastChild == null) {
lastChildIndex = 0;
}
else {
final List<Integer> lastChildOrder = lastChild.myOrderRef.get();
lastChildIndex = lastChildOrder.get(lastChildOrder.size() - 1);
}
if (lastChildIndex == Integer.MAX_VALUE) {
LOG.error("Too many child buses");
}
childOrder.set(childOrder.size() - 1, lastChildIndex + 1);
if (myChildOrders.putIfAbsent(childOrder, Boolean.TRUE) == null) {
break;
}
}
childBus.myOrderRef.set(childOrder);
myChildBuses.add(childBus);
rootBus().clearSubscriberCache();
}
private void onChildBusDisposed(final MessageBusImpl childBus) {
boolean removed = myChildBuses.remove(childBus);
myChildOrders.remove(childBus.myOrderRef.get());
Map<MessageBusImpl, Integer> map = getRootBus().myWaitingBuses.get();
if (map != null) map.remove(childBus);
rootBus().clearSubscriberCache();
LOG.assertTrue(removed);
}
private static class DeliveryJob {
public DeliveryJob(final MessageBusConnectionImpl connection, final Message message) {
this.connection = connection;
this.message = message;
}
public final MessageBusConnectionImpl connection;
public final Message message;
@NonNls
@Override
public String toString() {
return "{ DJob connection:" + connection + "; message: " + message + " }";
}
}
@Override
@NotNull
public MessageBusConnection connect() {
return connect(myConnectionDisposable);
}
@Override
@NotNull
public MessageBusConnection connect(@NotNull Disposable parentDisposable) {
checkNotDisposed();
final MessageBusConnection connection = new MessageBusConnectionImpl(this);
Disposer.register(parentDisposable, connection);
return connection;
}
@Override
@NotNull
@SuppressWarnings("unchecked")
public <L> L syncPublisher(@NotNull final Topic<L> topic) {
checkNotDisposed();
L publisher = (L)mySyncPublishers.get(topic);
if (publisher == null) {
final Class<L> listenerClass = topic.getListenerClass();
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
sendMessage(new Message(topic, method, args));
return NA;
}
};
publisher = (L)Proxy.newProxyInstance(listenerClass.getClassLoader(), new Class[]{listenerClass}, handler);
publisher = (L)ConcurrencyUtil.cacheOrGet(mySyncPublishers, topic, publisher);
}
return publisher;
}
@Override
@NotNull
@SuppressWarnings("unchecked")
public <L> L asyncPublisher(@NotNull final Topic<L> topic) {
checkNotDisposed();
L publisher = (L)myAsyncPublishers.get(topic);
if (publisher == null) {
final Class<L> listenerClass = topic.getListenerClass();
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
postMessage(new Message(topic, method, args));
return NA;
}
};
publisher = (L)Proxy.newProxyInstance(listenerClass.getClassLoader(), new Class[]{listenerClass}, handler);
publisher = (L)ConcurrencyUtil.cacheOrGet(myAsyncPublishers, topic, publisher);
}
return publisher;
}
@Override
public void dispose() {
checkNotDisposed();
Disposer.dispose(myConnectionDisposable);
Queue<DeliveryJob> jobs = myMessageQueue.get();
if (!jobs.isEmpty()) {
LOG.error("Not delivered events in the queue: " + jobs);
}
myMessageQueue.remove();
if (myParentBus != null) {
myParentBus.onChildBusDisposed(this);
myParentBus = null;
}
else {
asRoot().myWaitingBuses.remove();
}
myDisposed = true;
}
@Override
public boolean hasUndeliveredEvents(@NotNull Topic<?> topic) {
if (!isDispatchingAnything()) return false;
for (MessageBusConnectionImpl connection : getTopicSubscribers(topic)) {
if (connection.containsMessage(topic)) {
return true;
}
}
return false;
}
private boolean isDispatchingAnything() {
return getRootBus().myWaitingBuses.get() != null;
}
private void checkNotDisposed() {
if (myDisposed) {
LOG.error("Already disposed: " + this);
}
}
private void calcSubscribers(Topic topic, List<MessageBusConnectionImpl> result) {
final List<MessageBusConnectionImpl> topicSubscribers = mySubscribers.get(topic);
if (topicSubscribers != null) {
result.addAll(topicSubscribers);
}
Topic.BroadcastDirection direction = topic.getBroadcastDirection();
if (direction == Topic.BroadcastDirection.TO_CHILDREN) {
for (MessageBusImpl childBus : myChildBuses) {
childBus.calcSubscribers(topic, result);
}
}
if (direction == Topic.BroadcastDirection.TO_PARENT && myParentBus != null) {
myParentBus.calcSubscribers(topic, result);
}
}
private void postMessage(Message message) {
checkNotDisposed();
List<MessageBusConnectionImpl> topicSubscribers = getTopicSubscribers(message.getTopic());
if (!topicSubscribers.isEmpty()) {
for (MessageBusConnectionImpl subscriber : topicSubscribers) {
subscriber.getBus().myMessageQueue.get().offer(new DeliveryJob(subscriber, message));
subscriber.getBus().notifyPendingJobChange(1);
subscriber.scheduleMessageDelivery(message);
}
}
}
@NotNull
private List<MessageBusConnectionImpl> getTopicSubscribers(Topic topic) {
List<MessageBusConnectionImpl> topicSubscribers = mySubscriberCache.get(topic);
if (topicSubscribers == null) {
topicSubscribers = new SmartList<MessageBusConnectionImpl>();
calcSubscribers(topic, topicSubscribers);
mySubscriberCache.put(topic, topicSubscribers);
}
return topicSubscribers;
}
private void notifyPendingJobChange(int delta) {
ThreadLocal<SortedMap<MessageBusImpl, Integer>> ref = getRootBus().myWaitingBuses;
SortedMap<MessageBusImpl, Integer> map = ref.get();
if (map == null) {
ref.set(map = new TreeMap<MessageBusImpl, Integer>(MESSAGE_BUS_COMPARATOR));
}
Integer countObject = map.get(this);
int count = countObject == null ? 0 : countObject;
int newCount = count + delta;
if (newCount > 0) {
checkNotDisposed();
map.put(this, newCount);
}
else if (newCount == 0) {
map.remove(this);
}
else {
LOG.error("Negative job count: " + this);
}
}
private void sendMessage(Message message) {
pumpMessages();
postMessage(message);
pumpMessages();
}
private void pumpMessages() {
checkNotDisposed();
if (myParentBus != null) {
LOG.assertTrue(myParentBus.myChildBuses.contains(this));
myParentBus.pumpMessages();
}
else {
Map<MessageBusImpl, Integer> map = asRoot().myWaitingBuses.get();
if (map != null) {
Set<MessageBusImpl> buses = map.keySet();
if (!buses.isEmpty()) {
pumpWaitingBuses(map, new ArrayList<MessageBusImpl>(buses));
}
}
}
}
private static void pumpWaitingBuses(Map<MessageBusImpl, Integer> map, ArrayList<MessageBusImpl> buses) {
List<Throwable> exceptions = null;
for (MessageBusImpl bus : buses) {
if (!ensureAlive(map, bus)) continue;
exceptions = appendExceptions(exceptions, bus.doPumpMessages());
}
rethrowExceptions(exceptions);
}
private static List<Throwable> appendExceptions(List<Throwable> exceptions, List<Throwable> busExceptions) {
if (!busExceptions.isEmpty()) {
if (exceptions == null) exceptions = new SmartList<Throwable>();
exceptions.addAll(busExceptions);
}
return exceptions;
}
private static void rethrowExceptions(List<Throwable> exceptions) {
if (exceptions == null) return;
ProcessCanceledException pce = ContainerUtil.findInstance(exceptions, ProcessCanceledException.class);
if (pce != null) throw pce;
CompoundRuntimeException.throwIfNotEmpty(exceptions);
}
private static boolean ensureAlive(Map<MessageBusImpl, Integer> map, MessageBusImpl bus) {
if (bus.myDisposed) {
map.remove(bus);
LOG.error("Accessing disposed message bus " + bus);
return false;
}
return true;
}
private List<Throwable> doPumpMessages() {
Queue<DeliveryJob> queue = myMessageQueue.get();
List<Throwable> exceptions = null;
do {
DeliveryJob job = queue.poll();
if (job == null) break;
notifyPendingJobChange(-1);
try {
job.connection.deliverMessage(job.message);
}
catch (Throwable e) {
if (exceptions == null) {
exceptions = new SmartList<Throwable>();
}
exceptions.add(e);
}
}
while (true);
return exceptions == null ? Collections.<Throwable>emptyList() : exceptions;
}
void notifyOnSubscription(@NotNull MessageBusConnectionImpl connection, @NotNull Topic<?> topic) {
checkNotDisposed();
List<MessageBusConnectionImpl> topicSubscribers = mySubscribers.get(topic);
if (topicSubscribers == null) {
topicSubscribers = ContainerUtil.createLockFreeCopyOnWriteList();
topicSubscribers = ConcurrencyUtil.cacheOrGet(mySubscribers, topic, topicSubscribers);
}
topicSubscribers.add(connection);
rootBus().clearSubscriberCache();
}
private void clearSubscriberCache() {
mySubscriberCache.clear();
for (MessageBusImpl bus : myChildBuses) {
bus.clearSubscriberCache();
}
}
void notifyConnectionTerminated(final MessageBusConnectionImpl connection) {
for (List<MessageBusConnectionImpl> topicSubscribers : mySubscribers.values()) {
topicSubscribers.remove(connection);
}
if (myDisposed) return;
rootBus().clearSubscriberCache();
final Iterator<DeliveryJob> i = myMessageQueue.get().iterator();
while (i.hasNext()) {
final DeliveryJob job = i.next();
if (job.connection == connection) {
i.remove();
notifyPendingJobChange(-1);
}
}
}
void deliverSingleMessage() {
checkNotDisposed();
final DeliveryJob job = myMessageQueue.get().poll();
if (job == null) return;
notifyPendingJobChange(-1);
job.connection.deliverMessage(job.message);
}
@NotNull
static <T> ThreadLocal<Queue<T>> createThreadLocalQueue() {
return new ThreadLocal<Queue<T>>() {
@Override
protected Queue<T> initialValue() {
return new ConcurrentLinkedQueue<T>();
}
};
}
public static class RootBus extends MessageBusImpl {
/**
* Holds the counts of pending messages for all message buses in the hierarchy
* This field is null for non-root buses
* The map's keys are sorted by {@link #myOrderRef}
* <p>
* Used to avoid traversing the whole hierarchy when there are no messages to be sent in most of it
*/
private final ThreadLocal<SortedMap<MessageBusImpl, Integer>> myWaitingBuses = new ThreadLocal<SortedMap<MessageBusImpl, Integer>>();
public RootBus(@NotNull Object owner) {
super(owner);
}
}
}