/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.test.cache.test.replicated;

import EDU.oswego.cs.dl.util.concurrent.FIFOSemaphore;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.jboss.cache.PropertyConfigurator;
import org.jboss.cache.TreeCache;
import org.jboss.cache.lock.IsolationLevel;
import org.jboss.cache.transaction.DummyTransactionManager;
import org.jboss.logging.Logger;

import javax.naming.Context;
import javax.transaction.NotSupportedException;
import javax.transaction.RollbackException;
import javax.transaction.SystemException;
import javax.transaction.Transaction;

/**
 * Replicated unit test for sync transactional TreeCache
 * Note: we use DummyTransactionManager for Tx purpose instead of relying on
 * jta.
 *
 * @version $Revision: 1.5.2.7 $
 */
public class SyncTxUnitTestCase extends TestCase {
   TreeCache cache1, cache2;
   int caching_mode=TreeCache.REPL_SYNC;
   final String group_name="TreeCacheTestGroup";
   String props=
         "UDP(ip_mcast=true;ip_ttl=64;loopback=false;mcast_addr=228.1.2.3;" +
         "mcast_port=45566;mcast_recv_buf_size=80000;mcast_send_buf_size=150000;" +
         "ucast_recv_buf_size=80000;ucast_send_buf_size=150000):" +
         "PING(down_thread=true;num_initial_members=2;timeout=500;up_thread=true):" +
         "MERGE2(max_interval=20000;min_interval=10000):" +
         "FD(down_thread=true;shun=true;up_thread=true):" +
         "VERIFY_SUSPECT(down_thread=true;timeout=1500;up_thread=true):" +
         "pbcast.NAKACK(down_thread=true;gc_lag=50;retransmit_timeout=600,1200,2400,4800;" +
         "up_thread=true):" +
         "pbcast.STABLE(desired_avg_gossip=20000;down_thread=true;up_thread=true):" +
         "UNICAST(down_thread=true;min_threshold=10;timeout=600,1200,2400;window_size=100):" +
         "FRAG(down_thread=true;frag_size=8192;up_thread=true):" +
         "pbcast.GMS(join_retry_timeout=2000;join_timeout=5000;print_local_addr=true;shun=true):" +
         "pbcast.STATE_TRANSFER(down_thread=true;up_thread=true)";

   final static Logger log_=Logger.getLogger(SyncTxUnitTestCase.class);
   String old_factory=null;
   final String FACTORY="org.jboss.cache.transaction.DummyContextFactory";
   FIFOSemaphore lock=new FIFOSemaphore(1);
   DummyTransactionManager tx_mgr;
   Throwable t1_ex, t2_ex;



   public SyncTxUnitTestCase(String name) {
      super(name);
   }

   public void setUp() throws Exception {
      super.setUp();
      old_factory=System.getProperty(Context.INITIAL_CONTEXT_FACTORY);
      System.setProperty(Context.INITIAL_CONTEXT_FACTORY, FACTORY);
      tx_mgr=DummyTransactionManager.getInstance();
      initCaches(TreeCache.REPL_SYNC);
      t1_ex=t2_ex=null;
   }

   public void tearDown() throws Exception {
      super.tearDown();
      DummyTransactionManager.destroy();
      destroyCaches();
      if(old_factory != null) {
         System.setProperty(Context.INITIAL_CONTEXT_FACTORY, old_factory);
         old_factory=null;
      }
   }

   Transaction beginTransaction() throws SystemException, NotSupportedException {
      tx_mgr.begin();
      return tx_mgr.getTransaction();
   }

   void initCaches(int caching_mode) throws Exception {
      this.caching_mode=caching_mode;
      cache1=new TreeCache();
      cache2=new TreeCache();
      PropertyConfigurator config=new PropertyConfigurator();
      config.configure(cache1, "META-INF/replSync-service.xml"); // read in generic replSync xml
      config.configure(cache2, "META-INF/replSync-service.xml"); // read in generic replSync xml
      cache1.setCacheMode(caching_mode);
      cache2.setCacheMode(caching_mode);

      cache1.setIsolationLevel(IsolationLevel.SERIALIZABLE);
      cache2.setIsolationLevel(IsolationLevel.SERIALIZABLE);
      /*
      cache1.setTransactionManagerLookupClass("org.jboss.cache.JBossTransactionManagerLookup");
      cache2.setTransactionManagerLookupClass("org.jboss.cache.JBossTransactionManagerLookup");
*/
      cache1.setLockAcquisitionTimeout(5000);
      cache2.setLockAcquisitionTimeout(5000);
      cache1.start();
      cache2.start();
   }

