package org.jboss.cache.lock;

import org.jboss.cache.util.TestingUtil;
import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;

import java.util.Vector;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.Lock;

/**
 * NonBlockingWriterLock is a read/write lock (with upgrade) that has
 * non-blocking write lock acquisition on existing read lock(s).
 * <p>Note that the write lock is exclusive among write locks, e.g.,
 * only one write lock can be granted at one time, but the write lock
 * is independent of the read locks. For example,
 * a read lock to be acquired will be blocked if there is existing write lock, but
 * will not be blocked if there are mutiple read locks already granted to other
 * owners. On the other hand, a write lock can be acquired as long as there
 * is no existing write lock, regardless how many read locks have been
 * granted.
 *
 * @author <a href="mailto:cavin_song@yahoo.com">Cavin Song</a> April 22, 2004
 * @version 1.0
 */
@Test(groups = { "functional" }, testName = "lock.ReadWriteLockWithUpgradeTest")
public class ReadWriteLockWithUpgradeTest
{
   static final ReadWriteLockWithUpgrade lock_ = new ReadWriteLockWithUpgrade();
   static long SLEEP_MSECS = 500;
   Vector<Object> lockResult = new Vector<Object>();
   int NO_MORE_OP = 0;
   int INVOKE_READ = 1;
   int INVOKE_WRITE = 2;
   int INVOKE_UPGRADE = 3;

   @AfterMethod
   public void tearDown()
   {
      cleanLockingResult();
   }



   /***************************************************************/
   /*     Utility functions to creat threads for RL, WL and UL    */
   /***************************************************************/
   /**
    * Creates a new thread and acquires a read lock with a timeout
    * value specified by the caller. Optionally, the caller can request
    * a second read or write lock after the first read lock request.
    * The locking result is stored in a vector with the following
    * format:
    * <p><DL>
    * <DD>'case number'-'thread name'-[RL|WL|UL]-[0|1]
    * </DL>
    * <p>where:
    * <DL>
    * <DD> 'case number' is the passed in test case # by the caller.
    * <DD> 'thread name' is the passed in thread name by the caller.
    * <DD> RL - indicating was doing read lock request.
    * <DD> WL - indicating was doing write lock request.
    * <DD> UL - indicating was doing upgrade lock request.
    * <DD> 0  - indicating the locking request failed.
    * <DD> 1  - indicating the locking request succeeded.
    * </DL>
    * <p/>
    * After all threads in each test case terminate, the test case
    * should make the following call to verify the test result:
    * <DL>
    * <DD> asssertTrue(checkLockingResult(expected-result-string);
    * </DL>
    * <p/>
    * 'expected-result-string' is the locking result string
    * described above. For example, "8-t1-RL-0" means that thread
    * t1 in test case #8 doing a Read Lock request expects the
    * operation to fail. If the expected result string can't be
    * found then the test case is considered FAILED (ie, either
    * the read lock request was successful or did not complete).
    * <p/>
    * Each test case should also call cleanLockingResult() to reset
    * result vector for the next test cases.
    */

   class ReadThread extends Thread
   {

     private String caseNum;
     private String name;
     volatile CountDownLatch notifyBeforeSecondOp = new CountDownLatch(1);
     volatile CountDownLatch notifyBeforeFinish = new CountDownLatch(1);
     volatile CountDownLatch waitLatch;
     private String errMsg;
     private int secondOP;

     ReadThread(String caseNum, String name, String errMsg, int secondOP, CountDownLatch waitLatch) {
       this.caseNum = caseNum;
       this.name = name;
       this.errMsg = errMsg;
       this.secondOP = secondOP;
       this.waitLatch = waitLatch;
     }

     public void run()
       {
          Lock rlock = lock_.readLock();
          try
          {
             if (!rlock.tryLock(0, TimeUnit.MILLISECONDS))
             {
                String str = caseNum + "-" + name + "-RL-0";
                postLockingResult(str);
                processNotifications();
                return;
             }
             // OK, read lock obtained, sleep and release it.
             String str = caseNum + "-" + name + "-RL-1";
             postLockingResult(str);
             processNotifications();

             if (secondOP == INVOKE_READ)
             {
                acquireReadLock(caseNum, name, errMsg);
             }
             else if (secondOP == INVOKE_WRITE)
             {
                acquireWriteLock(caseNum, name, errMsg);
             }
             else if (secondOP == INVOKE_UPGRADE)
             {
                acquireUpgradeLock(caseNum, name, errMsg);
             }
             rlock.unlock();
             notifyBeforeFinish.countDown();
          }
          catch (Exception ex)
          {
          }
       }

