3
votes

I am in the process of learning Spring Integration and using it to implement a basic email service in Grails. What I want to be able to do is call my email service but only have the email be sent if the transaction trying to send the email is successful. Although this is being done in Grails, it really shouldn't be different from a regular Spring app except for using the BeanBuilder DSL instead of the XML configuration.

Anyway, here is my configuration for the channel:

beans = {
    xmlns integration:'http://www.springframework.org/schema/integration'
    integration.channel(id: 'email')
}

Here is my service:

class MailService {

    @ServiceActivator(inputChannel = "email")
    MailMessage sendMail(Closure callable) {
        //sending mail code
    }
}

Now what I expect to happen is that when I inject this MailService into another service and call send mail, that will place a message on the email channel, which will only get published if my transaction completes. What leads me to believe this is the section on UserProcess here: http://docs.spring.io/spring-integration/reference/html/transactions.html, which states that a user started process will have all the transactional properties that Spring provides.

I am attempting to test this with an integration test:

void "test transactionality"() {
        when:
        assert DomainObject.all.size() == 0
        DomainObject.withNewTransaction { status ->
            DomainObject object = buildAndSaveNewObject()
            objectNotificationService.sendEmails(object) //This service injects emailService and calls sendMail

            throw new Exception()
        }

        then:
        thrown(Exception) // This is true
        DomainObject.all.size() == 0 // This is true
        greenMail.receivedMessages.length == 0 // This fails
    }

What this does is create and save an object and send emails all within the same transaction. I then throw an exception to cause that transaction to fail. As expected, none of my domain objects are persisted. However, I still receive emails.

I am quite new to Spring Integration and Spring in general, so it's possible I'm misunderstanding how this is all supposed to work, but I would expect the sendMail message to never be placed on the email channel.

2

2 Answers

2
votes

It turns out that I don't think Spring Integration is the best way to achieve "only perform on commit" functionality (but if you do, Gary Russell's answer is the way to go.) You should instead use the TransactionSynchronizationManager provided as part of the Spring transaction management framework.

As an example, I created a TransactionService in grails:

class TransactionService {

    def executeAfterCommit(Closure c) {
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            void afterCommit() {
                c.call()
            }
        })
    }
}

You can then inject this anywhere you need it and use it like so:

def transactionService

transactionService.executeAfterCommit {
     sendConfirmationEmail()
}
1
votes

I don't know how this would be done in Grails, but in Java, you could use a transaction synchronization factory whereby you can take different actions depending on success/failure...

<int:transaction-synchronization-factory id="syncFactory">
    <int:after-commit expression="payload.renameTo('/success/' + payload.name)" channel="committedChannel" />
    <int:after-rollback expression="payload.renameTo('/failed/' + payload.name)" channel="rolledBackChannel" />
</int:transaction-synchronization-factory>

The result of the expression evaluation is sent to the channel, where you can have your outbound mail adapter.