Hosted by Three Crickets

Diligence
Web Framework
For Prudence and MongoDB

Diligence is still under development and incomplete, and some this documentation is wrong. For a more comprehensive but experimental version download the Savory Framework, which was the preview release of Diligence.

Diligence logo: sleeping monkey

Events Service

Almost every application framework provides some generic way to listen to and fire one-way messages called "events." By decoupling event producer code from event consumer code, you can allow for a looser, more dynamic code architecture.
Some frameworks go a step beyond simple code decoupling, and treat producers and consumers as separate components, in which the producer cannot make any assumptions on the consumer's thread behavior. Consider two extremes: a consumer might respond to events immediately, in thread, possibly tying up the producer's thread in the process. Or, it might allow for events to be queued up, and poll occasionally to handle them. In the latter highly abstracted situations, events are called "messages," and implementations often involve sophisticated middleware to queue messages, persist them, create interdependencies, and make sure they travel from source to destination via repeated attempts, back-off algorithms, notifications to system administrators in case of failure, etc.
One size does not fit all. With Diligence, we wanted to keep events lightweight: we assume that your consumer and producer components are all running inside a Prudence container: either they are explicit or implicit resources running in web request threads, or they are asynchronous tasks. This allows us to optimize for this situation without having to rely on abstracting middleware. Still, more sophisticated, dedicated messaging middleware is out there and available if you need it. We suggest you try RabbitMQ.
That said, the combination of Prudence Hazelcast clusters, MongoDB, and JavaScript's inherent dynamism within the Prudence container allows for a truly scalable event framework. If what you need is asynchrony and scalable distribution, rather than generic decoupling, then Diligence events might be far more useful and simpler than deploying complex middleware.
The point of an event-driven architecture is that you're relinquishing some control of your code-flow. It's thus hard to know, simply by looking at the code, which parts of it will be triggered when an event is fired. You also need to know what exactly is subscribing and where that listener code is. Decoupling code is a great way to introduce some really difficult bugs into your codebase, and vastly reduce its debuggability. We present this service for your use, but encourage you to think of the costs vs. the benefits in terms of code clarity. Perhaps there is a more straightforward way to solve your problem? If all you need as asynchronicity, then you can also use the Prudence.Task API more directly, allowing you to call specific listening code, rather than any generic subscriber. The bottom line is that as great as this service is, we recommend using it with discrimination.

Usage

Make sure to check out the API documentation for Diligence.Events.

In-Thread Events

First, the basics. Here's our "/libraries/politeness/acknowledgements.js":
Diligence.Events.subscribe({
	name: 'payments.successful',
	fn: function(name, context) {
		logger.info('User {0} has paid us {1}!', context.username, context.amount)
		Acknowledgements.sendThankYou(context.username)
	}
})
Then, to fire the event, somewhere in our payments workflow:
document.executeOnce('/politeness/acknowledgements/')
Diligence.Events.fire({
	name: 'payments.successful',
	context: {
		username: user.name,
		id: user.id,
		amount: payment.amount
	}
})
For this to work, you have to make sure the firing code has already run the code that hooks up the listeners. Often, a simple document.execute will do the trick, as in this example.

Asynchronous Events