   void destroyCaches() throws Exception {
      cache1.stop();
      cache2.stop();
      cache1=null;
      cache2=null;
   }


   public void testLockRemoval() throws Exception {
      cache1.setSyncCommitPhase(true);
      cache1.releaseAllLocks("/");
      Transaction tx=beginTransaction();
      cache1.put("/bela/ban", "name", "Bela Ban");
      assertEquals(2, cache1.getNumberOfLocksHeld());
      assertEquals(0, cache2.getNumberOfLocksHeld());
      tx.commit();
      assertEquals(0, cache1.getNumberOfLocksHeld());
      assertEquals(0, cache2.getNumberOfLocksHeld());
    }



   public void testSyncRepl() throws Exception {
      Integer age;
      Transaction tx;

      try {
         // cache1.setSyncCommitPhase(true);
         tx=beginTransaction();
         cache1.put("/a/b/c", "age", new Integer(38));
         assertNull("age on cache2 must be null as the TX has not yet been committed", cache2.get("/a/b/c", "age"));
         tx.commit();

         // value on cache2 must be 38
         age=(Integer)cache2.get("/a/b/c", "age");
         assertNotNull("\"age\" obtained from cache2 must be non-null ", age);
         assertTrue("\"age\" must be 38", age.intValue() == 38);
      }
      catch(Exception e) {
         fail(e.toString());
      }
   }



   public void testASyncRepl() throws Exception {
      Integer age;
      Transaction tx;

      cache1.setCacheMode(TreeCache.REPL_ASYNC);
      cache2.setCacheMode(TreeCache.REPL_ASYNC);

      try {
         tx=beginTransaction();
         cache1.put("/a/b/c", "age", new Integer(38));
         Thread.sleep(100);
         assertNull("age on cache2 must be null as the TX has not yet been committed", cache2.get("/a/b/c", "age"));
         tx.commit();
         Thread.sleep(100);

         // value on cache2 must be 38
         age=(Integer)cache2.get("/a/b/c", "age");
         assertNotNull("\"age\" obtained from cache2 is null ", age);
         assertTrue("\"age\" must be 38", age.intValue() == 38);
      }
      catch(Exception e) {
         fail(e.toString());
      }
   }

   /**
    * Tests concurrent modifications: thread1 succeeds and thread2 is blocked until thread1 is done, and then succeeds
    * too. However, this is flawed with the introduction of interceptors, here's why.<br/>
    * <ul>
    * <li>Thread1 acquires the lock for /bela/ban on cache1
    * <li>Thread2 blocks on Thread1 to release the lock
    * <li>Thread1 commits: this means the TransactionInterceptor and the ReplicationInterceptor are called in
    * the sequence in which they registered. Unfortunately, the TransactionInterceptor registered first. In the
    * PREPARE phase, the ReplicationInterceptor calls prepare() in cache2 synchronously. The TxInterceptor
    * does nothing. The the COMMIT phase, the TxInterceptor commits the data by releasing the locks locally and
    * then the ReplicationInterceptor sends an asynchronous COMMIT to cache2.
    * <li>Because the TxInterceptor for Thread1 releases the locks locally <em>before</em> sending the async COMMIT,
    * Thread2 is able to acquire the lock for /bela/ban in cache1 and then starts the PREPARE phase by sending a
    * synchronous PREPARE to cache2. If this PREPARE arrives at cache2 <em>before</em> the COMMIT from Thread1,
    * the PREPARE will block because it attempts to acquire a lock on /bela/ban on cache2 still held by Thread1
    * (which would be released by Thread1's COMMIT). This results in deadlock, which is resolved by Thread2 running
    * into a timeout with subsequent rollback and Thread1 succeeding.<br/>
    * </ul>
    * There are 3 solutions to this:
    * <ol>
    * <li>Do nothing. This is standard behavior for concurrent access to the same data. Same thing if the 2 threads
    * operated on the same data in <em>separate</em> caches, e.g. Thread1 on /bela/ban in cache1 and Thread2 on
    * /bela/ban in cache2. The semantics of Tx commit as handled by the interceptors is: after tx1.commit() returns
    * the locks held by tx1 are release and a COMMIT message is on the way (if sent asynchronously).
    * <li>Force an order over TxInterceptor and ReplicationInterceptor. This would require ReplicationInterceptor
    * to always be fired first on TX commit. Downside: the interceptors have an implicit dependency, which is not
    * nice.
    * <li>Priority-order requests at the receiver; e.g. a COMMIT could release a blocked PREPARE. This is bad because
    * it violates JGroups' FIFO ordering guarantees.
    * </ol>
    * I'm currently investigating solution #2, ie. creating an OrderedSynchronizationHandler, which allows other
    * SynchronizationHandlers to register (atHead, atTail), and the OrderedSynchronizationHandler would call the
    * SynchronizationHandler in the order in which they are defined.
    * @throws Exception
    */
   public void testConcurrentPuts() throws Exception {
      cache1.setSyncCommitPhase(true);

      Thread t1=new Thread("Thread1") {
         Transaction tx;

         public void run() {
            try {
               tx=beginTransaction();
               cache1.put("/bela/ban", "name", "Bela Ban");
               _pause(2000); // Thread2 will be blocked until we commit
               tx.commit();
               System.out.println("[Thread1] ** LOCK INFO cache1: " + cache1.printLockInfo());
               System.out.println("[Thread1] ** LOCK INFO cache2: " + cache2.printLockInfo());
            }
            catch(Throwable ex) {
               ex.printStackTrace();
               t1_ex=ex;
            }
         }
      };

      Thread t2=new Thread("Thread2") {
         Transaction tx;

         public void run() {
            try {
               _pause(1000); // give Thread1 time to acquire the lock
               tx=beginTransaction();
               System.out.println("[Thread2] ** LOCK INFO cache1: " + cache1.printLockInfo());
               System.out.println("[Thread2] ** LOCK INFO cache2: " + cache2.printLockInfo());
               cache1.put("/bela/ban", "name", "Michelle Ban");
               System.out.println("[Thread2] ** LOCK INFO cache1: " + cache1.printLockInfo());
               System.out.println("[Thread2] ** LOCK INFO cache2: " + cache2.printLockInfo());
               tx.commit();
               System.out.println("[Thread2] ** LOCK INFO cache1: " + cache1.printLockInfo());
               System.out.println("[Thread2] ** LOCK INFO cache2: " + cache2.printLockInfo());
            }
            catch(Throwable ex) {
               ex.printStackTrace();
               t2_ex=ex;
            }
         }
      };

      // Let the game start
      t1.start();
      t2.start();

      // Wait for threads to die
      t1.join();
      t2.join();

      if(t1_ex != null)
         fail("Thread1 failed: " + t1_ex);
      if(t2_ex != null)
         fail("Thread2 failed: " + t2_ex);

      assertEquals("Michelle Ban", cache1.get("/bela/ban", "name"));
   }


