/* $Id: LockGate.java 1865954 2019-08-26 20:27:25Z schuch $ */

/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.manifoldcf.core.lockmanager;

import java.util.*;
import java.io.*;
import org.apache.manifoldcf.core.interfaces.*;

/** This class creates a first-come, first-serve local queue for locks.
* The usage model is as follows:
* (1) There is one lock gate per key for all threads.
* (2) The LockGate replaces LockObjects in the pool, but each LockGate refers to
*   a LockObject.  They are separate objects though because they require separate locking.
* (3) When a lock is desired, the appropriate LockGate method is called to obtain
*   the lock.  This method places the thread ID onto the queue, and waits until
*   the thread ID reaches the head of the queue before executing the lock request.
* (4) Only when the lock request is successful, or is aborted, is the thread ID
*   removed from the queue.  Write requests therefore serve to block read requests
*   until the write request is serviced.
* Preferred structure:
* <pre>
* &lt;wait_for_permission&gt;
* try {
* ... obtain the lock ...
* } finally {
*   &lt;release_permission&gt;
* }
* </pre>
* Seeing lockups.  These lockups are characterized by a thread waiting on a lock object
* while another thread waits on permission to do something else with the lock object.
* It is by no means clear at this point how this situation causes a hang-up; the 
* lock object is waiting to be awakened, but there is no obvious entity holding the lock elsewhere.
* But one thread (A) seems always to be in a multi-lock situation, waiting to obtain a lock, e.g.:
* <pre>
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:503)
	at org.apache.manifoldcf.core.lockmanager.LockObject.enterWriteLock(LockObject.java:80)
	- locked &lt;0x00000000fe205720&gt; (a org.apache.manifoldcf.core.lockmanager.LockObject)
	at org.apache.manifoldcf.core.lockmanager.LockGate.enterWriteLock(LockGate.java:132)
	at org.apache.manifoldcf.core.lockmanager.BaseLockManager.enter(BaseLockManager.java:1483)
	at org.apache.manifoldcf.core.lockmanager.BaseLockManager.enterCriticalSections(BaseLockManager.java:920)
	at org.apache.manifoldcf.core.lockmanager.LockManager.enterCriticalSections(LockManager.java:455)
	</pre>
* Here's the second thread (B), which is waitingForPermission:
* <pre>
	at java.lang.Object.wait(Native Method)
	- waiting on &lt;0x00000000f8b71c78&gt; (a org.apache.manifoldcf.core.lockmanager.LockGate)
	at java.lang.Object.wait(Object.java:503)
	at org.apache.manifoldcf.core.lockmanager.LockGate.waitForPermission(LockGate.java:91)
	- locked &lt;0x00000000f8b71c78&gt; (a org.apache.manifoldcf.core.lockmanager.LockGate)
	at org.apache.manifoldcf.core.lockmanager.LockGate.enterWriteLock(LockGate.java:129)
	at org.apache.manifoldcf.core.lockmanager.BaseLockManager.enterWrite(BaseLockManager.java:1130)
	at org.apache.manifoldcf.core.lockmanager.BaseLockManager.enterWriteCriticalSection(BaseLockManager.java:896)
	at org.apache.manifoldcf.core.lockmanager.LockManager.enterWriteCriticalSection(LockManager.java:431)
	at org.apache.manifoldcf.core.interfaces.IDFactory.make(IDFactory.java:55)
	</pre>
* The problem is that (A) has already obtained permission, but cannot obtain the lock.  (B) is somehow blocking
* (A) from obtaining the lock even though it has not yet taken its own lock!  Or, maybe it has, and we don't see it in
* the stack trace.
* Another example: (C)
* <pre>
	at java.lang.Object.wait(Native Method)
	- waiting on &lt;0x00000000ffbdc038&gt; (a org.apache.manifoldcf.core.lockmanager.LockGate)
	at java.lang.Object.wait(Object.java:503)
	at org.apache.manifoldcf.core.lockmanager.LockGate.waitForPermission(LockGate.java:91)
	- locked &lt;0x00000000ffbdc038&gt; (a org.apache.manifoldcf.core.lockmanager.LockGate)
	at org.apache.manifoldcf.core.lockmanager.LockGate.enterReadLock(LockGate.java:211)
	at org.apache.manifoldcf.core.lockmanager.BaseLockManager.enter(BaseLockManager.java:1532)
	at org.apache.manifoldcf.core.lockmanager.BaseLockManager.enterLocks(BaseLockManager.java:813)
	at org.apache.manifoldcf.core.lockmanager.LockManager.enterLocks(LockManager.java:355)
	</pre>
* and (D):
* <pre>
	at java.lang.Object.wait(Native Method)
	- waiting on &lt;0x00000000ffbdd2f8&gt; (a org.apache.manifoldcf.core.lockmanager.LockObject)
	at java.lang.Object.wait(Object.java:503)
	at org.apache.manifoldcf.core.lockmanager.LockObject.enterWriteLock(LockObject.java:83)
	- locked &lt;0x00000000ffbdd2f8&gt; (a org.apache.manifoldcf.core.lockmanager.LockObject)
	at org.apache.manifoldcf.core.lockmanager.LockGate.enterWriteLock(LockGate.java:132)
	at org.apache.manifoldcf.core.lockmanager.BaseLockManager.enter(BaseLockManager.java:1483)
	at org.apache.manifoldcf.core.lockmanager.BaseLockManager.enterLocks(BaseLockManager.java:813)
	at org.apache.manifoldcf.core.lockmanager.LockManager.enterLocks(LockManager.java:355)
	</pre>
* Problem here: The LockGate 0x00000000ffbdc038 has no other instance anywhere, which should not be able to happen.
* Debugging must entail dumping ALL outstanding locks periodically -- and who holds each.
*/
public class LockGate
{
  protected final List<Long> threadRequests = new ArrayList<Long>();
  protected final LockObject lockObject;
  protected final Object lockKey;
  protected LockPool lockPool;

