package org.neo4j.util; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.transaction.RollbackException; import javax.transaction.Status; import javax.transaction.Synchronization; import javax.transaction.SystemException; import javax.transaction.Transaction; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.kernel.impl.event.Event; import org.neo4j.kernel.impl.event.EventData; import org.neo4j.kernel.impl.event.ProActiveEventListener; /** * A layer on top of EventManager. EventManager delegates the events on to its * listeners immediately when generated (or rather immediately if they are * reactive). That doesn't hold water when transactions comes into the picture. * Events mustn't reach its listeners of the transaction fails, therefore * all events will be sent after a successful transaction. That is what * TransactionEventManager does. So instead of registering an event listener * at EventManager, register it at TransactionEventManager instead to gain this * functionality. * */ public class TransactionEventManager { /** * Will be sent to all the listeners with the EventData consisting of * a EventContext[] with all the events in the (successful) transaction */ public static final Event TX_EVENT_BUFFER = new TransactionEvent( "TX_EVENT_BUFFER" ); private ConcurrentMap<Transaction, TransactionHook> transactions = new ConcurrentHashMap<Transaction, TransactionHook>(); private InternalEventListener internalListener = new InternalEventListener(); private Map<Event, Set<ProActiveEventListener>> listeners = new HashMap<Event, Set<ProActiveEventListener>>(); private Set<Event> registeredEvents = new HashSet<Event>(); private GraphDatabaseUtil graphDbUtil; private TransactionHookFactory hookFactory = new TransactionHookFactory() { public TransactionHook newHook( Transaction tx ) { return new TransactionHook( tx ); } }; private boolean wrapEventsInTx; public TransactionEventManager( GraphDatabaseService graphDb ) { this( graphDb, false ); } public TransactionEventManager( GraphDatabaseService graphDb, boolean wrapEventsInTx ) { this.graphDbUtil = new GraphDatabaseUtil( graphDb ); this.wrapEventsInTx = wrapEventsInTx; } /** * Contains an event buffer with all the events generated in this tx. * TransactionHooks are created when the first business logic event gets * generated, not necessarily when the tx begins. * @param id the tx id * @return the TransactionHook with the event buffer, or null if no * TransactionHook was associated with this id */ private TransactionHook getTransactionHook( Transaction tx ) { return this.transactions.get( tx ); } private TransactionHook createTransactionHook( Transaction tx ) { TransactionHook hook = this.hookFactory.newHook( tx ); TransactionHook previous = this.transactions.putIfAbsent( tx, hook ); if ( previous != null ) { throw new RuntimeException( "There was a previous tx id " + tx ); } return hook; } /** * Removes a TransactionHook. This is done when a tx is finished and * its events have been sent. * @param id the transaction id. */ private void removeTransactionHook( TransactionHook hook ) { if ( !this.transactions.remove( hook.tx, hook ) ) { throw new RuntimeException( "Couldn't remove tx hook " + hook.tx ); } } /** * Gets all the listeners which listens to <CODE>event</CODE> * @param event the Event * @return a Set containing the listeners. */ private Set<ProActiveEventListener> getListenersSet( Event event ) { synchronized ( listeners ) { Set<ProActiveEventListener> listenersForEvent = listeners.get( event ); if ( listenersForEvent == null ) { listenersForEvent = new HashSet<ProActiveEventListener>(); listeners.put( event, listenersForEvent ); } return listenersForEvent; } } /** * Sets the factory instance for creating transaction hooks. * @param factory */ public void setTransactionHookFactory( TransactionHookFactory factory ) { this.hookFactory = factory; } /** * Registers a listener to listen for <CODE>event</CODE> events. * <CODE>listener</CODE> will receive the events after a successfull * transaction commit. * * @param listener the listener to listen for <CODE>event</CODE>. * @param event the type of event to listen for. */ public void registerEventListener( ProActiveEventListener listener, Event event ) { synchronized ( listeners ) { Set<ProActiveEventListener> listenersForEvent = getListenersSet( event ); if ( listenersForEvent.contains( listener ) ) { throw new RuntimeException( "Listener " + listener + " already registered for " + event ); } listenersForEvent.add( listener ); } registerInternalEventListener( event ); } private void registerInternalEventListener( Event event ) { // Only have to register an event for the internal listener once. synchronized ( registeredEvents ) { if ( !registeredEvents.contains( event ) ) { graphDbUtil.registerProActiveEventListener( internalListener, event ); registeredEvents.add( event ); } } } /** * Unregisters a listener from <CODE>event</CODE> events. * * @param listener the listener to stop listen to <CODE>event</CODE>. * @param event the type of event to stop listen to. */ public void unregisterEventListener( ProActiveEventListener listener, Event event ) { synchronized ( listeners ) { Set < ProActiveEventListener > listenersForEvent = getListenersSet( event ); if ( !listenersForEvent.contains( listener ) ) { throw new RuntimeException( "Listener " + listener + " not registered for " + event ); } listenersForEvent.remove( listener ); } unregisterInternalEventListener( event ); } private void unregisterInternalEventListener( Event event ) { // Only have to unregister an event for the internal listener once. synchronized ( registeredEvents ) { if ( registeredEvents.contains( event ) ) { graphDbUtil.unregisterProActiveEventListener( internalListener, event ); registeredEvents.remove( event ); } } } /** * General method for sending an event from a TransactionHook. * All the listeners for the given event are looped through and those * who require immediate delivery or not (depending on the * <CODE>immediate</CODE> argument) gets the event delivered. * @param event class containing both Event and EventData * @param immediate which listeners to receive this event, those * who require immediate delivery or not. */ private void sendEvent( EventContext event/*, boolean immediate*/ ) { synchronized ( listeners ) { for ( ProActiveEventListener listener : getListenersSet( event.getEvent() ) ) { safeSendEvent( listener, event.getEvent(), event.getData() ); } } } /** * Sends all the events in a tx for a specific listener as one chunk. * This is performed after all the events have been sent individually. * @param listener the listenter to receive the chunk * @param events the events this listener will receive */ private void sendEventBuffer( ProActiveEventListener listener, EventContext[] events ) { safeSendEvent( listener, TX_EVENT_BUFFER, new EventData( events ) ); } private void safeSendEvent( ProActiveEventListener listener, Event event, EventData data ) { try { listener.proActiveEventReceived( event, data ); } catch ( Exception e ) { //TODO Log } } /** * This method is not for normal use... just for testing purposes. */ public void flushEvents() { try { TransactionHook hook = getTransactionHook( graphDbUtil.getTransactionManager().getTransaction() ); if ( hook == null ) { return; } hook.flushEvents(); } catch ( SystemException e ) { throw new RuntimeException( e ); } } private class EventList { private Map<ProActiveEventListener, List<EventContext>> eventsPerListener = new HashMap<ProActiveEventListener, List<EventContext>>(); void sendEvent( EventContext context ) { TransactionEventManager.this.sendEvent( context ); bufferEvent( context ); } private void bufferEvent( EventContext context ) { for ( ProActiveEventListener listener : TransactionEventManager.this.getListenersSet( context.getEvent() ) ) { List<EventContext> eventList = eventsPerListener.get( listener ); if ( eventList == null ) { eventList = new ArrayList<EventContext>(); eventsPerListener.put( listener, eventList ); } eventList.add( context ); } } void sendEventBuffer() { for ( ProActiveEventListener listener : eventsPerListener.keySet() ) { List < EventContext > list = eventsPerListener.get( listener ); EventContext[] events = list.toArray( new EventContext[ list.size() ] ); TransactionEventManager.this.sendEventBuffer( listener, events ); } } } /** * A simple factory interface for creating {@link TransactionHook} * instances. */ public static interface TransactionHookFactory { /** * Creates a new transaction hook for a given transaction. * @param tx the transaction to use. * @return a new transaction hook for a given transaction. */ public TransactionHook newHook( Transaction tx ); } /** * A code hook to execute when a transaction is committed. */ public class TransactionHook implements Synchronization { private Transaction tx; private List < EventContext > events; private EventList eventList; TransactionHook( Transaction tx ) { this.tx = tx; } protected void sendEvents() { if ( this.events == null ) { // No events in the queue return; } try { beforeSendingEvents(); this.eventList = new EventList(); for ( EventContext context : events ) { this.eventList.sendEvent( context ); } this.eventList.sendEventBuffer(); } finally { afterSendingEvents(); } } /** * Used in tests only */ private void flushEvents() { sendEvents(); this.events = null; } protected void beforeSendingEvents() { } protected void afterSendingEvents() { } protected void queueEvent( Event event, EventData data ) { if ( this.events == null ) { this.events = new ArrayList<EventContext>(); } this.events.add( new EventContext( event, data ) ); } public void afterCompletion( int status ) { removeTransactionHook( this ); if ( status != Status.STATUS_COMMITTED ) { return; } Thread thread = new Thread() { @Override public void run() { org.neo4j.graphdb.Transaction tx = wrapEventsInTx ? graphDbUtil.graphDb().beginTx() : null; try { flushEvents(); if ( wrapEventsInTx ) { tx.success(); } } finally { if ( wrapEventsInTx ) { tx.finish(); } } } }; thread.start(); try { thread.join(); } catch ( InterruptedException e ) { // Ok } } public void beforeCompletion() { } } private class InternalEventListener implements ProActiveEventListener { public boolean proActiveEventReceived( Event event, EventData data ) { try { Transaction tx = graphDbUtil.getTransactionManager().getTransaction(); TransactionHook hook = getTransactionHook( tx ); if ( hook == null ) { hook = createTransactionHook( tx ); tx.registerSynchronization( hook ); } hook.queueEvent( event, data ); } catch ( SystemException e ) { throw new RuntimeException( e ); } catch ( RollbackException e ) { throw new RuntimeException( e ); } return true; } } protected static class TransactionEvent extends Event { TransactionEvent( String name ) { super( name ); } } }