I have an interface for a Service object that looks something like the following (simplified for brevity):
public interface ItemService {
public Item getItemById(String itemId, int version);
public void create(Item item, User user);
public void update(Item item, User user);
public void delete(Item item, User user);
}
ItemService
a single implementation, and is wired up as a Spring bean. It gets used by the UI portion of our project, and by the code that handles Ajax requests, to create and modify Item
objects in our data store.
Under the hood each method sends out a series of Events when it gets called. The Events are received by other modules to do things like keep Lucene indexes up to date, or to send messages to administrators to let them know something has changed. Each method call constitutes a single transaction in Spring (using org.springframework.orm.hibernate3.HibernateTransactionManager
and org.springframework.transaction.interceptor.TransactionProxyFactoryBean
).
Recently there has been a need to compose a number of method calls together in a single transaction. Sometimes with more than one Service. For example, we might want to do something like:
*Begin transaction*
Get Items created by User Bill using ItemService
for each Item in Items
Update field on Item
Link Item to User Bill with LinkService
Update Item using ItemService
*Finish transaction*
We've done this by creating another Service which allows you to compose calls from Services in a single method in the parent service. Lets call it ComposingService
. ComposingService
, as with all the others, is also managed by Spring, and as transactions are reentrant, this should all work..
However, there is a problem: if any of those operations within the transaction fail, causing the transaction to roll back we do not want to send out any Events whatsoever.
As it stands, if the transaction fails halfway through, half the Events will be sent by ItemService
before the transaction rolls back, meaning that some modules will receive a bunch of Events for things that haven't happened.
We are trying to find some way of fixing this, but we've been unable to think of anything elegant. The best we've come up with so far, is something like this (and it's ugly):
public interface ItemService {
public Item getItemById(String itemId, int version);
public void create(Item item, User user, List<Event> events);
public void update(Item item, User user, List<Event> events);
public void delete(Item item, User user, List<Event> events);
}
In this modified ItemService, instead of the Events being sent right away, they are added to the List of Events passed in as an argument. The list is maintained by ComposingService
, and the Events get sent by ComposingService
once all the calls to ItemService
and other services have exited successfully.
Obviously, the problem is that we've changed the contract on ItemService in an ugly manner. Calling classes, even if they are services, should not have to worry about managing Events. But I've been unable to think of a way around this, hence this question.
This looks like the kind of problem that has probably been solved before. Has anyone had a problem that looks similar, and if so, how did you resolve it?
Summarizing your question: You're looking for a transactionally-safe way to send messages.
Transactionally safe messaging is exactly what JMS is for. There's also good JMS integration in Spring, See the JMS chapter in the Spring documentation.
That will make sure that the messages are sent if and only if the transaction is committed. It also helps for dealing with errors in listeners for these events.
A difference with your current setup is that these events will be handled asynchronously: your service will return before these events have been handled. (JMS will make sure that they are processed eventually, it can be configured to try multiple times and how to deal with errors,...). Depending on your needs, that may be a good or a bad thing.
Alternatively, if JMS is too heavy-weight for your case, you could use transaction synchronization: When sending an event, instead of sending it directly use Spring's TransactionSynchronizationManager.registerSynchronization
, and send the message in the afterCommit()
of your TransactionSynchronization
.
You could either add a new synchronization per event to be sent, or add one synchronization and keep track of which events to be sent by binding an object containing that list to the transaction using TransactionSynchronizationManager.bindResource
.
I would advise against trying to use your own ThreadLocal
for this, because that would go wrong in some cases; for example if inside your transaction you would start a new transaction (RequiresNew
).
Differences with your current setup:
Alternatively, you can use beforeCommit
instead of afterCommit
, but then your events will be handled (mails sent,...) even if the actual commit to the database later fails.
This is less robust (less transactional), than using JMS, but lighter and easier to set up, and usually good enough.
In contrast to Wouter Coekaerts I understand that you are asking for an way to send notifications that will be only submitted to the reciver if the transaction in which they are created was succesfull. -- So you are looking for something that is similar to the transactional CDI-Event mechanism.
My idea to solve it is this way:
To forward or delete the events I would first have a lookt at the spring transaction mechanism. If there is no way to extend it, you could write an AOP aspect that forward the events if a method annotated with @Transactional
was leaving wihtout an (Runtime)Exception. But if the @Transactional
annotated Method was leaving with an (Runtime)Exception then delete the events from the list.
As @Wouter mentioned one alternative is using asynchronous messaging techniques. I can think of 2 other approaches:
ItemService
get stored in database table after commit (so on rollback they are not avialable). A background (async) job examines the events and calls the appropriate services. You could call it 'selfmade JMS'. Possible alternative if messaging infrastructure is not available.I guess a final answer is not possible as it really depends on the details of your system, especially all the external services that get triggered by the events.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With