     private void processNotifications() throws InterruptedException {
       notifyBeforeSecondOp.countDown();
       if (waitLatch != null) waitLatch.await(10, TimeUnit.SECONDS);
     }
   }

  private class WriteThread extends Thread
  {
    String caseNum;
    String name;
    volatile CountDownLatch notifyLatch = new CountDownLatch(1);
    volatile CountDownLatch waitLatch;
    String errMsg;
    int secondOP;

    WriteThread(String caseNum, String name, String errMsg, int secondOP, CountDownLatch waitLatch) {
      this.caseNum = caseNum;
      this.name = name;
      this.errMsg = errMsg;
      this.secondOP = secondOP;
      this.waitLatch = waitLatch;
    }


    public void run()
    {
       try
       {
          Lock wlock = lock_.writeLock();
          if (!wlock.tryLock(0, TimeUnit.MILLISECONDS))
          {
             String str = caseNum + "-" + name + "-WL-0";
             postLockingResult(str);
             processNotifications();
             return;
          }
          // OK, write lock obtained, sleep and release it.
          String str = caseNum + "-" + name + "-WL-1";
          postLockingResult(str);

          processNotifications();

          if (secondOP == INVOKE_READ)
          {
             acquireReadLock(caseNum, name, errMsg);
          }
          else if (secondOP == INVOKE_WRITE)
          {
             acquireWriteLock(caseNum, name, errMsg);
          }
          else if (secondOP == INVOKE_UPGRADE)
          {
             acquireUpgradeLock(caseNum, name, errMsg);
          }

          wlock.unlock();
       }
       catch (Exception ex)
       {
          ex.printStackTrace();
       }
    }

    private void processNotifications() throws InterruptedException {
      notifyLatch.countDown();
      if (waitLatch != null)
        waitLatch.await(10, TimeUnit.SECONDS);
    }
  }

  class UpgradeThread extends Thread
  {
    String caseNum;
    String name;
    private CountDownLatch notifyBeforeFinish = new CountDownLatch(1);
    private CountDownLatch notifyBeforeSecondOp = new CountDownLatch(1);
    private CountDownLatch beforeUpgrateWait;
    private CountDownLatch beforeFinishWait;
    String errMsg;
    int secondOP;

    UpgradeThread(String caseNum, String name, String errMsg, int secondOP, CountDownLatch beforeUpgrateWait) {
      this.caseNum = caseNum;
      this.name = name;
      this.errMsg = errMsg;
      this.secondOP = secondOP;
      this.beforeUpgrateWait = beforeUpgrateWait;
    }

    public void run()
    {
       try
       {
          Lock rlock = lock_.readLock();
          Lock wlock;
          if (!rlock.tryLock(0, TimeUnit.MILLISECONDS))
          {
             String str = caseNum + "-" + name + "-RL-0";
             postLockingResult(str);
             notifyBeforeFinish.countDown();
            if (beforeUpgrateWait != null) beforeUpgrateWait.await(10, TimeUnit.SECONDS);
            return;
          }
          // OK, read lock obtained, sleep and upgrade it later.
          notifyBeforeSecondOp.countDown();
          if (beforeUpgrateWait != null) beforeUpgrateWait.await(10, TimeUnit.SECONDS);
          String str = caseNum + "-" + name + "-UL-";
          if ((wlock = lock_.upgradeLockAttempt(0)) == null)
          {
             str += "0";
          }
          else
          {
             str += "1";
          }
          postLockingResult(str);
          // Sleep again and then release the lock.
          TestingUtil.sleepThread(SLEEP_MSECS);
          if (wlock != null)
          {
             wlock.unlock();
          }
          rlock.unlock();
          notifyBeforeFinish.countDown();
          if (beforeFinishWait != null) beforeFinishWait.await(10, TimeUnit.SECONDS);
       }
       catch (Exception ex)
       {
       }
    }

  }


   /***************************************************************/
   /*     Utility functions to acquire RL and WL (no thread)      */
   /***************************************************************/
   /**
    * This routine tries to acquire a read lock with a timeout value
    * passed in by the caller. It then stores the locking result in the result vector depending
    * on the outcome of the request.
    */

   protected void acquireReadLock(final String caseNum, final String name,
                                  final String errMsg)
   {
      try
      {
         Lock rlock = lock_.readLock();
         if (!rlock.tryLock(0, TimeUnit.MILLISECONDS))
         {
            String str = caseNum + "-" + name + "-RL-0";
            postLockingResult(str);
            return;
         }
         // OK, read lock obtained, sleep and release it.
         String str = caseNum + "-" + name + "-RL-1";
         postLockingResult(str);
         TestingUtil.sleepThread(SLEEP_MSECS);
         rlock.unlock();
      }
      catch (Exception ex)
      {
      }
   }

   /**
    * Same as {@link #acquireReadLock acquireReadLock()} except
    * it's for write lock request.
    */
   protected void acquireWriteLock(final String caseNum, final String name, final String errMsg)
   {
      try
      {
         Lock wlock = lock_.writeLock();
         if (!wlock.tryLock(0, TimeUnit.MILLISECONDS))
         {
            String str = caseNum + "-" + name + "-WL-0";
            postLockingResult(str);
            return;
         }
         // OK, write lock obtained, sleep and release it.
         String str = caseNum + "-" + name + "-WL-1";
         postLockingResult(str);
         TestingUtil.sleepThread(SLEEP_MSECS);
         wlock.unlock();
      }
      catch (Exception ex)
      {
      }
   }

   /**
    * Same as {@link #acquireReadLock acquireReadLock()} except
    * it's for upgrade lock request.
    */
   protected void acquireUpgradeLock(final String caseNum, final String name,
                                     final String errMsg)
   {
      try
      {
         Lock ulock = null;
         if ((ulock = lock_.upgradeLockAttempt(0)) == null)
         {
            String str = caseNum + "-" + name + "-UL-0";
            postLockingResult(str);
            return;
         }
         // OK, write lock obtained, sleep and release it.
         String str = caseNum + "-" + name + "-UL-1";
         postLockingResult(str);
         TestingUtil.sleepThread(SLEEP_MSECS);
         ulock.unlock();
      }
      catch (Exception ex)
      {
      }
   }

   /***************************************************************/
   /*     Synchronized methods handling locking result vector     */
   /***************************************************************/
   /**
    * Clean/remove all locking results in the vector.
    */
   protected synchronized void cleanLockingResult()
   {
      lockResult.removeAllElements();
   }

   /**
    * Post a locking result to the vector for later verification.
    */
   protected synchronized void postLockingResult(Object obj)
   {
      // Make sure we only have one in the vector
      //if (!checkLockingResult((String)obj))
      lockResult.addElement(obj);
   }

   /**
    * Check if a given expected locking result is in the vector.
    */
   protected synchronized boolean checkLockingResult(String expected)
   {
      boolean rc = false;
      for (int i = 0; i < lockResult.size(); i++)
      {
         Object ele = lockResult.elementAt(i);
         String str = (String) ele;
         if (expected.equals(str))
         {
            rc = true;
            break;
         }
      }
      return rc;
   }

   /***************************************************************/
   /*                   T e s t  C a s e s                        */
   /***************************************************************/
   /**
    * Case #10 - T1 acquires RL, T2 acquires RL followed by WL.
    */
   public void testWriteWithMultipleReaders() throws Exception
   {
      String caseNum = "10";
      ReadThread t1 = new ReadThread(caseNum, "t1", "1st read lock attempt failed", NO_MORE_OP, null);
      ReadThread t2 = new ReadThread(caseNum, "t2", "2nd read lock attempt failed", INVOKE_WRITE, t1.notifyBeforeSecondOp);

      t1.start();
      t2.start();
      t1.join(3000);
      t2.join(3000);
      assertTrue(checkLockingResult(caseNum + "-t1-RL-1") &&
              checkLockingResult(caseNum + "-t2-RL-1") &&
              checkLockingResult(caseNum + "-t2-WL-0"));
      cleanLockingResult();
      // possilbe deadlock check
      if (t1.isAlive() || t2.isAlive())
      {
         fail("Possible deadlock resulted in testRead.");
      }
   }

   /**
    * Case #11 - T1 acquires RL followed by WL, T2 acquires RL.
    */
   public void testUpgradeWithMultipleReadersOn1() throws Exception
   {
      String caseNum = "11";
      ReadThread t1 = new ReadThread(caseNum, "t1", "1st read lock attempt failed", INVOKE_WRITE, null);
      ReadThread t2 = new ReadThread(caseNum, "t2", "2nd read lock attempt failed", NO_MORE_OP, t1.notifyBeforeSecondOp);

      t1.start();
      t2.start();
      t1.join(3000);
      t2.join(3000);
      assertTrue(checkLockingResult(caseNum + "-t1-RL-1") &&
              checkLockingResult(caseNum + "-t2-RL-1") &&
              checkLockingResult(caseNum + "-t1-WL-0"));
      cleanLockingResult();
      // possilbe deadlock check
      if (t1.isAlive() || t2.isAlive())
      {
         fail("Possible deadlock resulted in testRead.");
      }
   }

   /**
    * Case #2 - T1 acquires RL followed by UL.
    */
   public void testUpgradeReadLock() throws Exception
   {
      String caseNum = "2";
      Thread t1 = new ReadThread(caseNum, "t1", "1st read lock attempt failed", INVOKE_UPGRADE, null);

      t1.start();
      t1.join(3000);
      assertTrue(checkLockingResult(caseNum + "-t1-RL-1") &&
              checkLockingResult(caseNum + "-t1-UL-1"));
      cleanLockingResult();
   }

   /**
    * Case #3 - T1 acquires RL followed by WL.
    */
   public void testReadThenWrite() throws Exception
   {
      String caseNum = "3";
      acquireReadLock(caseNum, "t1", "1st read lock attempt failed");
      acquireWriteLock(caseNum, "t1.1", "2nd write lock attempt failed");
      assertTrue(checkLockingResult(caseNum + "-t1-RL-1") &&
              checkLockingResult(caseNum + "-t1.1-WL-1"));
      cleanLockingResult();
   }


   /**
    * Case #5 - T1 acquires WL followed by RL.
    */
   public void testWriteThenRead() throws Exception
   {
      String caseNum = "5";
      acquireWriteLock(caseNum, "t1", "1st write lock attempt failed");
      acquireReadLock(caseNum, "t1.1", "2nd read lock attempt failed");
      assertTrue(checkLockingResult(caseNum + "-t1-WL-1") &&
              checkLockingResult(caseNum + "-t1.1-RL-1"));
      cleanLockingResult();
   }

   /**
    * Case #6 - T1 acquires RL, T2 acquires RL.
    */
   public void testMultipleReadlock() throws Exception
   {
      String caseNum = "6";
      Thread t1 = new ReadThread(caseNum, "t1", "1st read lock attempt failed", NO_MORE_OP, null);
      Thread t2 = new ReadThread(caseNum, "t2", "2nd read lock attempt failed", NO_MORE_OP, null);

      t1.start();
      t2.start();
      t1.join(3000);
      t2.join(3000);
      assertTrue(checkLockingResult(caseNum + "-t1-RL-1") &&
              checkLockingResult(caseNum + "-t2-RL-1"));
      cleanLockingResult();
      // possilbe deadlock check
      if (t1.isAlive() || t2.isAlive())
      {
         fail("Possible deadlock resulted in testRead.");
      }
   }

   /**
    * Case #8 - T1 acquires WL, T2 acquires RL.
    */
   public void testWriteWithExistingReader() throws Exception
   {
      String caseNum = "8";
      CountDownLatch waitFor = new CountDownLatch(1);
      ReadThread t1 = new ReadThread(caseNum, "t1",   "1st write lock attempt failed", NO_MORE_OP, waitFor);
      WriteThread t2 = new WriteThread(caseNum, "t2", "2nd read lock attempt failed", NO_MORE_OP, waitFor);

      t1.start();
      t2.start();

      t1.notifyBeforeSecondOp.await();
      t2.notifyLatch.await();
      waitFor.countDown();

      waitFor.countDown();
      t1.join(3000);
      t2.join(3000);


      // there is NO guarantee as to which thread will get the lock!!
      boolean t0GetsLock = checkLockingResult(caseNum + "-t1-RL-1") && checkLockingResult(caseNum + "-t2-WL-0");
      boolean t1GetsLock = checkLockingResult(caseNum + "-t1-RL-0") && checkLockingResult(caseNum + "-t2-WL-1");

      assert !(t0GetsLock && t1GetsLock); // both can't be true
      assert t0GetsLock || t1GetsLock; // one must be true

      cleanLockingResult();
      // possilbe deadlock check
      if (t1.isAlive() || t2.isAlive())
      {
         fail("Possible deadlock resulted in testRead.");
      }
   }

   /**
    * Case #13 - T1 acquires RL, T2 acquires WL.
    */
   public void testReadWithExistingWriter() throws Exception
   {
      String caseNum = "13";
      CountDownLatch waitFor = new CountDownLatch(1);
      WriteThread t1 = new WriteThread(caseNum, "t1", "1st write lock attempt failed", NO_MORE_OP, waitFor);
      ReadThread t2 = new ReadThread(caseNum, "t2", "2nd read lock attempt failed", NO_MORE_OP, waitFor);

      t1.start();
      t2.start();

      t1.notifyLatch.await();
      t2.notifyBeforeSecondOp.await();
      waitFor.countDown();

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

      // there is NO guarantee as to which thread will get the lock!!
      boolean t0GetsLock = checkLockingResult(caseNum + "-t1-WL-1") && checkLockingResult(caseNum + "-t2-RL-0");
      boolean t1GetsLock = checkLockingResult(caseNum + "-t1-WL-0") && checkLockingResult(caseNum + "-t2-RL-1");

      assert !(t0GetsLock && t1GetsLock); // both can't be true
      assert t0GetsLock || t1GetsLock; // one must be true

      cleanLockingResult();
      // possilbe deadlock check
      if (t1.isAlive() || t2.isAlive())
      {
         fail("Possible deadlock resulted in testRead.");
      }
   }

   /**
    * Case #14 - T1 acquires WL, T2 acquires WL.
    */
   public void testMultipleWritelocks() throws Exception
   {
      String caseNum = "14";
      CountDownLatch waitFor = new CountDownLatch(1);
      WriteThread t1 = new WriteThread(caseNum, "t1", "1st write lock attempt failed", NO_MORE_OP, waitFor);
      WriteThread t2 = new WriteThread(caseNum, "t2", "2nd write lock attempt failed", NO_MORE_OP, waitFor);

      t1.start();
      t2.start();
      t1.notifyLatch.await();
      t2.notifyLatch.await();
      waitFor.countDown();

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

      // there is NO guarantee as to which thread will get the lock!!
      boolean t0GetsLock = checkLockingResult(caseNum + "-t1-WL-1") && checkLockingResult(caseNum + "-t2-WL-0");
      boolean t1GetsLock = checkLockingResult(caseNum + "-t1-WL-0") && checkLockingResult(caseNum + "-t2-WL-1");

      assert !(t0GetsLock && t1GetsLock); // both can't be true
      assert t0GetsLock || t1GetsLock; // one must be true
      
      cleanLockingResult();
      // possilbe deadlock check
      if (t1.isAlive() || t2.isAlive())
      {
         fail("Possible deadlock resulted in testRead.");
      }
   }

   /**
    * Case #7 - T1 acquires RL, T2 acquires UL.
    */
   public void testUpgradeWithExistingReader() throws Exception
   {
      String caseNum = "7";
      CountDownLatch waitFor = new CountDownLatch(1);
      ReadThread t1 = new ReadThread(caseNum, "t1", "1st read lock attempt failed", NO_MORE_OP, waitFor);
      UpgradeThread t2 = new UpgradeThread(caseNum, "t2", "2nd upgrade lock attempt failed", NO_MORE_OP, t1.notifyBeforeSecondOp);
      t2.beforeFinishWait = waitFor;

      t1.start();
      t2.start();
      t2.notifyBeforeFinish.await(10, TimeUnit.SECONDS);
      waitFor.countDown();

      t1.join(3000);
      t2.join(3000);
      assertTrue(checkLockingResult(caseNum + "-t1-RL-1"));
      assertTrue(checkLockingResult(caseNum + "-t2-UL-0"));
      cleanLockingResult();
      // possilbe deadlock check
      if (t1.isAlive() || t2.isAlive())
      {
         fail("Possible deadlock resulted in testRead.");
      }
   }

   /**
    * Case #9 - T1 acquires RL, T2 acquires RL followed by UL.
    */
   public void testUpgradeWithMultipleReaders() throws Exception
   {
      String caseNum = "9";
      CountDownLatch waitFor = new CountDownLatch(1);
      ReadThread t1 = new ReadThread(caseNum, "t1", "1st read lock attempt failed", NO_MORE_OP, waitFor);
      ReadThread t2 = new ReadThread(caseNum, "t2", "2nd read lock attempt failed", INVOKE_UPGRADE, t1.notifyBeforeSecondOp);

      t1.start();
      t2.start();

      t1.notifyBeforeSecondOp.await(10, TimeUnit.SECONDS);
      t2.notifyBeforeFinish.await(10, TimeUnit.SECONDS);
      waitFor.countDown();

      t1.join(3000);
      t2.join(3000);
      assertTrue(checkLockingResult(caseNum + "-t1-RL-1") &&
              checkLockingResult(caseNum + "-t2-RL-1") &&
              checkLockingResult(caseNum + "-t2-UL-0"));
      cleanLockingResult();
      // possilbe deadlock check
      if (t1.isAlive() || t2.isAlive())
      {
         fail("Possible deadlock resulted in testRead.");
      }
   }
}

