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.
Since much of the following discussion deals with events in the context of transactions, you should be familiar with the Guide to Transactions.
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:
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.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.
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:
ROLLBACK_ONLY
.
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.
Putting together all those events, this is what happens when a top-level transaction is committed, in the precise order:
BEFORE_COMMIT
TransactionEvent is sent to all listeners.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.COMMITTED
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.
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.ActionEvent
s 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);
// 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:
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.
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.
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.
This coupling mode executes actions during the
BEFORE_COMMIT
phase.
A transaction is committed through the following
steps
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.
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.
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.
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.
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.