User: mnf999
Date: 01/07/03 16:13:45
Modified: src/main/org/jboss/ejb/plugins
EntityInstanceInterceptor.java
Log:
The biggy, we rewrite the eii.
The driving force was getting rid of the busy wait bug, but it ended up in a full
rewrite of the core interceptors.
The new interceptor works on a 2 level lock for transactional and context contention.
This code has been marked "CRITICAL" further modifications should be approved on
jboss-dev.
Revision Changes Path
1.32 +283 -333
jboss/src/main/org/jboss/ejb/plugins/EntityInstanceInterceptor.java
Index: EntityInstanceInterceptor.java
===================================================================
RCS file:
/cvsroot/jboss/jboss/src/main/org/jboss/ejb/plugins/EntityInstanceInterceptor.java,v
retrieving revision 1.31
retrieving revision 1.32
diff -u -r1.31 -r1.32
--- EntityInstanceInterceptor.java 2001/06/18 20:01:23 1.31
+++ EntityInstanceInterceptor.java 2001/07/03 23:13:45 1.32
@@ -39,327 +39,277 @@
import org.jboss.util.Sync;
/**
-* This container acquires the given instance.
*
+* <p>The instance interceptors role is to acquire a context representing the
target object from the
+* cache.
+*
+* <p>This particular container interceptor implements pessimistic locking on the
transaction that
+* is associated with the retrieved instance. If there is a transaction
associated with the
+* target component and it is different from the transaction associated with the
MethodInvocation
+* coming in then the policy is to wait for transactional commit.
+*
+* <p>We also implement serialization of calls in here (this is a spec
requirement).
+* This is a fine grained notify, notifyAll mechanism. We notify on ctx
serialization locks and
+* notifyAll on global transactional locks
+*
+* <p><b>WARNING: critical code</b>, get approval from senior developers before
changing.
+*
+*
* @see <related>
-* @author <a href="mailto:[EMAIL PROTECTED]">Rickard �berg</a>
-* @author <a href="mailto:[EMAIL PROTECTED]">Marc Fleury</a>
-* @author <a href="mailto:[EMAIL PROTECTED]">Sebastien Alborini</a>
-* @version $Revision: 1.31 $
+* @author <a href="mailto:[EMAIL PROTECTED]">Marc Fleury</a>
+* @version $Revision: 1.32 $
+*
+* <p><b>Revisions:</b><br>
+* <p><b>2001/06/28: marcf</b>
+* <ol>
+* <li>Moved to new synchronization
+* <li>Pools are gone simple design
+* <li>two levels of syncrhonization with Tx and ctx
+* <li>remove busy wait from previous mechanisms
+* </ol>
+*
*/
public class EntityInstanceInterceptor
extends AbstractInterceptor
{
- // Constants -----------------------------------------------------
-
- // Attributes ----------------------------------------------------
- protected EntityContainer container;
-
- // Static --------------------------------------------------------
-
- // Constructors --------------------------------------------------
-
- // Public --------------------------------------------------------
- public void setContainer(Container container)
- {
- this.container = (EntityContainer)container;
- }
-
- public Container getContainer()
- {
- return container;
- }
-
- // Interceptor implementation --------------------------------------
- public Object invokeHome(MethodInvocation mi)
- throws Exception
- {
- // Get context
- EnterpriseContext ctx =
((EntityContainer)getContainer()).getInstancePool().get();
- mi.setEnterpriseContext(ctx);
-
- // It is a new context for sure so we can lock it
- ctx.lock();
-
- try
- {
- // Invoke through interceptors
- return getNext().invokeHome(mi);
- } finally
- {
- // Always unlock, no matter what
- ctx.unlock();
-
- // Still free? Not free if create() was called successfully
- if (ctx.getId() == null)
- {
- container.getInstancePool().free(ctx);
- }
- }
- }
-
- public Object invoke(MethodInvocation mi)
- throws Exception
- {
- // The id store is a CacheKey in the case of Entity
- CacheKey key = (CacheKey)mi.getId();
-
- // Get cache
- AbstractInstanceCache cache =
(AbstractInstanceCache)container.getInstanceCache();
- BeanSemaphore mutex = (BeanSemaphore)cache.getLock(key);
-
- EnterpriseContext ctx = null;
- boolean exceptionThrown = false;
-
- try
- {
- boolean waitingOnTransaction = false; // So we don't output
LOCKING-WAITING all the time
- boolean waitingOnContext = false; // So we don't output LOCKING-WAITING
all the time
- do
- {
- if (mi.getTransaction() != null && mi.getTransaction().getStatus()
== Status.STATUS_MARKED_ROLLBACK)
- throw new RuntimeException("Transaction marked for rollback,
possibly a timeout");
-
+ // Constants -----------------------------------------------------
+
+ // Attributes ----------------------------------------------------
+ protected EntityContainer container;
+
+ // Static --------------------------------------------------------
+
+ // Constructors --------------------------------------------------
+
+ // Public --------------------------------------------------------
+ public void setContainer(Container container)
+ {
+ this.container = (EntityContainer)container;
+ }
+
+ public Container getContainer()
+ {
+ return container;
+ }
+
+ // Interceptor implementation --------------------------------------
+ public Object invokeHome(MethodInvocation mi)
+ throws Exception
+ {
+ // Get context
+ EnterpriseContext ctx =
((EntityContainer)getContainer()).getInstancePool().get();
+
+ // Pass it to the method invocation
+ mi.setEnterpriseContext(ctx);
+
+ // Give it the transaction
+ ctx.setTransaction(mi.getTransaction());
+
+ // This context is brand new. We can lock without more "fuss"
+ // The reason we need to lock it is that it will be put in cache
before the end
+ // of the call. So another thread could access it before we are done.
+
+ ctx.lock();
+
try
{
-
- mutex.acquire();
-
- // This loop guarantees a mutex lock for the specified object id
- // When a cache.remove happens, it disassociates the mutex from
the bean's id,
- // Thus, the mutex in this code could be invalid. We avoid this
problem with the following loop.
- //
- // Also we should have a while loop so we don't mess up the
finally clause that does
- // mutex.release()
- //
- while (!mutex.isValid())
- {
- BeanSemaphore newmutex = (BeanSemaphore)cache.getLock(key);
- mutex.release();
- mutex = newmutex;
-
- // Avoid infinite loop.
- if (mi.getTransaction() != null &&
mi.getTransaction().getStatus() == Status.STATUS_MARKED_ROLLBACK)
- throw new RuntimeException("Transaction marked for
rollback, possibly a timeout");
-
- mutex.acquire();
- }
- // Get context
- ctx = cache.get(key);
-
- // Do we have a running transaction with the context
- Transaction tx = ctx.getTransaction();
- if (tx != null &&
- // And are we trying to enter with another transaction
- !tx.equals(mi.getTransaction()))
- {
- // Let's put the thread to sleep a lock release will wake
the thread
- // Possible deadlock
- if (!waitingOnTransaction)
- {
- Logger.debug("LOCKING-WAITING (TRANSACTION) in Thread "
- + Thread.currentThread().getName()
- + " for id "+ctx.getId()+" ctx.hash
"+ctx.hashCode()
- +" tx:"+((tx == null) ? "null" :
tx.toString()));
- waitingOnTransaction = true;
- }
-
- // Try your luck again
- ctx = null;
- Thread.yield(); // Give the OS some help.
- continue;
- }
- if (waitingOnTransaction)
- {
- Logger.debug("FINISHED-LOCKING-WAITING (TRANSACTION) in Thread
"
- + Thread.currentThread().getName()
- + " for id "+ctx.getId()
- +" ctx.hash "+ctx.hashCode()
- +" tx:"+((tx == null) ? "null" : tx.toString()));
- waitingOnTransaction = false;
- }
- // OK so we now know that for this PrimaryKey, no other
- // thread has a transactional lock on the bean.
- // So, let's setTransaction of the ctx here instead of in later
code.
- // I really don't understand why it wasn't done here anyways
before.
- // Later on, in the finally clause, I'll check to see if the ctx
was
- // invoked upon and if not, dissassociate the transaction with the
ctx.
- // If there is no transactional assocation here, there is a race
condition
- // for re-entrant entity beans, so don't remove the code below.
- //
- // If this "waitingOnTransaction" loop is ever removed in favor of
- // something like using db locks instead, this transactional
assocation
- // must be removed.
- if (mi.getTransaction() != null && tx != null &&
!tx.equals(mi.getTransaction()))
- {
- // Do transactional "lock" on ctx right now!
- ctx.setTransaction(mi.getTransaction());
- }
- // If we get here it's the right tx, or no tx
- if (!ctx.isLocked())
- {
- //take it!
- ctx.lock();
- }
- else
- {
- if (!isCallAllowed(mi))
- {
- // Go to sleep and wait for the lock to be released
- // This is not one of the "home calls" so we need to
wait for the lock
-
- // Possible deadlock
- if (!waitingOnContext)
- {
- Logger.debug("LOCKING-WAITING (CTX) in Thread "
- + Thread.currentThread().getName()
- + " for id "+ctx.getId()
- +" ctx.hash "+ctx.hashCode());
- waitingOnContext = true;
- }
-
- // Try your luck again
- ctx = null;
- Thread.yield(); // Help out the OS.
- continue;
- }
- else
- {
- //We are in a home call so take the lock, take it!
- ctx.lock();
- }
- }
- if (waitingOnContext)
- {
- Logger.debug("FINISHED-LOCKING-WAITING (CTX) in Thread "
- + Thread.currentThread().getName()
- + " for id "
- + ctx.getId()
- + " ctx.hash " + ctx.hashCode());
- waitingOnContext = false;
- }
- }
- catch (InterruptedException ignored) {}
+ // Invoke through interceptors
+ return getNext().invokeHome(mi);
+ }
finally
{
- mutex.release();
+ //Other threads can be coming for this instance if it is in
cache
+ synchronized(ctx) {
+ // Always unlock, no matter what
+ ctx.unlock();
+ // Wake everyone up in case of create with Tx
contention
+ ctx.notifyAll();
+ }
+ //Do not send back to pools in any case, let the instance be
GC'ed
}
- } while (ctx == null);
-
- // Set context on the method invocation
- mi.setEnterpriseContext(ctx);
-
- // Go on, you won
- return getNext().invoke(mi);
-
- }
- catch (RemoteException e)
- {
- exceptionThrown = true;
- throw e;
- } catch (RuntimeException e)
- {
- exceptionThrown = true;
- throw e;
- } catch (Error e)
- {
- exceptionThrown = true;
- throw e;
- }
- finally
- {
- try
- {
- mutex.acquire();
- // Logger.debug("Release instance for "+id);
- // ctx can be null if cache.get throws an Exception, for
- // example when activating a bean.
- if (ctx != null)
+ }
+
+ public Object invoke(MethodInvocation mi)
+ throws Exception
+ {
+ // It's all about the context
+ EntityEnterpriseContext ctx = null;
+
+ // And it must correspond to the key.
+ CacheKey key = (CacheKey) mi.getId();
+
+ while (ctx == null)
{
- // unlock the context
- ctx.unlock();
-
- Transaction tx = ctx.getTransaction();
- if (tx != null
- && mi.getTransaction() != null
- && tx.equals(mi.getTransaction())
- && !((EntityEnterpriseContext)ctx).isInvoked())
- {
- // The context has been associated with this method's
transaction
- // but the entity has not been invoked upon yet, so let's
- // disassociate the transaction from the ctx.
- // I'm doing this because I'm assuming that the bean hasn't
been registered with
- // the TxManager.
- ctx.setTransaction(null);
- }
-
- // If an exception has been thrown, DO NOT remove the ctx
- // if the ctx has been registered in an InstanceSynchronization.
- // InstanceSynchronization will remove the key for us
- if (exceptionThrown &&
- (tx == null || (mi.getTransaction() != null &&
!((EntityEnterpriseContext)ctx).isInvoked())))
- {
- // Discard instance
- // EJB 1.1 spec 12.3.1
- //
- cache.remove(key);
- }
- else if (ctx.getId() == null)
- {
- // Work only if no transaction was encapsulating this remove()
- if (ctx.getTransaction() == null)
+ ctx = (EntityEnterpriseContext)
container.getInstanceCache().get(key);
+
+ //Next test is independent of whether the context is locked or
not, it is purely transactional
+ // Is the instance involved with another transaction? if so we
implement pessimistic locking
+ synchronized(ctx.getTxLock())
{
- // Here we arrive if the bean has been removed and no
- // transaction was associated with the remove, or if
- // the bean has been passivated
-
- // Remove from cache
- cache.remove(key);
-
- // It has been removed -> send to the pool
- // REVISIT: FIXME:
- // We really should only let passivation free an instance
because it is
- // quite possible that another thread is working with
- // the same context, but let's do it anyways.
- //
- container.getInstancePool().free(ctx);
+ Transaction tx = ctx.getTransaction();
+
+ // Do we have a running transaction with the context?
+ if (tx != null &&
+ // And are we trying to enter with another
transaction?
+ !tx.equals(mi.getTransaction()))
+ {
+ // That's no good, only one transaction per
context
+ // Let's put the thread to sleep the
transaction demarcation will wake them up
+ Logger.debug("Transactional contention on
context"+ctx.getId());
+
+ // Wait for it to finish, note that we use
wait() and not wait(5000), why?
+ // cause we got cojones, if there a problem in
this code we want a freeze not illusion
+ // Threads finishing the transaction must
notifyAll() on the ctx.txLock
+ try {ctx.getTxLock().wait();}
+
+ // We need to try again
+ finally {ctx = null; continue;}
+ }
+
+ /*
+ In future versions we can use copies of the instance
per transaction
+ */
}
- else
+
+ // The next test is the pure serialization from the EJB
specification.
+ synchronized(ctx)
{
- // We want to remove the bean, but it has a Tx associated
with
- // the remove() method. We remove it from the cache, to
avoid
- // that a successive insertion with same pk will break the
- // cache. Anyway we don't free the context, since the tx
must
- // finish. The EnterpriseContext instance will be GC and
not
- // recycled.
- cache.remove(key);
- }
- }
+ // synchronized is a time gap, when the thread enters
here it can be after "sleep"
+ // we need to make sure that stuff is still kosher
+
+ // Maybe my transaction already expired?
+ if (mi.getTransaction() != null &&
mi.getTransaction().getStatus() == Status.STATUS_MARKED_ROLLBACK)
+ throw new RuntimeException("Transaction marked
for rollback, possibly a timeout");
+
+ // We do not use pools any longer so the only thing
that can happen is that
+ // a ctx has a null id (instance removed) (no more
"wrong id" problem)
+ if (ctx.getId() == null)
+ {
+ // This will happen when the instance is
removed from cache
+ // We need to go through the same mechs and
get the new ctx
+ ctx = null;
+ continue;
+ }
+ // So the ctx is still valid and the transaction is
still game and we own the context
+ // so no one can change ctx. Make sure that all
access to ctx is synchronized.
+
+ // The ctx is still kosher go on with the real
serialization
+
+ //Is the context used already (with or without a
transaction), is it locked?
+ if (ctx.isLocked())
+ {
+ // It is locked but re-entrant calls permitted
(reentrant home ones are ok as well)
+ if (!isCallAllowed(mi))
+ {
+ // This instance is in use and you are
not permitted to reenter
+ // Go to sleep and wait for the lock
to be released
+ Logger.debug("Thread contention on
context"+key);
+
+ // we want to know about freezes so we
wait(), let us know if this locks
+ // Threads finishing invocation will
come here and notify() on the ctx
+ try {ctx.wait();}
+ catch (InterruptedException ignored)
{}
+ // We need to try again
+ finally {ctx = null; continue;}
+ }
+ else
+ {
+ //We are in a valid reentrant call so
take the lock, take it!
+ ctx.lock();
+ }
+ }
+ // No one is using that context
+ else
+ {
+
+ // We are now using the context
+ ctx.lock();
+ }
+
+ // The transaction is associated with the ctx while we
own the lock
+ ctx.setTransaction(mi.getTransaction());
+
+ }// end sychronized(ctx)
+
}
- else
- {
- /*
- * This used to happen in the old code.
- * Why? If the ctx is null, why should we remove it? Another
thread could
- * be screwed up by this.
- if (exceptionThrown)
- {
- // Discard instance
- // EJB 1.1 spec 12.3.1
- cache.remove(key);
- }
- */
+
+ // Set context on the method invocation
+ mi.setEnterpriseContext(ctx);
+
+ boolean exceptionThrown = false;
+
+ try {
+
+ // Go on, you won
+ return getNext().invoke(mi);
+
}
- }
- finally
- {
- mutex.release();
- }
+ catch (RemoteException e)
+ {
+ exceptionThrown = true;
+ throw e;
+ } catch (RuntimeException e)
+ {
+ exceptionThrown = true;
+ throw e;
+ } catch (Error e)
+ {
+ exceptionThrown = true;
+ throw e;
+ }
+ finally
+ {
+ // ctx can be null if cache.get throws an Exception, for
+ // example when activating a bean.
+ if (ctx != null)
+ {
+
+ synchronized(ctx)
+ {
+ ctx.unlock();
+
+ Transaction tx = ctx.getTransaction();
+
+ // If an exception has been thrown,
+ if (exceptionThrown &&
+ // if tx, the ctx has been registered
in an InstanceSynchronization.
+ // that will remove the context, so we
shouldn't.
+ // if no synchronization then we need to do
it by hand
+ !ctx.isTxSynchronized())
+ {
+ // Discard instance
+ // EJB 1.1 spec 12.3.1
+
container.getInstanceCache().remove(key);
+
+ // A cache removal wakes everyone up
+ ctx.notifyAll();
+ }
+
+ else if (ctx.getId() == null)
+ {
+ // The key from the MethodInvocation
still identifies the right cachekey
+
container.getInstanceCache().remove(key);
+
+ // A cache removal wakes everyone up
+ ctx.notifyAll();
+
+ // no more pool return
+ }
+
+ // We are done using the context so we wake up
the next thread waiting for the ctx
+ // marcf: I suspect we could use it only if
lock = 0 (code it in the context.lock in fact)
+ // this doesn't hurt here, meaning that even
if we don't wait for 0 to come up
+ // we will wake up a thread that will go back
to sleep and the next coming out of
+ // the body of code will wake the next one etc
until we reach 0. Reentrants are a pain
+ // a minor though and I really suspect not
checking for 0 is quite ok in all cases.
+ ctx.notify();
+ }
+ }// synchronized ctx
+ } // finally
}
- }
-
+
// Private --------------------------------------------------------
-
+
private static Method getEJBHome;
private static Method getHandle;
private static Method getPrimaryKey;
@@ -367,39 +317,39 @@
private static Method remove;
static
{
- try
- {
- Class[] noArg = new Class[0];
- getEJBHome = EJBObject.class.getMethod("getEJBHome", noArg);
- getHandle = EJBObject.class.getMethod("getHandle", noArg);
- getPrimaryKey = EJBObject.class.getMethod("getPrimaryKey", noArg);
- isIdentical = EJBObject.class.getMethod("isIdentical", new Class[]
{EJBObject.class});
- remove = EJBObject.class.getMethod("remove", noArg);
- }
- catch (Exception x) {x.printStackTrace();}
+ try
+ {
+ Class[] noArg = new Class[0];
+ getEJBHome = EJBObject.class.getMethod("getEJBHome", noArg);
+ getHandle = EJBObject.class.getMethod("getHandle", noArg);
+ getPrimaryKey = EJBObject.class.getMethod("getPrimaryKey",
noArg);
+ isIdentical = EJBObject.class.getMethod("isIdentical", new
Class[] {EJBObject.class});
+ remove = EJBObject.class.getMethod("remove", noArg);
+ }
+ catch (Exception x) {x.printStackTrace();}
}
-
+
private boolean isCallAllowed(MethodInvocation mi)
{
- boolean reentrant =
((EntityMetaData)container.getBeanMetaData()).isReentrant();
-
- if (reentrant)
- {
- return true;
- }
- else
- {
- Method m = mi.getMethod();
- if (m.equals(getEJBHome) ||
- m.equals(getHandle) ||
- m.equals(getPrimaryKey) ||
- m.equals(isIdentical) ||
- m.equals(remove))
- {
- return true;
- }
- }
-
- return false;
+ boolean reentrant =
((EntityMetaData)container.getBeanMetaData()).isReentrant();
+
+ if (reentrant)
+ {
+ return true;
+ }
+ else
+ {
+ Method m = mi.getMethod();
+ if (m.equals(getEJBHome) ||
+ m.equals(getHandle) ||
+ m.equals(getPrimaryKey) ||
+ m.equals(isIdentical) ||
+ m.equals(remove))
+ {
+ return true;
+ }
+ }
+
+ return false;
}
}
_______________________________________________
Jboss-development mailing list
[EMAIL PROTECTED]
http://lists.sourceforge.net/lists/listinfo/jboss-development