Introduction

DRAGOS features a comprehensive and powerful event system. Most changes in the DRAGOS system and graph data are advertised using events, and interested parties can register with the EventManager to be notified of such changes.

There are different kinds of events. For every one of these kinds there is a corresponding listener interface that must be implemented by those classes that want to be receive those events.

DataEvents (GraphEntityClassEvents and GraphEntityEvents) GraphEntityClassEvents signal changes in the schema, e.g. definition of a new Node class or a new attribute for such a class.



GraphEntityEvents indicate changes in the graph data: creation of entities, modification of attribute values and so on.



Both types of events describe manipulation of data in the database, and can only occur inside transactions. The abstract super class DataEvent not only emphasizes these commonalities, but also allows uniform support for transaction-awareness (see below).
GraphPoolEvents These events are triggered for graph pool related actions such as opening, closing, or deleting a graph pool.
TransactionEvents Events of this type are fired before and after a transaction changes its state, e.g. is rolled back or committed

Built on top of the event system is the RuleEngine, which provides a uniform way to define patterns of events and actions to execute when a match is found, making it a great tool to perform automated graph rewriting.

Most applications should make use of the RuleEngine, because it handles transaction details and thus leads to fewer bugs and also better code readability and maintainability. But for cases where that extra bit of control is needed, the underlying event mechanism is also accessible.

It is recommended to read the first part of this document, dealing with the event system itself, even if you only want to use the RuleEngine. Understanding the event system will help you in choosing the right coupling mode and avoiding pitfalls.

Prerequisites

Since much of the following discussion deals with events in the context of transactions, you should be familiar with the Guide to Transactions.

The event system

The EventManager

The EventManager is the heart of event handling in DRAGOS. In contrast to what you may be used to (e.g. from GUI programming), you do not register with the actual object generating the events, but with the EventManager.

There are a number of reasons for this:

  • In a graph environment, entities are created and deleted much more often than in a GUI, so making sure you are registered as a listener with every one of them would be both a complex task and a waste of memory, since in almost every case, you are interested in all of the graph entities.
  • EventManager knows about transactions, and can provide a number of related services to your application. For instance, it can queue events until the transaction (and thus the changes indicated by the events) are actually committed, or discard them if the transaction is rolled back, freeing you from manually keeping track of these things.
  • EventManager can provide network transparency. This means that in a networked environment where multiple DRAGOS kernels are running on different machines, you are informed when another user starts a transaction, which graph entities he is modifying (so they can be locked) and so on. (Note: The current default implementation does not yet provide this feature, but it can be added anytime without changing any of the existing methods in the interface.)

DataEvents, transactions and consistency

When registering as a listener for a DataEvent, you must specify an EventCouplingMode that identifies when you want to receive the events:

  • IMMEDIATE - events will be fired as soon as they occur (as in GUI programming).
  • BEFORE_COMMIT - events will be queued until the data has passed the consistency checks and the top-level transaction is about to be committed. This implies that for nested transactions, no queued events are fired when calling commit() on the nested transaction itself. It also implies that, when queued event handling starts, the top-level transaction will be the current one (otherwise, commit() would result in an exception), thus providing a fixed and well-defined context of execution.
  • AFTER_COMMIT - events will be queued and fired after the data is actually committed to the database.
Only the last of these three options guarantees that the changes indicated by the events will actually be written to the database. For the other options, a rollback might happen, meaning all changes are discarded.

Operating on possibly inconsistent data is very difficult and requires great care. If possible at all, try to avoid doing so!

When receiving events in IMMEDIATE mode, no checks have been performed.

In BEFORE_COMMIT mode, the data has passed the consistency checks. If a listener performs any changes on the data, the data is checked again (possibly resulting in a consistency exception), and any events generated by this change are added to the end of the queue, as if they had occured in the main body of the transaction. No guarantees are made as to the order of listeners or the fairness of event distribution, only that each listener will receive all of the events that occur in this transaction in the same order they are generated. So if you need a fixed order of execution for a number of listeners working closely together, it would be best to only register a helper class as listener that receives the events and redistributes them according to your needs internally. Also, when implementing listeners, please take care that you do not keep modifying the data in reaction to every event, which in turn will generate another event, thus creating an infinite loop!

