-->
These old forums are deprecated now and set to read-only. We are waiting for you on our new forums!
More modern, Discourse-based and with GitHub/Google/Twitter authentication built-in.

All times are UTC - 5 hours [ DST ]



Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 
Author Message
 Post subject: Trouble with changing values in <map> from multiple th
PostPosted: Thu Jan 19, 2006 7:33 pm 
Newbie

Joined: Thu Jan 19, 2006 2:27 pm
Posts: 4
In trying to test out Hibernate performance before using it in an application, I'm running into trouble with one aspect of my testing. I'm trying to create a bunch of threads (10-50), and get them to retreive, modify, and store some objects from the database, a specific number of times.

One of the modifications they make is to increment a value, called "commits". This value is in a map. It's the "data" column/field of the "intdata" map, as shown in my mapping document. (It is NOT the "commits" property, it's a name/value pair in the "intdata" map. The "commits" property was for some other testing.) If the commit fails, for any reason, the transaction is rolled back, the session closed, and the iteration is retried. This keeps happening until the commit succeeds.

At the end of the performance test, I retrieve and print out that same "commits" value, from the "intdata" of each entity, and I expect them all to be equal to the number of threads * iterations in each thread. When using a single thread this is always the case.

However, when using multiple threads, often, the value I get is significantly lower than the one I expect.

As far as I understand, this is something transactions are supposed to help you do, so I'm sure I'm doing something wrong, but I can't for the life of me figure out what.