   /**
    * Conncurrent put on 2 different instances.
    */
   public void testConcurrentPutsOnTwoInstances() throws Exception {
      final TreeCache cache1=this.cache1;
      final TreeCache cache2=this.cache2;

      Thread t1=new Thread() {
         Transaction tx;

         public void run() {
            try {
               tx=beginTransaction();
               cache1.put("/ben/wang", "name", "Ben Wang");
               _pause(8000);
               tx.commit(); // This should go thru
            }
            catch(Throwable ex) {
               ex.printStackTrace();
               t1_ex=ex;
            }
         }
      };

      Thread t2=new Thread() {
         Transaction tx;

         public void run() {
            try {
               _pause(1000); // give Thread1 time to acquire the lock
               tx=beginTransaction();
               cache2.put("/ben/wang", "name", "Ben Jr.");
               tx.commit(); // This will time out and rollback first because Thread1 has a tx going as well.
            }
            catch(RollbackException rollback_ex) {
               System.out.println("received rollback exception as expected");
            }
            catch(Throwable ex) {
               ex.printStackTrace();
               t2_ex=ex;
            }
         }
      };

      // Let the game start
      t1.start();
      t2.start();

      // Wait for thread to die but put an insurance of 5 seconds on it.
      t1.join();
      t2.join();

      if(t1_ex != null)
         fail("Thread1 failed: " + t1_ex);
      if(t2_ex != null)
         fail("Thread2 failed: " + t2_ex);
      assertEquals("Ben Wang", cache1.get("/ben/wang", "name"));
   }


   public void testPut() throws Exception {
      final TreeCache cache1=this.cache1;
      final TreeCache cache2=this.cache2;


      Thread t1=new Thread() {
         public void run() {
            try {
               lock.acquire();
               System.out.println("-- t1 has lock");
               cache1.put("/a/b/c", "age", new Integer(38));
               System.out.println("[Thread1] set value to 38");

               System.out.println("-- t1 releases lock");
               lock.release();
               _pause(300);
               Thread.yield();

               lock.acquire();
               System.out.println("-- t1 has lock");
               cache1.put("/a/b/c", "age", new Integer(39));
               System.out.println("[Thread1] set value to 39");

               System.out.println("-- t1 releases lock");
               lock.release();
               assertEquals(new Integer(39), cache1.get("/a/b/c", "age"));
            }
            catch(Throwable ex) {
               ex.printStackTrace();
               t1_ex=ex;
            }
            finally {
               lock.release();
            }
         }
      };

      Thread t2=new Thread() {
         public void run() {
            try {
               _pause(100);
               Thread.yield();
               lock.acquire();
               System.out.println("-- t2 has lock");
               // Should replicate the value right away.
               Integer val=(Integer)cache2.get("/a/b/c", "age");
               System.out.println("[Thread2] value is " + val);
               assertEquals(new Integer(38), val);
               System.out.println("-- t2 releases lock");
               lock.release();
               _pause(300);
               Thread.yield();

               lock.acquire();
               System.out.println("-- t2 has lock");
               val=(Integer)cache2.get("/a/b/c", "age");
               System.out.println("-- t2 releases lock");
               lock.release();
               assertEquals(new Integer(39), val);
            }
            catch(Throwable ex) {
               ex.printStackTrace();
               t2_ex=ex;
            }
            finally {
               lock.release();
            }
         }
      };

      // Let the game start
      t1.start();
      t2.start();

      // Wait for thread to die but put an insurance of 5 seconds on it.
      t1.join();
      t2.join();
      if(t1_ex != null)
         fail("Thread1 failed: " + t1_ex);
      if(t2_ex != null)
         fail("Thread2 failed: " + t2_ex);
   }

   /**
    * Test replicated cache with transaction. Idea is to have two threads running
    * a local cache each that is replicating. Depending on whether cache1 commit/rollback or not,
    * the cache2.get will get different values.
    * Note that we have used sleep to interpose thread execution sequence.
    * Although it's not fool proof, it is rather simple and intuitive.
    *
    * @throws Exception
    */
   public void testPutTx() throws Exception {
      Transaction tx=null;

      try {
         tx=beginTransaction();
         cache1.put("/a/b/c", "age", new Integer(38));
         cache1.put("/a/b/c", "age", new Integer(39));
         Object val=cache2.get("/a/b/c", "age"); // must be null as not yet committed
         assertNull(val);
         tx.commit();

         tx=beginTransaction();
         assertEquals(new Integer(39), cache2.get("/a/b/c", "age")); // must not be null
         tx.commit();
      }
      catch(Throwable ex) {
         ex.printStackTrace();
         t1_ex=ex;
      }
      finally {
         lock.release();
      }
   }