All these problems do not apply to AFTER_COMMIT. The changes are already written to the database, and consistency is guaranteed. If a transaction is rolled back, its event queue is silently discarded, and the listeners will not receive any events (because no actual changes have been performed on the data).

Executive summary: Use AFTER_COMMIT if you can, BEFORE_COMMIT if you need to modify the data before is is written to the database, and IMMEDIATE only if you absolutely have to. Also, take a look at the rules section below, since the RuleEngine can take care of many of those details for you.

TransactionEvents

Another group of events handled by the EventManager are the TransactionEvents. These events are fired before and after a transaction has changed its state.

To prevent confusion: The transactions we are talking about here are always user transactions, never database transactions (see the Guide to Transactions for a discussion of these terms).

You can register to receive events only from a specific TransactionManager or from all TransactionManagers.

A number of events may be generated during the life cycle of a transaction. Normally, events are fired when:

  • the transaction is created.
  • the transaction begins.
  • the transaction is precommitted (only for nested transactions).
  • the transaction is committed.
But there are also events fired when:
  • the transaction is marked ROLLBACK_ONLY.
  • the transaction is rolled back.

Each type of event occurs at most once during a single transaction's life cycle, some are even mutually exclusive (like COMMIT and ROLLBACK). The only exception to this rule is when a GrasGXLException is thrown by a listener during the BEFORE_COMMIT or BEFORE_ROLLBACK - in this case, it is possible to restart the operation, thus triggering a second event of that type, or even perform a rollback instead of a commit (or vice versa), which would trigger the appropriate event. It still holds for AFTER_ event types, though.

A sample transaction commit, step by step

Putting together all those events, this is what happens when a top-level transaction is committed, in the precise order:

  1. A consistency check is performend, and an exception thrown if it fails.
  2. DRAGOS commit phase:
    1. A BEFORE_COMMIT TransactionEvent is sent to all listeners.
    2. The GraphEntity event queue with EventCouplingMode.BEFORE_COMMIT is processed. Among the listeners is the RuleEngine, which executes all rules registered for CouplingMode.DEFERRED. Every single time a listener has processed a single event, a consistency check is performed on any changed graph data elements.
  3. A final consistency check is performed, just to be sure.
  4. Database commit phase: the commit call is forwarded to the underlying data source(s)
  5. The Transaction's state is set to COMMITTED
  6. An AFTER_COMMIT TransactionEvent is sent to all listeners. The RuleEngine is among the listeners again, this time executing CouplingMode.DECOUPLED rules (in separate transactions).

For more details on the events generated when nested transactions are involved, please refer to the Transaction API documentation.

Generic events

Besides the different DRAGOS-specific types of events described above, the EventManager can also relay generic events. This facility is offered as a service to extensions and applications, and not used in the DRAGOS core itself. Right now, the only reason to use it would be asthetic: all events would run through the EventManager, thus providing a single responsible instance. But later on, when EventManager is extended, other advantages like network transparency would also become available to the clients automatically.

Please note that this is not the only way for an extension to use custom events! In fact, if your event can be expressed as a subclass of any of the pre-defined event classes, it is strongly encouraged to do so, especially for DataEvents, where you gain transaction-awareness for free!

The generic event service is offered through channels. A channel is identified by a subinterface of java.util.EventListener, which the listeners implement.

Example:

If you want to broadcast java.awt.event.ActionEvents through the EventManager, you would choose the java.awt.event.ActionListener as both interface for your listeners and channel identifier.

You would add a listener like this:

EventManager.getInstance().addGenericListener(ActionListener.class, myActionListener);


To fire an event:

