Designing an event-based application

Firstly, decide if using an event-based paradigm for the application makes sense. Event-driven systems are very useful if they meet the following characteristics:

  • Components are loosely coupled
  • Operations can be processed asynchronously
  • The state of an operation may be part of a transient (in-memory) workflow
  • Events can be broadcast and received by multiple listeners
  • There is a standard agreement for what details an event should have
  • The event topics are (or become) known at development time

On the other hand, the following are not suitable for (OSGi) event-driven systems:

  • Where the state of the workflow is not only UI-based but part of the domain
  • Where the consumption of an event is handled transactionally
  • Where large volumes of events can throttle single-threaded delivery
  • Where there is a lack of event payload structure
  • Where there is a requirement for a synchronous response to occur

Componentizing the application

The first step in designing an event-driven system is to create components out of the parts of the application that need to talk to each other. This might correspond with the natural boundary of OSGi bundles, or it may be more fine-grained. There may be other boundaries—such as package boundaries or Declarative Services components—that more naturally represent the components in the application.

Once the components are known, it becomes easier to track the relationships between them, including what the messages are that the various components will need to send to one another to work.

For each of the components, there should be one or more input events, and one or more output events (or other side-effect changes). These should be represented as entry and exit points of the components, with a separate input for each type of event that might flow in.

Identifying the channels

For each of the input and output channels of the components, the main purpose needs to be identified. In the first iteration, this can be as simple as a noun (such as "mouse event" or "mail message"). Subsequent iterations will fill out details on the channels that get passed.

The result of this should be a high-level event diagram of the system. It may not be as detailed or object specific as an object interaction diagram, but it should show the graph of input events, followed by the directed triggers that could flow from them. For example, an incoming mail message might trigger a mail processing script, which in turn fires more events to send auto-response mails or log the message to a database.

Tip

Identify whether the channels are firing an event for the purpose of causing a downstream event to occur, or whether they may be firing events for informational purposes (such as logging). Having events fired at different points in a life cycle means that it is easier to add additional functionality afterwards.

Identifying the properties

For each of the events that are sent, there may be zero or more properties containing additional information regarding the event. In the case of an incoming mail message, this could include the sender of the e-mail, the subject, the time the mail was sent, the importance, and of course the e-mail body as well.

The first iteration of the properties is likely to be a rough cut, and will evolve over time. As the event system is fleshed out, it may be necessary to record additional details that weren't captured in the first place. This may include things such as the time zone of the sender, or what hosts it hopped through to be delivered. The flexibility of the event pattern is that it's easy to evolve by adding additional information in subsequent releases and clients who do not need to know this information can simply ignore it.

Tip

A similar mechanism for evolution exists in JSON messages. Provided that a client knows how to parse a JSON object and knows which fields to specifically look for, it is possible to add additional fields to the object without breaking backwards compatibility.

Mapping the channels to topics

Once the channels and event properties have been decided, the next step is to map these to topics so they can be used in EventAdmin. Topics are represented in slash-separated format, and this is important because the wildcard character * can be used to subsume additional levels in the topic hierarchy.

Typically, the event hierarchy is based on a reverse domain name style prefix. This allows events produced by one organization to not conflict with other events when installed into the same runtime. In the case of OSGi bundles, it is very often the case that the event topic prefix will be a variation on the bundle name itself.

The topic may then be further segregated by the sub-channel, depending on what level of granularity is needed. In the E4 model, the topics for items changing in the workspace model begin with org/eclipse/e4/ui/model/ and then continue with a type such as commands or application.

