Savory
The Scalable Prudence/MongoDB
Web Development Framework

It is now 11:21:57.06

Savory's Events Service

Every framework provides some generic way to send, listen to and fire one-way messages called "events". By decoupling event producer code from event consumer code, you can better organize your code architecture.

...Or not. The problem with events is that they are hooked up dynamically while the application runs. It's hard to know, simply looking at the code, what code will be triggered when an event is fired. Decoupling is a great way to introduce some really difficult bugs into your codebase. It's best not to use events if you don't actually need them!

Some frameworks go a step beyond simple code decoupling, and treat producers and consumers as separate components, where the producer cannot make any assumptions on the consumer's thread behavior. For example, a consumer might respond to events immediately, in thread, possibly tying up the producer's thread in the process. Or, it might have events queues up, and poll occassionally to handle them. In such highly generic situations, events are called "messages," and implementations often involve sophisticated middleware to queue messages, persist them, create inter-dependencies, and make sure they travel from source to destination via repeated attempts, back-off algorithms, notifications to system administrators, etc.

One size does not fit all. With Savory, we wanted to keep events lightweight: we assume that your consumer and producer components are all running inside a Prudence container: they are /resources/ and /web/dynamic/ documents that run in web request threads, or /tasks/ that run asynchronously. There's thus no need for generic middleware, though more sophisticated, dedicated messaging middleware is out there and available if you need it. (See RabbitMQ.)

On the other hand, the combination of Prudence Hazelcast clusters, MongoDB, and JavaScript's inherent dynamism within the Prudence container allows for some really scalable events! If what you need is asynchronicity and scalable distribution, rather than generic decoupling, then Savory events might be far more useful and simpler than deploying complex middleware.

Refer to the Savory.Events API documentation for more details.

In-Thread Events

First, the basics. Here's our "/libraries/politeness/acknowledgements.js":

Savory.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('/libraries/politeness/acknowledgements/')
	
Savory.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, like in this example.

Asynchronous Events

You can easily make the listeners run outside your thread, in fact anywhere in your Prudence cluster:

Savory.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 added "dependencies" to the listener, to allow it to be called in different contexts. These dependencies are document.executedOnce to make sure the thread has access to all the code it needs.

Firing it:

document.executeOnce('/libraries/politeness/acknowledgements/')

Savory.Events.fire({
	name: 'payments.successful',
	async: true,
	context: {username: user.name, id: user.id, amount: payment.amount}
})

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.

How does this magic work? It's JavaScript magic: the listener function's source code is serialized. The code that fires the event is called via Savory's tasks library. 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, make sure to store as little as possible in the listener function. It's best to just 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 subcribes 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 Savory.Events.GlobalsStore(application.distributedGlobals, 'myevents.')
	
Savory.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 is that with stored listeners is that they are inherently multithreaded: so, 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.

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:

Savory.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 "Events.defaultStores", so you can concat that to your custom store if you want to fire the event across all stores. Or, set "Events.defaultStore" to your own value.

Persistent Listeners

In the above example, the listeners would have to be re-subcribed 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):

Savory.Events.subscribe({
	name: 'payments.successful',
	stores: new Savory.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 "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 mechnisms and storage.

Finally, you can create your own custom store class to store events anywhere else.