Undeterministic behavior on Session.close without commit or rollback

Description

Closing a session without rollback or commit may cause the uncommitted changes to be committed.

First scenario:

Step 1. Obtain a session using SessionFactory.openSession, begin a transaction, makes some changes, don't commit, don't rollback, close the session.
Step 2. Obtain a new session, begin a transaction, do some unrelated changes, commit the transaction, close the session.

Result: the uncommitted changes performed in the first transaction are committed when the second transaction is committed.
This shouldn't happen, what happens with the second session should not affect the changes performed with the first session.
However, if instead of committing, you rollback the transaction from step 2, it also rollbacks the changes from step 1.
Even worse, the second session has access to the uncommitted data generated by the first transaction, violating transaction isolation.

Second scenario:

0. Configure a SessionFactory for an Oracle Database, with the c3p0 pool.
1. Obtain a session using SessionFactory.openSession, begin a transaction, makes some changes, don't commit, don't rollback, close the session.
2. Close the SessionFactory.

Result: the uncommitted changes performed in the first transaction are committed when the SessionFactory is closed, because all the connections in the pool are closed, and oracle connections perform an implicit commit on close.
If you use a different database, the changes will be rolled back.

To conclude the behavior when a session doesn't commit or rollback depends on:

  • what the next Session does with the Connection;

  • what database you are using.

What makes it worse is that the idiom recommended in the documentation for a non-managed environments (http://docs.jboss.org/hibernate/stable/core/reference/en/html/transactions.html#transactions-demarcation-nonmanaged) is not safe:

// Non-managed environment idiom
Session sess = factory.openSession();
Transaction tx = null;
try {
tx = sess.beginTransaction();

// do some work
...

tx.commit();
}
catch (RuntimeException e) {
if (tx != null) tx.rollback();
throw e; // or display error message
}
finally {
sess.close();
}

If "do some work" will throw java.lang.Error, it will not be caught by the catch clause for RuntimeException, the compiler will not complain that the Error is not caught, because a java.lang.Error behaves the same as RuntimeException, not requiring the programmer to catch it.

This way, both the commit and the rollback code will not be executed, which may cause the first or second scenario to occur.

What developers using hibernate may do until the issue is fixed:

  • replace "catch (RuntimeException e)" with "catch (Throwable t)" this will catch both java.lang.RuntimeException and java.lang.Error

This may not be a good idea though, because according to the java.lang.Error JavaDoc (http://java.sun.com/javase/6/docs/api/java/lang/Error.html): "a reasonable application should not try to catch" it.

  • a better solution might be to move the rollback code to the "finally" block, like this:

// Non-managed environment idiom
Session sess = factory.openSession();
Transaction tx = null;
try {
tx = sess.beginTransaction();

// do some work
...

tx.commit();
}
finally {
if (tx != null && !tx.wasCommitted()) tx.rollback();
sess.close();
}

What I feel the developers of Hibernate should do:

  • update the documentation to document the issue

  • modify Session.close to perform rollback if the transaction has not been explicitly committed or rollback. If for some reason this can't be the default behavior, at least provide a configuration parameter to activate such behavior. This will result in much simpler and safer code, that will look like this:

// Non-managed environment idiom
Session sess = factory.openSession();
try {
sess.beginTransaction();

// do some work
...

sess.getTransaction().commit();
}
finally {
sess.close();
}

Which reminds me that it would be also be nice if Session would have a commitTransaction method...

I attached example code that reproduces the problem.

Environment

Hibernate 3.3.0.SP1, Oracle 10.2.0.1.0, c3p0 pool

Activity

Show:
Vladimir Nicolici
October 27, 2009, 3:42 AM

First, this problem doesn't have anything to do with Oracle, it is just a coincidence I was using Oracle when I discovered the issue.

Yes, the second scenario can be only reproduced on Oracle, but when I discovered the bug I was hitting the first scenario, as the JDBC connection was never closed, and the issue still occurred. I first suspected I was hitting the second scenario, but I didn't.

So even if you can't see how it could happen unless I use JTA, it happened, and I was using managed session context, not jta or thread.

But, to be absolutely sure Oracle is not the cause of the problem, today I tested on Postgres 8.4.1, and the same issue occurred. A thread was able to see uncommitted data left by another thread, and was able to inadvertently commit or rollback the transaction left open by the other thread.

I will attach the source files for the test, and an executable JAR including all the required libraries.

To test, you will need:

  • a Postgres SQL database running on localhost on the default port, 5432, with the password for the "postgres" user set to "change.me";

  • a "hibernatetest" table in the "public" schema of the default "postgres" database, having a single varchar(30) column named "testcolumn", containing a single record with the text 'initial state'.

You can run the test with java -jar hibernate-test.jar on the same machine running the database.

What you will see is three threads run in sequence, with 5 second delays between them:

  • one thread updating the record, setting the text to 'uncommitted data';

  • the second thread will be able to read 'uncommitted data';

  • depending on Math.random the second thread will commit or rollback;

  • the third thread will see 'initial state' if the second thread performed a rollback or 'uncommitted data' if it performed a commit;

As for your accusation that the resources were not cleaned correctly, I fully agree, BUT, I was cleaning them as recommended by the hibernate documentation at http://docs.jboss.org/hibernate/stable/core/reference/en/html/transactions.html#transactions-demarcation-nonmanaged , meaning the documentation examples show "bad programming practice".

The examples in the documentation (or as it calls them "the idioms") fail to handle the case when java.lang.Error and its children are raised during a transaction, leading to the bug.

Since you closed the bug, I can no longer upload files here, so to be able to show "evidence to the contrary" I uploaded the source code here:

http://www.megaupload.com/?d=5V40YE45

and the executable jar file here:

http://www.megaupload.com/?d=EMMI3X5H

If you need more evidence, I'll be glad to provide it.

Steve Ebersole
October 25, 2009, 7:02 PM

The only way I can see scenario#1 happening is if you use JTA and the first session's begin-transaction actually starts the JTA transaction and the second session is obtained in the same context (however your JTA impl defines that for your environment) as the first session because now it is sharing the JTA tranasaction. This would also explain how the second session can see flushed, though yet uncommitted changes from the first session because they would actually be sharing the same JDBC connection per JTA. However, in this case the call to commit on the second session's "local transaction" would simply no-op because it did not start the underlying JTA transaction. And here we are back to Oracle committing uncompleted transactions instead of the more reasonable rolling back.

So unless I see evidence to the contrary, this to me is just an issue with Oracle and generally bad programming practice in terms of not cleaning up properly after oneself. If you had actually cleaned up your resources correctly, this "feature" of Oracle would never be an issue.

Vladimir Nicolici
October 23, 2009, 9:27 AM

I agree, but the first scenario is not related to Oracle, only the second, and the behavior is the same.

Steve Ebersole
October 23, 2009, 5:04 AM

Well unfortunately this is actually a "feature" of the Oracle server. Every other database of which I am aware will rollback work in an uncompleted transaction when the corresponding connection is closed; Oracle commits in that scenario.

Rejected

Assignee

Unassigned

Reporter

Vladimir Nicolici

Fix versions

None

Labels

None

backPortable

None

Suitable for new contributors

None

Requires Release Note

None

Pull Request

None

backportDecision

None

Components

Affects versions

Priority

Major