Since topics can be matched with wildcards, it may make sense to add another name segment for an event channel (such as application/ApplicationElement/* instead of just application/ApplicationElement) as this will permit future partitioning of the event space. A terminal leaf node cannot be split down into more children, whereas a segment can have more segments added afterwards. This was a pattern identified by the E4 platform, which initially just used the terminal node but then subsequently switched to a more partitioned space so that changes in individual attributes could be nominated using a common prefix.

Simulating events

One advantage of an event-driven system is that it is very easy to test in isolation. Besides having events driven by the EventAdmin specification directly, it is also possible to simulate the arrival of an event by calling the method directly. It is thus possible to test individual components by setting up listeners looking for output events and simulating the incoming events.

This also helps to test the component in a black box manner. Provided that the events delivered are well formed, and the events generated have the correct data, it is possible to show that the component is operating as expected.

It may be necessary to set up other mock services, event sources, or event sinks in order to test the functionality of that component, but the principle of segregated components making it easy to test are still important.

Versioning and loose typing

Because event-based systems are inherently loosely typed, it is necessary to define both the values of event topics and the schema of those events in an external location. This may be part of the project's documentation, or there may be other systems that record this information externally to the project or with schemas such as RelaxNG (though that is more suited for XML documents).

Changing the event's properties or modifying the event's topics will not be picked up by a static compiler. This results in a higher testing requirement being placed on the system itself, but also gives it additional flexibility for being able to respond to changes in the future.

When the version of the API changes, it may be necessary to implement version numbering information in the payload of the event itself. This can be used to communicate the state of the API to clients at any time, and if backward compatible changes are required, then these can be brought into play. It may also be possible for the client and server to agree on the version number to use, even if it means degrading to a lower version.

Tip

Always design a version number in your message formats, be they OSGi Events, JSON messages, or even XML documents. The version number should be stored at the top level and revisions to this number may indicate different content elsewhere or in a child element of the message.

It may be desirable to store the version number as a single integer, or it may be a pair or triplet of numbers. Whatever value is chosen, it should be treated as a semantic version, with major digits indicating a backwardly incompatible change, and minor versions being backwardly compatible but with potentially new features being added.

Servers (or event sources) hardly ever get rolled back, so typically these numbers will be monotonically increasing. It is thus usually sufficient to represent just the major number, or possibly the major and minor number as part of the API definition. It is usually an error to include the micro/patch numbers in the public part of the API as this binds the client too tightly to the version of the API in use. The main reason for exposing the minor version is in case a client is implemented to selectively enable additions for newer functions; this comes in handy if the same client is exposed to both old and new versions for an extended period of time.

With a known version and a known set of event properties and types, it is possible to document changes and upgrade the API when necessary to add new features or to document backward compatibility issues.

Event object contents

Since the Event objects are an in-memory representation, and the map that is passed can store objects of any type, it is possible to put any kind of object into the event object itself. For example, the Bundle can be embedded in the Event object or UI-specific components such as Color or even open InputStream objects.

The OSGi specification suggests limiting the use of the Event properties to the set of primitive values such as int (or their object counterparts such as Integer) along with String and single-dimensional arrays of the same. In other words, although it is possible to store URL instances in the map directly, it is recommended that it be stored as a String and then converted on the client into a URL object.

The reason for this recommendation is that while EventAdmin is a system designed for use within a single VM, it is not limited to being used in a single VM. In fact, in conjunction with OSGi Remote Services, it is possible to set up a distributed EventAdmin fabric, where events generated on one node get transported over the network and then handled on a remote node. To make this possible, all the values in the Event object need to be Serializable, and because it may be the case that the events are processed in a different language (such as JavaScript or C), having a standard set of known datatypes facilitates that translation.

Similarly, objects placed into an OSGi Event should be immutable. If an object placed into an Event is not immutable (such as the old Date class), then it would be possible to dispatch an event, and later modify its contents before a consumer has time to process the original value. No runtime checks are made by EventAdmin, but violating this rule can lead to surprises.

Comparison with JMS

Designing an event-driven system looks very similar to designing a message-driven system using an API such as Java Messaging Service (JMS). Both follow a similar paradigm for being able to build an application; the system is modeled as a set of state changes triggered by incoming events (messages), resulting in either system updates or subsequent events (messages) being fired.

The following differences are worth observing between the event-driven and message-driven systems:

  • No broker: In a JMS system, there is an intermediary (broker) that runs in a separate process with memory separation between the clients. The lifetime of this process is orthogonal to the lifetime of the client, and in particular, there may not be a broker running at any point. On the other hand, with EventAdmin, there is no separate standalone broker process, although the EventAdmin service acts like a centralized in-process broker.
  • Transactional: Probably the biggest single difference is that JMS systems are designed to be transactional in nature. If the message is not processed successfully on a node, then the intermediary broker can attempt to pass that message on to another subscriber for redelivery. The transactional support can also extend to other transactional resources (such as databases) for a clean separation. No such transactional support is available in EventAdmin.
  • Broadcast versus point-to-point: JMS provides different types of message deliveries. In a broadcast mechanism, all subscribers are notified of a message (these are typically called topics), and this is the mechanism that EventAdmin uses for Event delivery. JMS also provides a point-to-point mechanism (called queues), which ensures that only one subscriber gets each message. Queues are often used to allow scaling by adding additional workers. The EventAdmin service does not have a concept of queues or single event delivery.
  • Persistent versus transient: JMS can be configured to operate in a persistent mode (where all messages are written to disk) or in a transient mode (where messages are held in memory and lost upon system restart). EventAdmin only has transient support; if the OSGi runtime crashes, then all in-flight events are lost. For unimportant states (such as which button in a GUI was being clicked at the time), this may not be an issue, but for data-specific processing, this may be a problem.
  • Language bindings: Typically, JMS systems support more languages since the intermediary broker provides a means to be able to convert the message types to different languages, provided that a standardized set of properties are used. The OSGi EventAdmin doesn't officially support other languages, but leaves it open to implementors of the frameworks to support them if desired. In practice, it is fairly easy to hook up a set of events to something like JSON messages, which are becoming the de facto interchange format between systems as well as between clients and browsers.

The advice is to use an in-memory system such as EventAdmin where the state of the workflow is transient or does not need transactional persistence, and use a more heavy-weight solution such as JMS when queues or transactional storage is required.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset