package org.jboss.cache.transaction;



import EDU.oswego.cs.dl.util.concurrent.Latch;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.jboss.cache.DummyTransactionManagerLookup;
import org.jboss.cache.Fqn;
import org.jboss.cache.TreeCache;
import org.jboss.cache.lock.IsolationLevel;
import org.jboss.cache.lock.TimeoutException;

import javax.transaction.NotSupportedException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;



/**
 * Tests READ_COMMITED isolation level.
 *
 * @author <a href="mailto:ovidiu@jboss.org">Ovidiu Feodorov</a>
 *
 * @version $Id: IsolationLevelReadCommittedTest.java 3014 2006-11-22 18:42:24Z bstansberry $
 */

public class IsolationLevelReadCommittedTest extends TestCase
{

   private TreeCache cache = null;
   private final Fqn FQN = Fqn.fromString("/a/b/c");
   private final Fqn PARENT_FQN = Fqn.fromString("/a/b");
   private final String KEY = "key";
   private final String VALUE = "value";

   private volatile boolean writerFailed;
   private volatile boolean readerFailed;
   private volatile AssertionFailedError writerError;
   private volatile AssertionFailedError readerError;

   protected void setUp() throws Exception
   {
      super.setUp();

      writerFailed = false;
      readerFailed = false;

      writerError = null;
      readerError = null;
      
      cache = new TreeCache();
      cache.setCacheMode(TreeCache.LOCAL);
      cache.setIsolationLevel(IsolationLevel.READ_COMMITTED);
      cache.setTransactionManagerLookupClass(DummyTransactionManagerLookup.class.getName());
      cache.setLockAcquisitionTimeout(1000);
      cache.startService();
   }


   protected void tearDown() throws Exception
   {
      super.tearDown();

      cache.stopService();
      cache.destroyService();
      cache=null;
   }


   /**
    * The test starts a reader and writer thread and coordinates access to the cache so the reader
    * reads after the writer changes a node in a transaction, but before roll back. The reader
    * should never read anything else than the original value (uncommitted values should be
    * isolated)
    */
   public void testReadCommitted() throws Exception
   {
      final Latch readerCanRead = new Latch();
      final Latch readerDone = new Latch();
      final Latch writerCanWrite = new Latch();
      final Latch writerCanRollback = new Latch();
      final Latch writerDone = new Latch();
      
      cache.put(FQN, KEY, VALUE);
      assertEquals(VALUE, cache.get(FQN, KEY));

      // start a reader thread; need a transaction so its initial
      // read lock is maintained while the writer does its work

      Thread readerThread = new Thread(new Runnable()
      {
         public void run()
         {
            Transaction tx = null;
            try
            {
               tx = startTransaction();
               
               // Read the value, thus acquiring a read lock on FQN
               assertEquals(VALUE, cache.get(FQN, KEY));
               
               writerCanWrite.release();
               
               // wait until the writer thread changes the value in a transaction, but it did not
               // yet commit or roll back.
               readerCanRead.acquire();

               try
               {
                  // I shouldn't be able to see the "dirty" value
                  assertEquals("thread w/ read lock " +
                               "can see subsequent uncommitted changes",
                                VALUE, cache.get(FQN, KEY));
               }
               catch (TimeoutException good)
               {
                  // this is what should happen due to writer's WL
               }
               
               // let the writer know it can rollback
               writerCanRollback.release();

               // I should still be able to see the "clean" value
               assertEquals(VALUE, cache.get(FQN, KEY));
            }
            catch (AssertionFailedError e)
            {
               readerError = e;
            }
            catch(Throwable t)
            {
               t.printStackTrace();
               readerFailed = true;
            }
            finally
            {               
               System.out.println("reader thread exits");
               if (tx != null)
                  try { tx.commit(); } catch (Exception e) {}
               writerCanWrite.release();
               writerCanRollback.release();
               readerDone.release();
            }
         }
      }, "READER");
      readerThread.start();


      // start a writer thread and a transaction

      Thread writerThread = new Thread(new Runnable()
      {
         public void run()
         {
            try
            {
               // wait until the reader thread reads and allows me to start
               writerCanWrite.attempt(3000);
               
               Transaction tx2 = startTransaction();

               // change VALUE in a transaction
               cache.put(FQN, KEY, "this-shouldnt-be-visible");

               // notify the reading thread
               readerCanRead.release();

               // wait until the reader thread reads and allows me to rollback or until I timeout
               writerCanRollback.attempt(3000);

               System.out.println("rolling back");

               tx2.rollback();
            }
            catch (AssertionFailedError e)
            {
               writerError = e;
            }
            catch(Throwable t)
            {
               t.printStackTrace();
               writerFailed = true;
            }
            finally
            {
               System.out.println("writer thread exits");
               readerCanRead.release();
               writerDone.release();
            }
         }
      }, "WRITER");
      writerThread.start();

      // wait for both threads to finish
      readerDone.acquire();
      writerDone.acquire();

      // If any assertion failed, throw on the AssertionFailedError
      if (readerError != null)
      {
         throw readerError;
      }
      
      if (writerError != null)
      {
         throw writerError;
      }
      
      if (readerFailed)
      {
         fail("The reader thread exited incorrectly. Watch the log for previous stack traces");
      }

      if (writerFailed)
      {
         fail("The writer thread exited incorrectly. Watch the log for previous stack traces");
      }
   }
   
   /**
    * Test creates a cache node then starts a separate thread that removes
    * the node inside a tx. Test confirms that the removal cannot be seen
    * before the test commits.
    * 
    * @throws Exception
    */
   public void testNodeRemoved() throws Exception
   {
      final Latch readerCanRead = new Latch();
      final Latch readerDone = new Latch();
      final Latch writerDone = new Latch();

      cache.put(FQN, KEY, VALUE);
      assertEquals(VALUE, cache.get(FQN, KEY));

      // start a writer thread and a transaction

      Thread writerThread = new Thread(new Runnable()
      {
         public void run()
         {
            try
            {
               Transaction tx = startTransaction();

               // change VALUE in a transaction
               cache.remove(PARENT_FQN);

               // notify the reading thread
               readerCanRead.release();
               
               readerDone.acquire();
               
               tx.commit();
            }
            catch (AssertionFailedError e)
            {
               writerError = e;
            }
            catch(Throwable t)
            {
               t.printStackTrace();
               writerFailed = true;
            }
            finally
            {
               System.out.println("writer thread exits");
               readerCanRead.release();
               writerDone.release();
            }
         }
      }, "WRITER");
      writerThread.start();
      
      try
      {
         // wait until the writer thread changes the value in a transaction, 
         // but it did not yet commit or roll back.
         readerCanRead.acquire();

         // I shouldn't be able to see the "dirty" value
         assertEquals("2nd thread cannot see uncommitted changes",
                       VALUE, cache.get(FQN, KEY));
      }
      catch (TimeoutException t)
      {
         // ignore, this is good
      }
      finally
      {               
         System.out.println("reader thread exits");
         readerDone.release();
      }

      // wait for the writer to finish
      writerDone.acquire();

      assertNull("Node was removed", cache.get(FQN));
      
      // If any assertion failed, throw on the AssertionFailedError
      
      if (writerError != null)
      {
         throw writerError;
      }

      if (writerFailed)
      {
         fail("The writer thread exited incorrectly. Watch the log for previous stack traces");
      }      
   }

   private Transaction startTransaction() throws SystemException, NotSupportedException
   {
      DummyTransactionManager mgr=DummyTransactionManager.getInstance();
      mgr.begin();
      return mgr.getTransaction();
   }

   public static Test suite() {

      return new TestSuite(IsolationLevelReadCommittedTest.class);

   }

}

