Fork me on GitHub

3 May 2010

Asynchronous application events in Grails

On the project for my current client we've been using JMS in a rather naïve way for some time now. We've also experienced a certain amount of pain getting JMS and ActiveMQ configured correctly. However, all we're really using JMS for is asynchronous event broadcasting. Essentially we have a handful actions such as flushing hardware caches and notifying external systems that take place when a document changes. We don't want these things blocking the request thread when users save data.

After wrestling with JMS one too many times we decided to take a look at Spring's event framework instead. It turns out it's extremely easy to use for these kinds of asynchronous notifications in a Grails application.

Essentially any artefact can publish an event to the Spring application context. A simple publishing service can be implemented like this:
import org.springframework.context.*

class EventService implements ApplicationContextAware {

    boolean transactional = false

    ApplicationContext applicationContext

    void publish(ApplicationEvent event) {
        println "Raising event '$event' in thread ${Thread.currentThread().id}"
        applicationContext.publishEvent(event)
    }
}
So a Grails domain class can then do something like this:
def eventService

void afterInsert() {
    eventService.publish(new DocumentEvent(this, "created"))
}

void afterUpdate() {
    eventService.publish(new DocumentEvent(this, "updated"))
}

void afterDelete() {
    eventService.publish(new DocumentEvent(this, "deleted"))
}
Grails services make ideal ApplicationListener implementations. As services are singleton Spring beans they are automatically discovered by Spring's event system without any configuration required. For example:
import org.springframework.context.*

class EventLoggingService implements ApplicationListener<DocumentEvent> {

    boolean transactional = false

    void onApplicationEvent(DocumentEvent event) {
        println "Recieved event '$event' in thread ${Thread.currentThread().id}"
    }
}
Of course, multiple listeners can respond to the same events.

If you run the code you will notice that by default Spring's event system processes events synchronously. The EventService and ApplicationListener will print out the same Thread id. This is not ideal if any of the listener implementations might take any time. Luckily it's easy to override the ApplicationEventMulticaster bean in resources.groovy so that it uses a thread pool:
import java.util.concurrent.*
import org.springframework.context.event.*

beans = {
    applicationEventMulticaster(SimpleApplicationEventMulticaster) {
        taskExecutor = Executors.newCachedThreadPool()
    }
}
Running the code again will show the event being published in one thread and consumed in another. If you have multiple listeners each one will be executed in its own thread.

Oddly, I would have thought it was possible to override the taskExecutor property of the default ApplicationEventMulticaster in Config.groovy using Grails' property override configuration, but I found the following didn't work:
beans {
    applicationEventMulticaster {
        taskExecutor = Executors.newCachedThreadPool()
    }
}

19 comments:

Victor Hugo Germano said...

Hello!

Just wondering(noob question): can I use the same features through a simple web server like tomcat? Or only through spring application server?

Rob said...

The code is based on the Spring framework API but yeah it runs in a standard Grails app on Tomcat.

Brad Rhoads said...

Would you recommend this technique for implementing simple work flow?

For example, I have a record that starts in worker queue. I want to check when certain fields have been completed and then move the record into the supervisor queue.

Rob said...

In that case what would fire the event? Perhaps the onUpdate of the domain class? Sounds reasonable I guess. Quartz would be another option.

NyWebGuy said...

Robert,

Thanks a lot for posting. I think this will resolve a design issue I've been trying to figure out for a while.

Jean

foxgem said...

Great Post!

Another skill, you can define a property named grailsApplication in your Service to get the Instance of GrailsApplication. Then get ApplicationContext from it. The following is a sample:

class EventService {

def grailsApplication

boolean transactional = false

void publish(def event) {
println "Raising event '$event' in thread ${Thread.currentThread().id}"
grailsApplication.mainContext.publishEvent(event)
}
}

cheers

Luke Daley said...

What didn't work about using property overrides?

Rob said...

@Luke: Purely that the taskExecutor property wasn't overridden. I've certainly used property override config successfully at other times so I'm not sure what the problem is.

vince99999999 said...

In the domain class, you are calling publish() with new DocumentEvent. Did you mean ApplicationEvent? If not, what is the import statement for DocumentEvent? I'm new to this stuff and it's not quite clear to me :^)

Rob said...

I'm assuming DocumentEvent is some kind of class that extends ApplicationEvent.

cristiancalugaru said...

Hello,

It seems that once you override the ApplicationEventMulticaster bean in resources.groovy(to set it to async), there is no longer support for lazy-loading properties of domain objects, hence you get LazyInititExceptions.

Anyone has any ideea about this?

Rob said...

@cristiancalugaru yes, I wrote the Grails Spring Events plugin based on this blog post & that includes a mechanism for ensuring the asynchronous event processing can access a Hibernate session.

cristiancalugaru said...

Hello Rob, thanks for your answer.

To be more precise, I have the following situation:
From a grails service, I'm doing:

applicationContext.publishEvent(new GenericIdEvent(id));

Then, I have an AsyncProcessingService, which implements ApplicationListener.
In it's onApplicationEvent() method, I'm fetching an entity based on the id I have published above.
Then when I want to get entity.myInnerProperty, I m getting the famous org.hibernate.LazyInitializationException: could not initialize proxy - no Session.

I have configured the threadpool in resources.groovy, just like in the example:
applicationEventMulticaster(SimpleApplicationEventMulticaster) {
taskExecutor = Executors.newCachedThreadPool()
}
The listener - AsyncProcessingService has the boolean transactional = false,
while the service that published the event has it set to true.

Am i missing something very obvious here?

Thanks

Rob said...

which version of the plugin are you using & which version of Grails?

Ajeesh P U said...

had to explicitly register my service for grails 2.1.. Only then were the events received. However with grails 2.2 they have built awesomeness with implicit event transmission capacity for controllers and even integration with spring integration

Chuck said...

Good Day Rob,

I am using the spring-events plugin in a Grails 2.2.4 application. I was able to configure it per your plugin README, and everything is working fine with the default retryPolicy. I would like to execute some code on the final failed attempt of a retryPolicy. I have a database domain object that gets populated via the 'event.source'. I would like to update the field on the last failed attempt, but I am not sure how to do that.

Thanks!

Simon said...

Hi Rob, I'm running Grails 2.2.0 and have been using the technique outlined in your article and it's been working great. For some reason I never noticed that it wasn't actually executing asynchronously until I bumped into a long running task the other day and started to investigate.

Coming back to your article I simply configured the resources.groovy to what you have outlined but now I'm getting really bizarre issues like domain objects rejecting validation because dateCreated is null (an attribute that Grails sets itself). Turn of the resources.groovy change and everything goes back to normal. Saves work no lack of session Exceptions.

I'm just wondering if I'm using an outdate technique now and should be perhaps using some other setting. This article is over 4 years old.

Catherine Augustine said...

Can I track events like login , logout and login failure?

Alice Taylor said...

I am very happy to read this. Appreciate your sharing

lennyfacetext.com