  public LockGate(Object lockKey, LockObject lockObject, LockPool lockPool)
  {
    this.lockKey = lockKey;
    this.lockObject = lockObject;
    this.lockPool = lockPool;
  }
  
  public void makeInvalid()
  {
    synchronized (this)
    {
      this.lockPool = null;
      lockObject.makeInvalid();
    }
  }

  protected void waitForPermission(Long threadID)
    throws InterruptedException, ExpiredObjectException
  {
    synchronized (this)
    {
      threadRequests.add(threadID);
    }
    try
    {
      // Now, wait until we are #1
      while (true)
      {
        synchronized (this)
        {
          if (lockPool == null)
            throw new ExpiredObjectException("Invalid");

          if (threadRequests.get(0).equals(threadID))
            return;
          
          wait();
        }
      }
    }
    catch (InterruptedException e)
    {
      freePermission(threadID);
      throw e;
    }
    catch (ExpiredObjectException e)
    {
      freePermission(threadID);
      throw e;
    }
    catch (Error e)
    {
      freePermission(threadID);
      throw e;
    }
    catch (RuntimeException e)
    {
      freePermission(threadID);
      throw e;
    }
  }
  
  protected void freePermission(Long threadID)
  {
    synchronized (this)
    {
      threadRequests.remove(threadID);
      notifyAll();
    }
  }
  
  public void enterWriteLock(Long threadID)
    throws ManifoldCFException, InterruptedException, ExpiredObjectException
  {
    waitForPermission(threadID);
    try
    {
      lockObject.enterWriteLock();
    }
    finally
    {
      freePermission(threadID);
    }
  }
  
  public void enterWriteLockNoWait(Long threadID)
    throws ManifoldCFException, LockException, LocalLockException, InterruptedException, ExpiredObjectException
  {
    waitForPermission(threadID);
    try
    {
      lockObject.enterWriteLockNoWait();
    }
    finally
    {
      freePermission(threadID);
    }
  }
  
  public void leaveWriteLock()
    throws ManifoldCFException, InterruptedException, ExpiredObjectException
  {
    synchronized (this)
    {
      // Leave, and if we succeed, flush from pool.
      if (lockObject.leaveWriteLock())
      {
        if (threadRequests.size() == 0 && lockPool != null)
          lockPool.releaseObject(lockKey, this);
      }
    }
  }
  
  public void enterNonExWriteLock(Long threadID)
    throws ManifoldCFException, InterruptedException, ExpiredObjectException
  {
    waitForPermission(threadID);
    try
    {
      lockObject.enterNonExWriteLock();
    }
    finally
    {
      freePermission(threadID);
    }
  }
  
  public void enterNonExWriteLockNoWait(Long threadID)
    throws ManifoldCFException, LockException, LocalLockException, InterruptedException, ExpiredObjectException
  {
    waitForPermission(threadID);
    try
    {
      lockObject.enterNonExWriteLockNoWait();
    }
    finally
    {
      freePermission(threadID);
    }
  }
  
  public void leaveNonExWriteLock()
    throws ManifoldCFException, InterruptedException, ExpiredObjectException
  {
    synchronized (this)
    {
      // Leave, and if we succeed, flush from pool.
      if (lockObject.leaveNonExWriteLock())
      {
        if (threadRequests.size() == 0 && lockPool != null)
          lockPool.releaseObject(lockKey, this);
      }
    }
  }

  public void enterReadLock(Long threadID)
    throws ManifoldCFException, InterruptedException, ExpiredObjectException
  {
    waitForPermission(threadID);
    try
    {
      lockObject.enterReadLock();
    }
    finally
    {
      freePermission(threadID);
    }
  }
  
  public void enterReadLockNoWait(Long threadID)
    throws ManifoldCFException, LockException, LocalLockException, InterruptedException, ExpiredObjectException
  {
    waitForPermission(threadID);
    try
    {
      lockObject.enterReadLockNoWait();
    }
    finally
    {
      freePermission(threadID);
    }
  }
  
  public void leaveReadLock()
    throws ManifoldCFException, InterruptedException, ExpiredObjectException
  {
    // Leave, and if we succeed (and the thread queue is empty), flush from pool.
    synchronized (this)
    {
      if (lockObject.leaveReadLock())
      {
        if (threadRequests.size() == 0 && lockPool != null)
          lockPool.releaseObject(lockKey, this);
      }
    }
  }

}