   /**
    * Have both cache1 and cache2 do add and commit. cache1 commit should time out
    * since it can't obtain the lock when trying to replicate cache2. On the other hand,
    * cache2 commit will succeed since now that cache1 is rollbacked and lock is
    * released.
    */
   public void testPutTx1() throws Exception {
      final TreeCache cache1=this.cache1;
      final TreeCache cache2=this.cache2;
      Thread t1=new Thread() {
         public void run() {
            Transaction tx=null;

            try {
               lock.acquire();
               tx=beginTransaction();
               cache1.put("/a/b/c", "age", new Integer(38));
               cache1.put("/a/b/c", "age", new Integer(39));
               lock.release();

               _pause(300);
               lock.acquire();
               try {
                  tx.commit();
               }
               catch(RollbackException ex) {
                  System.out.println("[Thread1] received RollbackException, as expected. Rolling back changes");
                  return;
               }
               finally {
                  lock.release();
               }
            }
            catch(Throwable ex) {
               ex.printStackTrace();
               t1_ex=ex;
            }
            finally {
               lock.release();
            }
         }
      };

      Thread t2=new Thread() {
         public void run() {
            Transaction tx=null;

            try {
               sleep(200);
               Thread.yield();
               lock.acquire();
               tx=beginTransaction();
               assertNull(cache2.get("/a/b/c", "age")); // must be null as not yet committed
               cache2.put("/a/b/c", "age", new Integer(40));
               lock.release();

               _pause(300);
               lock.acquire();
               assertEquals(new Integer(40), cache2.get("/a/b/c", "age")); // must not be null
               tx.commit();
               lock.release();

               _pause(1000);
               tx=beginTransaction();
               assertEquals("After cache2 commit", new Integer(40), cache2.get("/a/b/c", "age"));
               tx.commit();
            }
            catch(Throwable ex) {
               ex.printStackTrace();
               t2_ex=ex;
            }
            finally {
               lock.release();
            }
         }
      };

      // Let the game start
      t1.start();
      t2.start();

      t1.join();
      t2.join();

      if(t1_ex != null)
         fail("Thread1 failed: " + t1_ex);
      if(t2_ex != null)
         fail("Thread2 failed: " + t2_ex);
   }



   public void testPutTxWithRollback() throws Exception {
      final TreeCache cache1=this.cache1;
      final TreeCache cache2=this.cache2;
      Thread t1=new Thread() {
         public void run() {
            Transaction tx=null;

            try {
               lock.acquire();
               tx=beginTransaction();
               cache1.put("/a/b/c", "age", new Integer(38));
               cache1.put("/a/b/c", "age", new Integer(39));
               lock.release();

               _pause(100);
               lock.acquire();
               tx.rollback();
               lock.release();
            }
            catch(Throwable ex) {
               ex.printStackTrace();
               t1_ex=ex;
            }
            finally {
               lock.release();
            }
         }
      };

      Thread t2=new Thread() {
         public void run() {
            Transaction tx=null;

            try {
               sleep(200);
               Thread.yield();
               lock.acquire();
               tx=beginTransaction();
               assertNull(cache2.get("/a/b/c", "age")); // must be null as not yet committed
               lock.release();

               _pause(100);
               lock.acquire();
               assertNull(cache2.get("/a/b/c", "age")); // must be null as rolledback
               tx.commit();
               lock.release();
            }
            catch(Throwable ex) {
               ex.printStackTrace();
               t2_ex=ex;
            }
            finally {
               lock.release();
            }
         }
      };

      // Let the game start
      t1.start();
      t2.start();

      // Wait for thread to die but put an insurance of 5 seconds on it.
      t1.join();
      t2.join();
      if(t1_ex != null)
         fail("Thread1 failed: " + t1_ex);
      if(t2_ex != null)
         fail("Thread2 failed: " + t2_ex);
   }

   static void _pause(long millis) {
      try {
         Thread.sleep(millis);
      }
      catch(Exception ex) {
      }
   }

   public static Test suite() throws Exception {
//        return getDeploySetup(SyncTxUnitTestCase.class, "cachetest.jar");
      return new TestSuite(SyncTxUnitTestCase.class);
   }


}
