/grails-spring-events

Grails plugin for dispatching Spring events asynchronously

Primary LanguageGroovyApache License 2.0Apache-2.0

Grails Spring Events Plugin

The Grails Spring Events plugin provides a lightweight mechanism for asynchronously publishing Spring application events.

The plugin adds a new ApplicationEventMulticaster to the Spring application context that processes events asynchronously and is capable of retrying certain types of notification failure.

Publishing events

Spring Events adds a bean called asyncEventPublisher to the Spring context. You can inject it into Grails artefacts and other Spring beans in the same way as any other bean dependency. To publish an event you just call the publishEvent method.

To make things even easier the plugin adds a publishEvent dynamic method to every domain class, controller and service in the application.

Example: Firing an event when a domain class is updated

class Pirate {
	
	String name
	
	void afterUpdate() {
		def event = new PirateUpdateEvent(this)
		publishEvent event
	}
}

class PirateUpdateEvent extends ApplicationEvent {
	PirateUpdateEvent(Pirate source) {
		super(source)
	}
}

Defining Listeners

Events are dispatched to any beans in the Spring context that implement the ApplicationListener interface. You can register listener beans in resources.groovy. Also, remember that Grails services are Spring beans, so simply implementing the interface in a service will automatically register it as a listener.

Filtering the type of event

The ApplicationListener interface has a generic type parameter that you can use to filter the types of event that a listener implementation will be notified about. Spring will simply not invoke your listener for other types of event.

Example: Using generics to filter the event type in a listener

class PirateUpdateResponderService implements ApplicationListener<PirateUpdateEvent> {
	
	void onApplicationEvent(PirateUpdateEvent event) {
		log.info "Yarrr! The dread pirate $event.source.name has been updated!"
	}
	
}

Retrying failed notifications

Listener implementations may declare a retryPolicy property of type grails.plugin.springevents.RetryPolicy (or declare a getRetryPolicy() method). If such a property is present and the listener throws grails.plugin.springevents.RetryableFailureException from the onApplicationEvent method it will be re-notified at some time in the future according to the retryPolicy value. Throwing any other exception type will not result in notification being retried.

Note: A RetryableFailureException thrown by a listener implementation is treated just like any other exception if the listener does not declare a retryPolicy.

The RetryPolicy class simply defines the rules governing how and when to re-notify the listener of any events it fails to handle. It defines the following properties:

  • maxRetries: The maximum number of times that the listener will be re-notified of an event. After maxRetries is reached an exception is thrown and will be handled as any other exception thrown by the listener would be. A value of -1 indicates that the listener should be re-notified indefinitely until it successfully processes the event. Defaults to 3.
  • initialRetryDelayMillis: The initial period in milliseconds that the service will wait before re-notifying the listener. Defaults to 1 minute.
  • backoffMultiplier: The multiplier applied to the retry timeout before the second and subsequent retry. For example with a backoffMultiplier of 2 and initialRetryDelayMillis of 1000 the listener will be re-notified after 1000 milliseconds, 2000 milliseconds, 4000 milliseconds, 8000 milliseconds and so on. A backoffMultiplier of 1 would mean the listener will be re-notified at a fixed interval until it successfully handles the event or maxRetries is exceeded. Defaults to 2.

Example: A listener that calls an unreliable external service:

class UnreliableListener implements ApplicationListener {
	
	def unreliableService
	final RetryPolicy retryPolicy = new RetryPolicy()
	
	void onApplicationEvent(ApplicationEvent event) {
		if (unreliableService.isAvailable()) {
			unreliableService.doSomething()
		} else {
			throw new RetryableFailureException("the unreliable service is currently unavailable")
		}
	}
}

In this example the listener throws RetryableFailureException to indicate that the external service it attempts to call is not currently available and notification should be attempted later.

Customising the multicaster

The multicaster has several default dependencies that can be overridden using Grails' property override configuration mechanism.

Handling notification errors

If a listener throws an exception from its onApplicationEvent method (or its retry policy's maxRetries is exceeded) then the multicaster will notify its error handler. The default error handler simply logs errors but you can override it by assigning a different ErrorHandler implementation to the service in Config.groovy:

Customising threading policy

The multicaster uses a ExecutorService to poll the queue and notify the target listener. By default the service uses a single thread but you can use an alternate ExecutorService implementation by overriding the service's taskExecutor property in Config.groovy.

Similarly the service uses a ScheduledExecutorService to re-queue failed notifications after the delay specified by the listener's retry policy. The default implementation uses a single thread which can be overridden by setting the property retryScheduler in Config.groovy.

Example: Overriding the dependencies of the multicaster in Config.groovy:

beans {
	applicationEventMulticaster {
		errorHandler = new SomeErrorHandlerImpl()
		taskExecutor = java.util.concurrent.Executors.newCachedThreadPool()
		retryScheduler = java.util.concurrent.Executors.newScheduledThreadPool(5)
	}
}

Sending notifications synchronously

Sometimes you might need to send notifications synchronously rather than via the ExecutorService. For example, it can make testing more straightforward. If you need to do this just set the multicaster's dispatchMode property to grails.plugin.springevents.DispatchMode.SYNCHRONOUS.