// generate the event
ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED , "test");
// use reflection to get the receiving method
Class parameterTypes = new Class[1];
parameterType[0] = ActionEvent.class;
Method receivingMethod = ActionListener.class.getMethod("actionPerformed", parameterTypes);
// fire the event
EventManager.getInstance().fireGenericEvent(ActionListener.class, receivingMethod, e);

You can do the reflection once during initialization, and store the Method object, thus reducing the amount of code and time required to fire an event.

Some notes:

  • Since channels are identified only by their listener interface, different clients of the service might accidentaly share the same channel. The best way to avoid this is to choose an interface under your control. If necessary, create a subclass of an existing interface that does not define any new methods, and refine / wrap your listeners to implement that interface. Example: com.company.projectname.MyActionListener extend java.awt.event.ActionListener. The same technique can be used to simulate listening on specific instances of event sources, if the number of instances is limited.
  • It is considered good style to only call methods that are defined in the listener interface, but nothing prevents you from calling any other method to receive the event. As long as it accepts an GrasGXLEvent as its only parameter and can be found and accessed through Reflection, everything should work fine.

Things to keep in mind

To put it simply: Event handling should not mess with transactions. The only allowed modification is setting the ROLLBACK_ONLY state if an unrecoverable error occured. You may start a new or nested transaction (depending on whether there is an active transaction), but only if you close it before returning from event handling. Anything else will most probably lead to random things breaking at some random time.

Rules

The RuleEngine

The rule engine is a service running in the DRAGOS kernel, like the EventManager. You just tell it which actions to perform when certain patterns of events are matched, and it will take care of the rest.

CouplingMode.DEFERRED

This coupling mode executes actions during the BEFORE_COMMIT phase. A transaction is committed through the following steps

  1. The commit command is issued by the application.
  2. Consistency checks are performed.
  3. Rule commands are executed for any pending events. This may in turn raise new events, which will be added to the end of the queue. This step is repeated until the queue is empty.
  4. Consistency checks are performed again (this step may be skipped if no events were processed in the step before).
  5. The transaction is now actually commited to the database.
Note that there is no defined order for actions in the BEFORE_COMMIT phase. Besides the RuleEngine, there might be other listeners for this type of event, and they too might modify the graph data. The difficulties that come with such undetermined behaviour (especially when debugging) are one reason to exclusively use the RuleEngine for transformations if possible, more reasons are given below.

CouplingMode.DECOUPLED

This coupling mode executes actions during the AFTER_COMMIT phase of the original transaction, but each wrapped inside their own, new transaction. The main advantage is that in this case, an action may set its transaction to "rollback only" if anything goes wrong, without affecting the main transaction or the actions of any other rules.

Especially when executed after very complex transactions involving many changes, this mode may yield better performance: The transaction overhead will probably be more than compensated by the vastly reduced number of changed entities that have to be verified by the GraphPoolChecker and SchemaChecker.

Rule or EventListener?

If given the choice, always define your logic as rules instead of writing an event listener yourself. Not only does this reduce the amount of application code and the associated chance of bugs. The common format for rules also means the application is easier to understand and maintain compared to custom code. And if necessary, changing rules at runtime is much easier than deploying and using a new class.

Things to keep in mind

What has been said regarding event handling in general also applies here: Manipulation of transactions is limited. Please refer to the subsection with the same name in the event section for details.

Guide for PEGS Implementers

As a PEGS developer, you have little to deal with events, and only in a quite straightforward way:

Whenever you modify the graph data, create the respective event as soon as you are done and hand it over to the EventManager, which will take care of the rest. For a complete list of event types, look at the API documentation.

Possible future changes and extensions

  • Event sharing across multiple DRAGOS kernel running in a network. Because some of the data in the events is not Serializable, e.g. the GraphEntity of a GraphEntityEvent, this would require custom (de)serialization code. In this example, we would just store the internal identifier, which, together with the DataSourceURL, allows us to uniquely identify the entity and thus reconstruct it later. Of course, such reconstruction can prove a lot harder when the entity has been deleted and is thus no longer retrievable from the GraphPool. These problems require thorough discussion.