JPA: Concurrent Access to Database

Hi all,

I'm have a problem whereby concurrent access to the database through an MDB is creating duplicate entries in my database. I assume it's some kind of lock related problem.

Here's some pseudocode of my situation:

@MessageDriven(mappedName="someTopic")

publicclass MyClass{

publicvoid onMessage(Message jmsMessage){

player = (Player)((ObjectMessage)jmsMessage).getObject();

/* make player managed */

player = em.merge(player);

q = em.createQuery("select t from Team where :player member of t.players");

q.setParameter("player", player);

if (q.getResultList().isEmpty()){

/* add the player to a new team */

Team team =new Team();

team.addPlayer(player);

em.persist(team);

}

}

}

The problem eventuates when I have a message producer send a lot of messages with the same player. For example, 3 threads may call getResultList() before any team is persisted, therefore creating 3 teams with the same player.

I understand that you cannot use synchronized in ejbs, so is there any way of locking the database or bean so that I can do the getResultList() and persist() in one go, without another instance getting in the way?

Thanks!

[1762 byte] By [BigMuka] at [2007-11-27 6:19:50]
# 1

So there is a delay between the first select and the persist of the Team in which other message threads do a select and reveives empty result lists. Maybe manipulating the transaction so that one on message gets finished before others are executed.

Or have some table which is aware of an on message querying the existence of a Team entity. It requires a second select but you won't have to deal with transactions.

tobiassen666a at 2007-7-12 17:34:48 > top of Java-index,Enterprise & Remote Computing,Enterprise Technologies...
# 2

Unfortunately this approach is prone to the same race condition that I experienced above. If I write a flag to the database, who's to say that another thread wont read the flag before it is written?

I solved the problem by using the lock() function of EntityManager and an interesting feature of MDBs I wasn't aware of.

For anyone in the same boat, here's how I fixed the code (again, this is pseudocode, so take it with a grain of salt):

@MessageDriven(mappedName="someTopic")

public class MyClass {

public void onMessage(Message jmsMessage) {

player = (Player)((ObjectMessage)jmsMessage).getObject();

/* make player managed */

player = em.merge(player);

/* prevents same player from being written while in this transaction

* this works because the players team is added to the player object */

em.lock(player, LockModeType.WRITE);

q = em.createQuery("select t from Team where :player member of t.players");

q.setParameter("player", player);

if (q.getResultList().isEmpty()) {

/* add the player to a new team */

Team team = new Team();

team.addPlayer(player);

em.persist(team);

[b]em.flush();[/b]

}

}

}

You also have to add a @Version annotation to the Player class. ie:

public class Player {

.....

private int version;

@Version

public int getVersion() {

return version;

}

public void setVersion(int version) {

return version;

}

}

This works because the persistence layer gets the version number from the object before the changes are written to the database. Since there will be a different version for any subsequent, concurrent accesses, the database realises that it has dirty data on it's hands.

When this occurs, the entity manager fires a rollback exception. If you let this go through to the container (ie the onMessage function throws the exception), the container will try to re-deliver the message later, when it will eventually work.

Also notice the bolded em.flush(). This is needed to ensure that the exception is thrown while it is still in the onMessage() call, not after.

Hope that helps somebody, and people can feel free to correct me where I'm wrong!

BigMuka at 2007-7-12 17:34:48 > top of Java-index,Enterprise & Remote Computing,Enterprise Technologies...