开发者

Sending events at the end of a transaction

开发者 https://www.devze.com 2023-03-27 21:26 出处:网络
I have an interface for a Service object that looks something like the following (simplified for brevity):

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.Hiber开发者_开发知识库nateTransactionManager 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.

Option 1: JMS

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.

Option 2: Transaction synchronization

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:

  • if an exception is thrown in the handling of the events in this case, your service will throw the exception, but the changes will already have been committed in the database.
  • if one of your listeners also writes into the database, it will have to do so in a new transaction.

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:

  • send the events and "store" them in a list in your event handling mechanism, but do not forward the events to the recivers. (I guess that is easy to implement)
  • forward the events from the list to the recviers if the transaction was sucessfull
  • delete the events from the list if the transaction has a roll-backed

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:

  1. the events fired by 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.
  2. Use a ThreadLocal to temporarily queue all the events and after the composite transactions commited, fire the events. This is not persistent and may fail, e.g. after commit and before all events have been dequeued. I'm not the fan of ThreadLocal but it's better than messing the business interfaces (ItemService) with non-related parameters. See this for spring related implementation.

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.

0

精彩评论

暂无评论...
验证码 换一张
取 消