Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

How To Fix Optimistic Locking Race Conditions With Pessimistic Locking

DZone's Guide to

How To Fix Optimistic Locking Race Conditions With Pessimistic Locking

· Java Zone
Free Resource

Learn how to troubleshoot and diagnose some of the most common performance issues in Java today. Brought to you in partnership with AppDynamics.

In my previous post, I explained the benefits of using explicit optimistic locking. As we then discovered, there’s a very short time window in which a concurrent transaction can still commit a Product price change right before our current transaction gets committed.

This issue can be depicted as follows:

ExplicitLockingLockModeOptimisticRaceCondition

  • Alice fetches a Product
  • She then decides to order it
  • The Product optimistic lock is acquired
  • The Order is inserted in the current transaction database session
  • The Product version is checked by the Hibernate explicit optimistic locking routine
  • The price engine manages to commit the Product price change
  • Alice transaction is committed without realizing the Product price has just changed

Replicating the issue

So we need a way to inject the Product price change in between the optimistic lock check and the order transaction commit.

After analyzing the Hibernate source code, we discover that theSessionImpl.beforeTransactionCompletion() method is calling the current configuredInterceptor.beforeTransactionCompletion() callback, right after the internalactionQueue stage handler (where the explicit optimistic locked entity version is checked):

public void beforeTransactionCompletion(TransactionImplementor hibernateTransaction) {
    LOG.trace( "before transaction completion" );
    actionQueue.beforeTransactionCompletion();
    try {
        interceptor.beforeTransactionCompletion( hibernateTransaction );
    }
    catch (Throwable t) {
        LOG.exceptionInBeforeTransactionCompletionInterceptor( t );
    }
}   

Armed with this info, we can set-up a test to replicate our race condition:

private AtomicBoolean ready = new AtomicBoolean();
private final CountDownLatch endLatch = new CountDownLatch(1);