Any help educating my on my mistakes (Messy Java aside, it's throw-away code) would be much appreciated. :)

Hibernate version: 3.1 rc3

Mapping documents:
Code:
<class name="com.lewisd.test.Entity" table="entity">
   <cache usage="read-write" />
        <id name="id" column="id">
            <generator class="increment"/>
        </id>
   <version name="version" type="long" />
   <property name="commits" />
   <map name="intdata" table="intdata" >
      <cache usage="read-write" />
      <key column="id" />
      <map-key column="name" type="string" />
      <element column="data" type="int" />
   </map>
   <map name="stringdata" table="stringdata" >
      <cache usage="read-write" />
      <key column="id" />
      <map-key column="name" type="string" />
      <element column="data" type="string" />
   </map>
</class>


The Entity class is nothing special, just a POJO, straightforward getter/setter methods.

hibernate.xml (fragment):
Code:
<session-factory>
        <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="connection.url">jdbc:mysql://localhost/hibernate</property>
        <property name="connection.username">lewisd</property>
        <property name="connection.password">mine123</property>

        <!-- JDBC connection pool (use the built-in) -->
        <property name="connection.pool_size">20</property>

        <!-- SQL dialect -->
        <property name="dialect">org.hibernate.dialect.MySQLDialect</property>

        <!-- Echo all executed SQL to stdout -->
        <property name="show_sql">true</property>

        <!-- Drop and re-create the database schema on startup -->
        <property name="hbm2ddl.auto">create</property>

      <property name="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property>
      <property name="hibernate.cache.use_second_level_cache">false</property>


      <property name="hibernate.transaction.factory_class">org.hibernate.transaction.JDBCTransactionFactory</property>
      <property name="hibernate.current_session_context_class">thread</property>
        <mapping resource="Entity.hbm.xml"/>

    </session-factory>


Code between sessionFactory.openSession() and session.close():
This is my entire test class code:

Code:
/*
* Created on 14-Jan-2006
*/
package com.lewisd.test;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Session;
import org.hibernate.StaleStateException;
import org.hibernate.Transaction;

public class TestPerformance<V> implements Callable<V>
{

   private static final Log log = LogFactory.getLog( TestPerformance.class );

   private static final int ITERATIONS = 50;

   private static final int THREADS = 2;

   private static final int ENTITIES = 1;
   
   private static int errors = 0;
   private static int stale = 0;
   
   public static void main( String args[] ) throws InterruptedException
   {
      ExecutorService executor = Executors.newFixedThreadPool( THREADS );
      List<Callable<Object>> tasks = new ArrayList<Callable<Object>>( THREADS );
      for ( int i = 0; i < THREADS; ++i )
      {
         tasks.add( new TestPerformance<Object>() );
      }

      setup();

      long t0 = System.currentTimeMillis();
      List<Future<Object>> results = executor.invokeAll( tasks );
      executor.shutdown();
      
      long t1 = System.currentTimeMillis();
      log.info( "Total time: " + ( t1 - t0 ) / 1000.0 + " sec" );
      log.info( "Errors: " + errors );
      log.info( "Stale: " + stale );

      for ( Future<Object> result : results )
      {
         try
         {
            log.info( result.get() );
         }
         catch ( InterruptedException e )
         {
            e.printStackTrace();
         }
         catch ( ExecutionException e )
         {
            e.printStackTrace();
         }
      }
      executor.shutdown();

      checkCorrectness();

   }

   private static void setup()
   {
      Session s = HibernateUtil.currentSession();
      Transaction tx = s.beginTransaction();
      for ( int i = 0; i < ENTITIES; ++i )
      {
         Entity e = new Entity();
         s.save( e );
         e.getIntdata().put( "commits", 0 );
      }
      tx.commit();
      HibernateUtil.closeSession();
   }

   private static void checkCorrectness()
   {
      Session s = HibernateUtil.currentSession();
      List<Entity> entities = s.createQuery( "from Entity" ).list();
      for ( Entity e : entities )
      {
         long commits = e.getIntdata().get( "commits" );
         log.info( "commits = " + commits + " (Should be " + (THREADS*ITERATIONS) + ")" );
      }
      HibernateUtil.closeSession();
   }
   
   protected void incrementCommits()
   {
      Session s = HibernateUtil.currentSession();
      Transaction tx = s.beginTransaction();
      
      List<Entity> entities = s.createQuery( "from Entity" ).list();
      Collections.shuffle( entities );
      for ( Entity e : entities )
      {
         Map<String, Integer> intdata = e.getIntdata();
         intdata.put( "commits", intdata.get( "commits" ) + 1 );
      }
      s.flush();
      tx.commit();
      HibernateUtil.closeSession();
   }
   
   public V call()
   {
      log.info( "Invoking " + Thread.currentThread() );
      for ( int x = 0; x < ITERATIONS; ++x )
      {
         boolean success = false;
         while ( !success )
         {
            try
            {
               incrementCommits();
               success = true;
            }
            catch ( StaleStateException e )
            {
               log.error( e );
               stale++;
            }
            catch ( Exception e )
            {
               log.error( e );
               errors++;
            }
            if ( ! success )
            {
               HibernateUtil.currentSession().getTransaction().rollback();
               HibernateUtil.closeSession();
            }
         }
      }
      log.info( "Exiting " + Thread.currentThread() );
      return null;
   }

}


The HibernateUtil used here is straight from the example here:
http://www.hibernate.org/hib_docs/refer ... ngwithcats


Name and version of the database you are using:

I've tried this with both MySQL 4.1.14 and HSQL (whatever version shipped with Hibernate 3.1 rc3)

I'm reluctant to post the SQL/debug logs, because I can only duplicate this when doing something like 100 transactions, so it's a lot of log.
The full log is available here:
http://www.lewisd.com/files/hibernate-3.log

On a side note, something in the logging has been really frustrating.
In this sample:
Code:
Hibernate: update entity set version=?, commits=? where id=? and version=?
14:33:03,886 Thread-1 DEBUG LongType:79 - binding '24' to parameter: 1
14:33:03,886 Thread-1 DEBUG LongType:79 - binding '0' to parameter: 2
14:33:03,887 Thread-1 DEBUG LongType:79 - binding '1' to parameter: 3
14:33:03,887 Thread-1 DEBUG LongType:79 - binding '23' to parameter: 4
Hibernate: update intdata set data=? where id=? and name=?
14:33:03,889 Thread-1 DEBUG IntegerType:79 - binding '23' to parameter: 1
14:33:03,687 Thread-0 DEBUG IntegerType:123 - returning '3' as column: data0_
14:33:03,889 Thread-0 DEBUG StringType:123 - returning 'commits' as column: name0_
Hibernate: update entity set version=?, commits=? where id=? and version=?
14:33:03,891 Thread-0 DEBUG LongType:79 - binding '5' to parameter: 1
14:33:03,892 Thread-0 DEBUG LongType:79 - binding '0' to parameter: 2
14:33:03,894 Thread-0 DEBUG LongType:79 - binding '1' to parameter: 3
14:33:03,894 Thread-0 DEBUG LongType:79 - binding '4' to parameter: 4

You can see one log entry, timestamped 14:33:03,687 that is logged out of order from the test, at least as far as the timestamp goes. I'm not sure when this is actually happening, so it's been making my attempt to debug my problem somewhat awkward.

Here's the code for Entity, just for completeness, in case anyone would like to try to run this to help me out:
Code:
/*
* Created on 14-Jan-2006
*/
package com.lewisd.test;

import java.util.HashMap;
import java.util.Map;

public class Entity
{
   
   private long id;
   private long version;
   private Map<String, Integer> intdata = new HashMap<String, Integer>();
   private Map<String, String> stringdata = new HashMap<String, String>();
   
   private long commits = 0;

   public Entity()
   {
      super();
   }

   public long getId()
   {
      return this.id;
   }

   public void setId( long id )
   {
      this.id = id;
   }

   public Map<String, Integer> getIntdata()
   {
      return this.intdata;
   }

   public void setIntdata( Map<String, Integer> intdata )
   {
      this.intdata = intdata;
   }

   public Map<String, String> getStringdata()
   {
      return this.stringdata;
   }

   public void setStringdata( Map<String, String> stringdata )
   {
      this.stringdata = stringdata;
   }

   public long getVersion()
   {
      return this.version;
   }

   public void setVersion( long version )
   {
      this.version = version;
   }

   public long getCommits()
   {
      return this.commits;
   }

   public void setCommits( long commits )
   {
      this.commits = commits;
   }

}


Top
 Profile  
 
 Post subject:
PostPosted: Thu Jan 19, 2006 9:40 pm 
Expert
Expert

Joined: Thu Dec 23, 2004 9:08 pm
Posts: 2008
You haven't a single line of synchronized java. Transactions do not help you sychronize, they ensure that either all of a chunk of work is done, or none of it is. Your code will have multiple threads reading a number from the DB, incrementing it, and all writing that same (incremented only once) number back (sequentially, with no deadlocks, thanks to the transactions). However, you haven't written any code that ensures that threads do not read the number after another thread has read it but before that other thread has written back the incremented value.


Top
 Profile  
 
 Post subject:
PostPosted: Thu Jan 19, 2006 9:42 pm 
Expert
Expert

Joined: Thu Dec 23, 2004 9:08 pm
Posts: 2008
Oops, forgot to post a solution. Synchronize the incrementCommits method, that should be enough.


Top
 Profile  
 
 Post subject:
PostPosted: Fri Jan 20, 2006 12:38 pm 
Newbie

Joined: Thu Jan 19, 2006 2:27 pm
Posts: 4
Hmm. I must be misunderstanding transactions then. I thought the "version" property on the objects meant that if thread #1 read the object from the db (with commits=0), and then thread #2 read it (with commits=0), then #1 increments and commits, it also increments and commits the version. I thought that when thread #2 tries to commit then, that the commit fails with that StaleStateException because the version in the database was not the same as the version that thread #2 thinks it's updating.
The problem with synchronizing the entire incrementCommits method, is that my entire "application" may as well become single threaded, because 100% of the work is being done inside a giant synchronized block. Is there no other way to do it? As I said, I thought the versions were supposed to at least somewhat handle this.


Top
 Profile  
 
 Post subject:
PostPosted: Mon Jan 23, 2006 4:55 pm 
Expert
Expert

Joined: Thu Dec 23, 2004 9:08 pm
Posts: 2008
You can switch to "application" level transactions: JTA or an equivalent. JDBC transactions are simple DB transactions; they need some augmentation before being properly thread-robust.

With pessimistic locking and read-often-write-once strategies you can continue to use JDBC transactions in massively-multithreaded apps. The version tag doesn't guarantee that your updates will always work in order, but it will (try to) guarantee that updates will never happen out of order. You'll get exceptions and rollbacks instead of inaccurate commits: I presume that this is exactly what your StaleStateException is.


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jan 24, 2006 5:03 pm 
Newbie

Joined: Thu Jan 19, 2006 2:27 pm
Posts: 4
The thing that really puzzles me is that the behavior of Hibernate's StaleObjectStateException does seem to be doing exactly as I expect most of the time, with respect to the versioning.
I've dug into the Hibernate code too, and found this place where it's being thrown:
org.hibernate.persister.entity.AbstractEntityPersister.check
Code:
protected boolean check(int rows, Serializable id, int tableNumber) throws HibernateException {
      if ( rows < 1 ) {
         if ( !isNullableTable( tableNumber ) ) {
            if ( getFactory().getStatistics().isStatisticsEnabled() ) {
               getFactory().getStatisticsImplementor()
                     .optimisticFailure( getEntityName() );
            }
            throw new StaleObjectStateException( getEntityName(), id );
         }
      }
      else if ( rows > 1 ) {
         throw new HibernateException(
               "Duplicate identifier in table for: " +
               MessageHelper.infoString( this, id, getFactory() )
         );
      }
      return rows > 0; //it could be zero if we have a "nullable" table
   }

I have a test where I do something in two threads with one object in the database, in a very specific order (signal and wait to force it to happen in this order)
Main thread: create object
Thread 1: load object from DB
Thread 2: load object from DB
Thread 1: modify intdata value in object (set to 1)
Thread 2: modify intdata value in object (set to 2)
Thread 1: commit (succeeds fine)
Thread 2: commit (throws StaleObjectStateException from the code sample above)

The first commit executes these updates:
Code:
Hibernate: update entity set version=?, commits=? where id=? and version=?
12:44:51,571 Thread-0 DEBUG LongType:79 - binding '2' to parameter: 1
12:44:51,572 Thread-0 DEBUG LongType:79 - binding '0' to parameter: 2
12:44:51,572 Thread-0 DEBUG LongType:79 - binding '1' to parameter: 3
12:44:51,573 Thread-0 DEBUG LongType:79 - binding '1' to parameter: 4
Hibernate: update intdata set data=? where id=? and name=?
12:44:51,575 Thread-0 DEBUG IntegerType:79 - binding '1' to parameter: 1
12:44:51,576 Thread-0 DEBUG LongType:79 - binding '1' to parameter: 2
12:44:51,576 Thread-0 DEBUG StringType:79 - binding 'commits' to parameter: 3

Filling in the blanks:
Code:
update entity set version=2, commits=0 where id=1 and version=1
update intdata set data=1 where id=1 and name='commits'


That failed commit occurs with the following update statement:
Code:
Hibernate: update entity set version=?, commits=? where id=? and version=?
12:44:51,581 Thread-1 DEBUG LongType:79 - binding '2' to parameter: 1
12:44:51,583 Thread-1 DEBUG LongType:79 - binding '0' to parameter: 2
12:44:51,583 Thread-1 DEBUG LongType:79 - binding '1' to parameter: 3
12:44:51,584 Thread-1 DEBUG LongType:79 - binding '1' to parameter: 4

Filling in the blanks gets:
Code:
update entity set version=2, commits=0 where id=1 and version=1


So, by the time the last update there happens, there is no row matching the were clause, because the version has been changed to 2. The Hibernate code snippet up there catches this, and throws the StaleObjectStateException.

What I can't figure out is why, in the original code I posted, it sometimes succeedes at the commit, even when it's committing an older value.

The only way it makes sense for me that that could happen is if somehow, Hibernate does the SQL update with the version update in it, in one thread, and then another thread does the same version update, but even then, in the database itself, shouldn't that second update modify zero rows, because the first update modified it already? Is there some database transaction isolation level I need to configure for that to behave as I expect?


Top
 Profile  
 
 Post subject:
PostPosted: Tue Jan 24, 2006 7:53 pm 
Newbie

Joined: Thu Jan 19, 2006 2:27 pm
Posts: 4
I think I've solved this.

As far as I can tell, everything that both tenwit and myself has said is correct, but isn't exactly what I was looking for.

As it turns out, the dialect I was using for MySQL creates the tables as "MyISAM" tables, which are NOT fully transactional. Changing the dialect to org.hibernate.dialect.MySQLInnoDBDialect seems to have fixed the problem.

For HypersonicSQL, the problem is similar.
This page:
http://hsqldb.org/doc/guide/ch02.html#N104FC
states
Quote:
If two transactions modify the same row, no exception is raised when both transactions are committed. This can be avoided by designing your database in such a way that application data consistency does not depend on exclusive modification of data by one transaction. You can set a database property to cause an exception when this happens.

I added this code:
Code:
session.connection().createStatement().execute( "SET PROPERTY \"sql.tx_no_multi_rewrite\" TRUE" );

to my setup method, and that fixes the problem in HSQL.

It might be that some of this should be added to the Hibernate documentation? It took me quite a bit of digging around to find out that the MySQL dialect that's used in all the examples creates the tables with a "backing store" (storage plugin, or whatever it's called) that doesn't fully support transactions, and that HypersonicSQL's transaction support is limited.


Top
 Profile  
 
Display posts from previous:  Sort by  
Forum locked This topic is locked, you cannot edit posts or make further replies.  [ 7 posts ] 

All times are UTC - 5 hours [ DST ]


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum

Search for:
© Copyright 2014, Red Hat Inc. All rights reserved. JBoss and Hibernate are registered trademarks and servicemarks of Red Hat, Inc.