You can easily make the listeners run outside your thread, in fact anywhere in your Prudence cluster. This, of course, is crucial for scalability, because you don't want the listeners holding your web request thread.
For this to work, we need to add something small to our subscription:
Diligence.Events.subscribe({
	name: 'payments.successful',
	dependencies: '/politeness/acknowledgements/',
	fn: function(name, context) {
		logger.info('User {0} has paid us {1}!', context.username, context.amount)
		Acknowledgements.sendThankYou(context.username)
	}
})
Note that we had to add a "dependencies" key to the listener, to allow it to be called in different contexts. These dependencies are document.executeOnce'd to make sure the calling thread has access to all the code it needs.
Firing it:
document.executeOnce('/politeness/acknowledgements/')
Diligence.Events.fire({
	name: 'payments.successful',
	async: true,
	context: {
		username: user.name,
		id: user.id,
		amount: payment.amount
	}
})
All we did was add "async: true", and… that's pretty much it. Every listener will run in its own thread within the global pool. You can add a "distributed: true" flag to cause listeners to be executed anywhere in the cluster, and there's where things get really powerful: you can properly scale out your event handling in the cluster, with nothing more than a simple flag.
How does this magic work? It's JavaScript magic: we're evaluating the serialized listener source code. The code that fires the event is called as a Prudence task. The task makes sure to run the dependencies and evaluate the JavaScript you stored. Voila. (Serialization and eval will only occur on async events: otherwise, it's a regular function call.)
Concerned about JavaScript eval performance? Generally, it's very fast, and surely whatever overhead is required to parse the JavaScript grammar would be less than any network I/O that a distributed event would involve. If you're really worried about performance, make sure to store as little code as possible in the listener function and quickly delegate to compiled code. For example, your listener can simply call a function from one of the dependency libraries, which are already compiled and at their most efficient.

Stored Listeners

So far so good, but both examples above require you to execute the code that subscribes the listeners before firing the event. Stored listeners remove this requirement by saving the event and its listeners in one of several storage implementations.
For example, let's store our listeners in application.distributedGlobals, so that we can fire the event anywhere in the Prudence cluster:
var globalEvents = new Diligence.Events.GlobalsStore(application.distributedGlobals, 'myevents.')
Diligence.Events.subscribe({
	name: 'payments.successful',
	stores: globalEvents,
	id: 'sendThankYou',
	dependencies: '/politeness/acknowledgements/',
	fn: function(name, context) {
		logger.info('User {0} has paid us {1}!', context.username, context.amount)
		Acknowledgements.sendThankYou(context.username)
	}
})
We can also use application.globals or application.sharedGlobals.
One small issue to note when using stored listeners is that storage must support concurrency. One implication of this is that you need to make sure that they are not registered more than once, say by multiple nodes in the cluster, otherwise your listener code would be called multiple times. And that's what the listener "id" field is for. (In fact, the "id" field can also be used for in-thread listeners.) It also might make sense to set up all your stored listeners in your "/startup/" task, but it's not a requirement: you can install listeners whenever necessary and relevant.
Because it's stored, firing the event does not require us to execute the listener code first in our thread. We can remain blissfully unaware of who or what is subscribed to our event:
Diligence.Events.fire({
	name: 'payments.successful',
	stores: globalEvents,
	async: true,
	distributed: true,
	context: {
		username: user.name,
		id: user.id,
		amount: payment.amount
	}
})
The "stores" param can also be an array, so you can fire the event on listeners from various stores. The in-thread store is in "Diligence.Events.defaultStores", so you can concat that to your custom store if you want to fire the event across all stores. Or, set "Diligence.Events.defaultStore" to your own value.

Persistent Listeners

In the above example, the listeners would have to be re-subscribed when the application restarts, because it cannot guaranteed that application.distributedGlobals would keep its value. (Well, you can configure Hazelcast to persist the distributedGlobals map…)
Let's store our listeners in MongoDB, instead (the default is to use the "events" MongoDB collection):
Diligence.Events.subscribe({
	name: 'payments.successful',
	stores: new Diligence.Events.MongoDbCollectionStore(),
	id: 'sendThankYou',
	dependencies: '/politeness/acknowledgements/',
	fn: function(name, context) {
		logger.info('User {0} has paid us {1}!', context.username, context.amount)
		Acknowledgements.sendThankYou(context.username)
	}
})
Everything is otherwise the same. Neat!
You can also store events inside a specific, arbitrary MongoDB document, using Diligence.Events.MongoDbDocumentStore. This is a great way to keep events and their listeners (and the namespace for events) localized to a specific object without adding external mechanisms and storage.
Finally, you can create your own custom store class to store events anywhere else.

Configuration

You don't have to configure the Events Service, but it is possible to set a few defaults. In your application's "settings.js" add something like this to your app.globals:
app.globals = {
	...
	diligence: {
		service: {
			events: {
				defaultAsync: true,
				defaultDistributed: true,
				defaultStores: [function() {
					document.executeOnce('/diligence/service/events/')
					return new Diligence.Events.MongoDbCollectionStore()
				}]
			}
		}
	}
}
Note the use of function(): this is required in order to allow the Events Service to lazily create the service implementations on demand during runtime.

The Diligence Manual is provided for you under the terms of the Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. The complete manual is available for download as a PDF.

Download manual as PDF Creative Commons License