@Override
protected Interceptor interceptor() {
    return new EmptyInterceptor() {
        @Override
        public void beforeTransactionCompletion(Transaction tx) {
            if(ready.get()) {
                LOGGER.info("Overwrite product price asynchronously");

                executeNoWait(new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        Session _session = getSessionFactory().openSession();
                        _session.doWork(new Work() {
                            @Override
                            public void execute(Connection connection) throws SQLException {
                                try(PreparedStatement ps = connection.prepareStatement("UPDATE product set price = 14.49 WHERE id = 1")) {
                                    ps.executeUpdate();
                                }
                            }
                        });
                        _session.close();
                        endLatch.countDown();
                        return null;
                    }
                });
                try {
                    LOGGER.info("Wait 500 ms for lock to be acquired!");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    };
}

@Test
public void testExplicitOptimisticLocking() throws InterruptedException {
    try {
        doInTransaction(new TransactionCallable<Void>() {
            @Override
            public Void execute(Session session) {
                try {
                    final Product product = (Product) session.get(Product.class, 1L, new LockOptions(LockMode.OPTIMISTIC));
                    OrderLine orderLine = new OrderLine(product);
                    session.persist(orderLine);
                    lockUpgrade(session, product);
                    ready.set(true);
                } catch (Exception e) {
                    throw new IllegalStateException(e);
                }
                return null;
            }
        });
    } catch (OptimisticEntityLockException expected) {
        LOGGER.info("Failure: ", expected);
    }
    endLatch.await();
}

protected void lockUpgrade(Session session, Product product) {}

When running it, the test generates the following output:

#Alice selects a Product
DEBUG [main]: Query:{[select abstractlo0_.id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_.id=?][1]} 

#Alice inserts an OrderLine
DEBUG [main]: Query:{[insert into order_line (id, product_id, unitPrice, version) values (default, ?, ?, ?)][1,12.99,0]} 
#Alice transaction verifies the Product version
DEBUG [main]: Query:{[select version from product where id =?][1]} 

#The price engine thread is started
INFO  [main]: c.v.h.m.l.c.LockModeOptimisticRaceConditionTest - Overwrite product price asynchronously
#Alice thread sleeps for 500ms to give the price engine a chance to execute its transaction
INFO  [main]: c.v.h.m.l.c.LockModeOptimisticRaceConditionTest - Wait 500 ms for lock to be acquired!

#The price engine changes the Product price
DEBUG [pool-1-thread-1]: Query:{[UPDATE product set price = 14.49 WHERE id = 1][]} 

#Alice transaction is committed without realizing the Product price change
DEBUG [main]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection

So, the race condition is real. It’s up to you to decide if your current application demands stronger data integrity requirements, but as rule of thumb, better safe than sorry.

Fixing the issue

To fix this issue, we just need to add a pessimistic lock request just before ending our transactional method.

@Override
protected void lockUpgrade(Session session, Product product) {
    session.buildLockRequest(new LockOptions(LockMode.PESSIMISTIC_READ)).lock(product);
}

The explicit shared lock will prevent concurrent writes on the entity we’ve previously locked optimistically. With this method, no other concurrent transaction can change the Product prior to releasing this lock (after the current transaction is committed or rolled back).

ExplicitLockingLockModeOptimisticRaceConditionFix

If you enjoy reading this article, you might want to subscribe to my newsletter and get a discount for my book as well.

Vlad Mihalcea&apos;s Newsletter

With the new pessimistic lock request in place, the previous test generates the following output:

#Alice selects a Product
DEBUG [main]: Query:{[select abstractlo0_.id as id1_1_0_, abstractlo0_.description as descript2_1_0_, abstractlo0_.price as price3_1_0_, abstractlo0_.version as version4_1_0_ from product abstractlo0_ where abstractlo0_.id=?][1]} 

#Alice inserts an OrderLine
DEBUG [main]: Query:{[insert into order_line (id, product_id, unitPrice, version) values (default, ?, ?, ?)][1,12.99,0]} 

#Alice applies an explicit physical lock on the Product entity
DEBUG [main]: Query:{[select id from product where id =? and version =? for update][1,0]} 

#Alice transaction verifies the Product version
DEBUG [main]: Query:{[select version from product where id =?][1]} 

#The price engine thread is started
INFO  [main]: c.v.h.m.l.c.LockModeOptimisticRaceConditionTest - Overwrite product price asynchronously
#Alice thread sleeps for 500ms to give the price engine a chance to execute its transaction
INFO  [main]: c.v.h.m.l.c.LockModeOptimisticRaceConditionTest - Wait 500 ms for lock to be acquired!

#The price engine cannot proceed because of the Product entity was locked exclusively, so Alice transaction is committed against the ordered Product price
DEBUG [main]: o.h.e.t.i.j.JdbcTransaction - committed JDBC Connection

#The physical lock is released and the price engine can change the Product price
DEBUG [pool-1-thread-1]: Query:{[UPDATE product set price = 14.49 WHERE id = 1][]} 

Even though we asked for a PESSIMISTIC_READ lock, HSQLDB can only execute a FOR UPDATE exclusive lock instead, equivalent to an explicit PESSIMISTIC_WRITElock mode.

If you enjoyed this article, I bet you are going to love my book as well.






Conclusion

If you wonder why we use both optimistic and pessimistic locking for our current transaction, you must remember that optimistic locking is the only feasible concurrency control mechanism for multi-request conversations.

In our example, The Product entity is loaded by the first request, using a read-only transaction. The Product entity has an associated version, and this read-time entity snapshot is going to be locked optimistically during the write-time transaction.

The pessimistic lock is useful only during the write-time transaction, to prevent any concurrent update from occurring after the Product entity version check. So, both the logical lock and the physical lock are cooperating for ensuring the Order price data integrity.

While I was working on this blog post, the Java Champion Markus Eisele took me an interview about the Hibernate Master Class initiative. During the interview I tried to explain the current post examples, while emphasizing the true importance of knowing your tools beyond the reference documentation.

Code available on GitHub.

Understand the needs and benefits around implementing the right monitoring solution for a growing containerized market. Brought to you in partnership with AppDynamics.

Topics:
java ,sql ,persistence ,tips and tricks

Published at DZone with permission of Vlad Mihalcea. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}