All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.eventbus.impl.EventBusImpl Maven / Gradle / Ivy

package com.netflix.eventbus.impl;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.base.Throwables;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import com.netflix.config.DynamicIntProperty;
import com.netflix.config.DynamicPropertyFactory;
import com.netflix.eventbus.spi.CatchAllSubscriber;
import com.netflix.eventbus.spi.EventBus;
import com.netflix.eventbus.spi.EventCreator;
import com.netflix.eventbus.spi.EventFilter;
import com.netflix.eventbus.spi.InvalidSubscriberException;
import com.netflix.eventbus.spi.Subscribe;
import com.netflix.eventbus.spi.SubscriberConfigProvider;
import com.netflix.eventbus.spi.SubscriberInfo;
import com.netflix.eventbus.utils.EventBusUtils;
import com.netflix.servo.monitor.Stopwatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;

/**
 * An implementation of {@link EventBus}. This implementation is based on the google eventbus
 * {@link com.google.common.eventbus.EventBus} but the absence of descent extension points forces us to inspire but
 * not extend that implementation. The critical parts that drove us towards this approach are:
 * 
  • Inability to add a filter for publisher/subscriber. We would need to copy part of code to do this.
  • Inability to easily create custom handler wrappers for our style of async dispatch.
* * @author Nitesh Kant ([email protected]) */ public class EventBusImpl implements EventBus { private static final Logger LOGGER = LoggerFactory.getLogger(EventBusImpl.class); static final DynamicIntProperty STATS_COLLECTION_DURATION_MILLIS = DynamicPropertyFactory.getInstance().getIntProperty("eventbus.stats.collection.duration.millis", 60*1000); /** * Event type VS consumers map. Any event for which consumers are required, must query this collection for all the * interfaces & classes the event implements/extends, directly or indirectly, typically by calling * {@link EventBusImpl#getAllTypesForAnEvent(Object)} */ private final SetMultimap, EventConsumer> consumersByEventType = Multimaps.newSetMultimap(new ConcurrentHashMap, Collection>(), new Supplier>() { @Override public Set get() { return new CopyOnWriteArraySet(); } }); /** * This is an index to aid in unregister of subscribers. This holds the mapping between, the class of the subscriber * and the list of consumers it is registered with. During unregister we get a list of consumers for that class * and then for every consumer get the event class it listens for and then delete the same from * {@link EventBusImpl#consumersByEventType} */ private final ConcurrentHashMap, CopyOnWriteArrayList> consumersBySubscriberClass = new ConcurrentHashMap, CopyOnWriteArrayList>(); /** * Filters attached to an event type. */ private final SetMultimap, EventFilter> eventTypeVsFilters = Multimaps.newSetMultimap(new ConcurrentHashMap, Collection>(), new Supplier>() { @Override public Set get() { return new CopyOnWriteArraySet(); } }); /** * Cache of the class hierarchy for an event type. This optimizes multiple publishing of the same event type which * typically will be the case. */ private static LoadingCache, Set>> eventHierarchyCache = CacheBuilder.newBuilder() .weakKeys() .build(new CacheLoader, Set>>() { @Override public Set> load(Class concreteClass) throws Exception { List> parents = Lists.newLinkedList(); Set> classes = Sets.newHashSet(); parents.add(concreteClass); while (!parents.isEmpty()) { Class clazz = parents.remove(0); classes.add(clazz); Class parent = clazz.getSuperclass(); if (parent != null && !parent.equals(Object.class)) { // Do not allow subs on java.lang.Object parents.add(parent); } Collections.addAll(parents, clazz.getInterfaces()); } return classes; } }); private ConsumerQueueSupplier consumerQueueSupplier = new DefaultConsumerQueueSupplier(); private EventBusStats stats = new EventBusStats(STATS_COLLECTION_DURATION_MILLIS.get()); private EventConsumer catchAllSubscriber; private volatile CatchAllSubscriber catchAllSubInstance; public EventBusImpl() { } @Override public void publish(Object event) { Stopwatch start = stats.publishStats.start(); try { if (!applyEventLevelFilters(event)) { return; } Set> allTypesForAnEvent = getAllTypesForAnEvent(event); for (Class eventType : allTypesForAnEvent) { Set eventConsumers = consumersByEventType.get(eventType); for (EventConsumer eventConsumer : eventConsumers) { eventConsumer.enqueue(event); } } if (null != catchAllSubInstance && catchAllSubInstance.isEnabled()) { catchAllSubscriber.enqueue(event); } } catch (Throwable th) { LOGGER.error("Error occured while publishing event. Swallowing the error to avoid publisher from failing.", th); stats.publishErrors.increment(); } finally { start.stop(); } } @Override public void publishIffNotDead(EventCreator creator, Class... eventTypes) { Stopwatch start = stats.conditionalPublishStats.start(); try { Map, Set> interestedConsumersByType = new HashMap, Set>(); for (Class eventType : eventTypes) { for (Class anEventSubType : getAllTypesForAnEventType(eventType)) { Set eventConsumers = consumersByEventType.get(anEventSubType); if (!eventConsumers.isEmpty()) { /* * Since any change in the underlying consumers get reflected in this set, we get the benefit of any changes * to the consumers after this check being reflected when we invoke these consumers. * We add the evenType to the map and not the subType as the event creator is only aware of the high * level types & not the entire hierarchy. */ interestedConsumersByType.put(eventType, eventConsumers); } } } if (interestedConsumersByType.isEmpty()) { LOGGER.debug(String.format("Skipping publishing of events types %s as there are no interested listeners.", Arrays.toString(eventTypes))); return; } List events = creator.createEvent(interestedConsumersByType.keySet()); if (null == events) { LOGGER.debug(String.format("No events created by event creator for event types %s", interestedConsumersByType.keySet())); return; } for (Object event : events) { if (!applyEventLevelFilters(event)) { continue; } Set eventConsumers = interestedConsumersByType.get(event.getClass()); for (EventConsumer eventConsumer : eventConsumers) { eventConsumer.enqueue(event); } } } catch (Throwable th) { LOGGER.error("Error occured while publishing event. Swallowing the error to avoid publisher from failing.", th); stats.conditionalPublishErrors.increment(); } finally { start.stop(); } } @Override public void registerSubscriber(@Nullable EventFilter filter, Object subscriber) throws InvalidSubscriberException { List allConsumersForThisSubscriber = new ArrayList(); List subscriberMethods = findSubscriberMethods(subscriber); for (Method subscriberMethod : subscriberMethods) { Class targetEventType = EventBusUtils.getInterestedEventType(subscriber, subscriberMethod); EventConsumer consumer = new EventConsumer(subscriberMethod, subscriber, filter, targetEventType, consumerQueueSupplier); allConsumersForThisSubscriber.add(consumer); consumersByEventType.put(targetEventType, consumer); } CopyOnWriteArrayList existingConsumers = consumersBySubscriberClass.putIfAbsent(subscriber.getClass(), new CopyOnWriteArrayList((allConsumersForThisSubscriber))); if (null != existingConsumers) { existingConsumers.addAll(allConsumersForThisSubscriber); } else { LOGGER.info(String.format("Registered a new subscriber: %s with filter: %s", subscriber, filter)); } } @Override public void registerSubscriber(Object subscriber) throws InvalidSubscriberException { registerSubscriber(null, subscriber); } @Override public synchronized boolean enableCatchAllSubscriber(BlockingQueue catchAllSink) { if (null == catchAllSubscriber) { catchAllSubInstance = new CatchAllSubscriber(); List subscriberMethods; try { subscriberMethods = findSubscriberMethods(catchAllSubInstance); if (!subscriberMethods.isEmpty()) { Method method = subscriberMethods.get(0); catchAllSubscriber = new EventConsumer(method, catchAllSubInstance, null, Object.class, consumerQueueSupplier); } } catch (InvalidSubscriberException e) { // We know it can not happen as the subscriber is valid. LOGGER.error("Catch all subscriber invalid!", e); return false; } } return catchAllSubInstance.enable(catchAllSink); } @Override public synchronized void disableCatchAllSubscriber() { if (null != catchAllSubInstance) { catchAllSubInstance.disable(); } else { LOGGER.info("Catch all subscriber is not enabled, disable call ignored."); } } @Override public Set unregisterSubscriber(Class subscriberClass) { LOGGER.info("Unregistring subscriber class: " + subscriberClass); Set toReturn = new HashSet(); CopyOnWriteArrayList eventConsumers = consumersBySubscriberClass.remove(subscriberClass); if (null != eventConsumers) { for (EventConsumer eventConsumer : eventConsumers) { eventConsumer.shutdown(); Class targetEventClass = eventConsumer.getTargetEventClass(); consumersByEventType.remove(targetEventClass, eventConsumer); toReturn.add(eventConsumer.getContainerInstance()); } LOGGER.info(String.format("Subscriber: %s successfully unregistered", subscriberClass)); } else { LOGGER.info(String.format("Subscriber: %s is not registered (or already removed). Ignoring unregister.", subscriberClass)); } return toReturn; } @Override public boolean unregisterSubscriber(Object subscriber) { LOGGER.info("Unregistring subscriber instance: " + subscriber); Class subscriberClass = subscriber.getClass(); CopyOnWriteArrayList eventConsumers = consumersBySubscriberClass.get(subscriberClass); boolean unregistered = false; EventConsumer toRemove = null; if (null != eventConsumers) { for (EventConsumer eventConsumer : eventConsumers) { if (eventConsumer.getContainerInstance() == subscriber) { toRemove = eventConsumer; } } if (null != toRemove && eventConsumers.remove(toRemove)) { toRemove.shutdown(); Class targetEventClass = toRemove.getTargetEventClass(); consumersByEventType.remove(targetEventClass, toRemove); unregistered = true; } } if (unregistered) { LOGGER.info(String.format("Subscriber instance: %s successfully unregistered", subscriber)); } else { LOGGER.info(String.format("Subscriber instance: %s is not registered (or already removed). Ignoring unregister.", subscriber)); } return unregistered; } @Override public void addFilterForSubscriber(EventFilter filter, SubscriberInfo subscriberInfo) { String callDescription = "add filter"; EventConsumer consumerInAction = findEventConsumerForSubscriberMethod(subscriberInfo, callDescription); if (null != consumerInAction) { consumerInAction.addFilters(filter); LOGGER.info(String.format("Added a new filter %s for subscriber method %s", filter, subscriberInfo.getSubscriberMethod().toGenericString())); } } @Override public void removeFiltersForSubscriber(SubscriberInfo subscriberInfo, EventFilter... filters) { String callDescription = "remove filter"; EventConsumer consumerInAction = findEventConsumerForSubscriberMethod(subscriberInfo, callDescription); if (null != consumerInAction) { consumerInAction.removeFilters(filters); LOGGER.info(String.format("Removed filters %s for subscriber method %s", Arrays.toString(filters), subscriberInfo.getSubscriberMethod().toGenericString())); } } @Override public void clearFiltersForSubscriber(SubscriberInfo subscriberInfo) { String callDescription = "add filter"; EventConsumer consumerInAction = findEventConsumerForSubscriberMethod(subscriberInfo, callDescription); if (null != consumerInAction) { consumerInAction.clearFilters(); LOGGER.info( String.format("Removed ALL filters for subscriber method %s", subscriberInfo.getSubscriberMethod().toGenericString())); } } @Override public void addFilterForEvent(EventFilter filter, Class eventClass) { boolean modified = eventTypeVsFilters.put(eventClass, filter); if (modified) { LOGGER.info(String.format("Added a new filter %s for the event type: %s", filter, eventClass)); } else { LOGGER.info(String.format("Filter %s already exists for the event type: %s", filter, eventClass)); } } @Override public void removeFiltersForEvent(Class eventClass, EventFilter... filters) { if (null == filters || filters.length == 0) { return; } Set eventFilters = eventTypeVsFilters.get(eventClass); boolean modified = eventFilters.removeAll(Arrays.asList(filters)); if (modified) { LOGGER.info(String.format("Removed filters %s for event type %s", Arrays.toString(filters), eventClass)); } else { LOGGER.info(String.format("None of the filters %s exists for event type %s. Ignoring remove.", Arrays.toString( filters), eventClass)); } } @Override public void clearFiltersForEvent(Class eventClass) { eventTypeVsFilters.removeAll(eventClass); LOGGER.info(String.format("Removed ALL filters for event type %s", eventClass)); } @Override public Set getAllSubscribers() { Set toReturn = new HashSet(); for (CopyOnWriteArrayList eventConsumers : consumersBySubscriberClass.values()) { for (EventConsumer eventConsumer : eventConsumers) { toReturn.add(new SubscriberInfo(eventConsumer.getDelegateSubscriber(), eventConsumer.getContainerInstance())); } } return Collections.unmodifiableSet(toReturn); } @Override public Set getAllSubscribersForAnEvent(Class eventType) { Set eventConsumers = consumersByEventType.get(eventType); if (null == eventConsumers || eventConsumers.isEmpty()) { return Collections.emptySet(); } Set toReturn = new HashSet(eventConsumers.size()); for (EventConsumer eventConsumer : eventConsumers) { toReturn.add(new SubscriberInfo(eventConsumer.getDelegateSubscriber(), eventConsumer.getContainerInstance())); } return Collections.unmodifiableSet(toReturn); } @Override public Set getFilterForASubscriber(SubscriberInfo subscriberInfo) { String callDescription = "get filter"; EventConsumer consumerInAction = findEventConsumerForSubscriberMethod(subscriberInfo, callDescription); if (null == consumerInAction) { return Collections.emptySet(); } return Collections.unmodifiableSet(consumerInAction.getAttachedFilters()); } @Override public Set getFiltersForAnEvent(Class eventType) { Set eventFilters = eventTypeVsFilters.get(eventType); return Collections.unmodifiableSet(eventFilters); } @Override public Set> getAllRegisteredEventTypes() { Set> eventTypesWithConsumers = consumersByEventType.keySet(); Set> eventTypesWithFilters = eventTypeVsFilters.keySet(); return Sets.union(eventTypesWithConsumers, eventTypesWithFilters); } public synchronized void shutdown() { Collection consumers = consumersByEventType.values(); for (EventConsumer consumer : consumers) { consumer.shutdown(); } // Clearing the data as the event bus instance *may* be stored somewhere & not GC'ed consumersByEventType.clear(); consumersBySubscriberClass.clear(); eventTypeVsFilters.clear(); } @VisibleForTesting void setConsumerQueueSupplier(ConsumerQueueSupplier consumerQueueSupplier) { this.consumerQueueSupplier = consumerQueueSupplier; } @VisibleForTesting Set getEventConsumer(Class eventClass) { return consumersByEventType.get(eventClass); } private boolean applyEventLevelFilters(Object event) { return EventBusUtils.applyFilters(event, eventTypeVsFilters.get(event.getClass()), stats.filterStats, " publisher ", LOGGER); } /** * Finds all the subscriber methods defined in the passed subscriber class. Also, validates whether the subscriber * methods adhere to the rules of the game using {@link SubscriberValidator} * * @param subscriber Subscriber instance for which the subscriber methods are to be found. * * @return List of valid subscriber methods. * * @throws InvalidSubscriberException If any one of the method is invalid. */ private List findSubscriberMethods(Object subscriber) throws InvalidSubscriberException { List subscriberMethods = new ArrayList(); Set allMethods = new HashSet(); Method[] methods = subscriber.getClass().getMethods(); allMethods.addAll(Arrays.asList(methods)); // This enables declaring even private methods in a consumer. We will try to enable access, failure of which // will remove this method from the methods list. Method[] allDeclaredMethods = subscriber.getClass().getDeclaredMethods(); allMethods.addAll(Arrays.asList(allDeclaredMethods)); for (Method method : allMethods) { if (method.isAnnotationPresent(Subscribe.class)) { try { method.setAccessible(true); subscriberMethods.add(method); } catch (SecurityException e) { LOGGER.error("A subscriber method: " + method.toGenericString() + " is not a public method and the security settings does not allow accessing non-public" + " methods via reflection. This subscriber method will not be registered.", e); } } } Map errors = SubscriberValidator.validate(subscriber, subscriberMethods); if (!errors.isEmpty()) { throw new InvalidSubscriberException(subscriber.getClass(), errors); } return subscriberMethods; } /** * Finds the {@link EventConsumer} instance registered with this event bus, for the passed subscriberMethod * * @param subscriberInfo Subscriber information. * @param callDescription Description of the original call for which this method is invoked. This is used for * logging. * * @return The {@link EventConsumer} instance. null if none found. */ private EventConsumer findEventConsumerForSubscriberMethod(SubscriberInfo subscriberInfo, String callDescription) { Method subscriberMethod = subscriberInfo.getSubscriberMethod(); Subscribe subscribeAnnotation = subscriberMethod.getAnnotation(Subscribe.class); if (null == subscribeAnnotation) { LOGGER.error(String.format("The subscriber method: %s is not annotated with @Subscribe. Ignoring %s call.", subscriberMethod, callDescription)); return null; } CopyOnWriteArrayList eventConsumers = consumersBySubscriberClass.get( subscriberMethod.getDeclaringClass()); if (null != eventConsumers) { // Here what we are dealing with is the subscriber methods in a SINGLE class, so O(n) will suffice. for (EventConsumer eventConsumer : eventConsumers) { if (isTheSameSubscriber(subscriberInfo, eventConsumer)) { return eventConsumer; } } } LOGGER.info(String.format("Subscriber: %s is not registered (or already removed). Ignoring %s call.", subscriberMethod, callDescription)); return null; } private static Set> getAllTypesForAnEvent(Object event) { try { return eventHierarchyCache.get(event.getClass()); } catch (ExecutionException e) { throw Throwables.propagate(e.getCause()); } } private static Set> getAllTypesForAnEventType(Class eventType) { try { return eventHierarchyCache.get(eventType); } catch (ExecutionException e) { throw Throwables.propagate(e.getCause()); } } private boolean isTheSameSubscriber(SubscriberInfo subscriberInfo, EventConsumer eventConsumer) { return eventConsumer.getDelegateSubscriber().equals(subscriberInfo.getSubscriberMethod()) && eventConsumer.getContainerInstance() == subscriberInfo.getSubscriberInstance(); } /** * Apart from testing, there isn't really a reason to override this.

* Our batching strategy is completely built on top of the queue implementation so care should be taken while using * a custom supplier so that it provides all the correct queues for all batching strategies. */ @VisibleForTesting interface ConsumerQueueSupplier { /** * Creates a new instance of the queue based on the passed subscribe annotation. * * @param subscriberMethod The subscriber method for the consumer. * @param subscriberConfig Subscriber configuration. * @param queueSizeCounter A counter that holds the current queue size. * @return The queue instance for the passed parameters. */ ConsumerQueue get(Method subscriberMethod, SubscriberConfigProvider.SubscriberConfig subscriberConfig, AtomicLong queueSizeCounter); /** * This is a stripped down version of {@link BlockingQueue} to only expose methods that are required by the * {@link EventConsumer}. The intention of stripping down the functionality is to have more control over the * queue implementation which in turn completely supplies the batching strategy. If the consumer starts using * other methods of the {@link BlockingQueue} our batching may completely break. Using {@link BlockingQueue} will * mean implementing methods that we never use. */ interface ConsumerQueue { /** * Offer an event to the queue without blocking. If the queue is full, this returns false, if * there is space the event is added to the queue. * * @param event Event to add to the queue. * * @return true if the event was added, false otherwise. */ boolean offer(Object event); /** * Removes an element from the queue. This method does not block and returns null if none is * available. * * @return Event if available. null otherwise. */ Object nonBlockingTake(); /** * Removes an element from the queue. This method blocks till an event is available. * * @return Next event in the queue. * * @throws InterruptedException If the wait gets interrupted. */ Object blockingTake() throws InterruptedException; /** * Clears the queue and disposes the events. */ void clear(); } } }