/*
* JBoss, Home of Professional Open Source.
* Copyright 2008, Red Hat Middleware LLC, and individual contributors
* as indicated by the @author tags. See the copyright.txt file 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.ejb.plugins;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.jboss.ejb.Container;
import org.jboss.ejb.EjbModule;
import org.jboss.ha.framework.interfaces.GenericClusteringException;
import org.jboss.invocation.Invocation;
import org.jboss.system.ServiceMBean;
import javax.management.AttributeChangeNotification;
import javax.management.AttributeChangeNotificationFilter;
import javax.management.Notification;
import javax.management.NotificationListener;
/**
* Track the incoming invocations and when shuting down a container (stop or
* destroy), waits for current invocations to finish before returning the
* stop or destroy call. This interceptor can be important in clustered environment
* where shuting down a node doesn't necessarly mean that an application cannot
* be reached: other nodes may still be servicing. Consequently, it is important
* to have a clean shutdown to keep a coherent behaviour cluster-wide.
*
* To avoid strange or inefficient behaviour, the facade session bean (if any)
* should be stopped first thus not blocking invocations in a middle-step (i.e.
* facade making multiple invocations to "sub-beans": if a "sub-bean" is
* shut down, the facade will get an exception in the middle of its activity)
*
* @see <related>
*
* @author <a href="mailto:sacha.labourey@cogito-info.ch">Sacha Labourey</a>.
* @version $Revision: 89150 $
*
* <p><b>Revisions:</b>
*
* <p><b>14 avril 2002 Sacha Labourey:</b>
* <ul>
* <li> First implementation </li>
* </ul>
*/
public class CleanShutdownInterceptor extends AbstractInterceptor
{
// Constants -----------------------------------------------------
// Attributes ----------------------------------------------------
protected Container container = null;
protected EjbModule ejbModule = null;
protected String ejbModuleName = null;
private static ThreadLocal<String> currentModule = new ThreadLocal<String>();
protected boolean allowInvocations = false;
protected boolean allowRemoteInvocations = false;
protected boolean isDebugEnabled = false;
public long runningInvocations = 0;
public long runningHomeInvocations = 0;
public long shutdownTimeout = 60000;
public long readAcquireTimeMs = 10000;
protected ReadWriteLock rwLock = new ReentrantReadWriteLock(true);
// Static --------------------------------------------------------
private static final String METHOD_INVOCATION_TAG = "WrappingEjbModuleName";
// Constructors --------------------------------------------------
public CleanShutdownInterceptor ()
{
}
// Public --------------------------------------------------------
public void onlyAllowLocalInvocations ()
{
if (isDebugEnabled) log.debug ("Only allow local invocation from now on: " + this.container.getServiceName ().toString ());
this.allowRemoteInvocations = false;
}
public void waitForNoMoreInvocations ()
{
this.log.debug ("Waiting that the container " + container.getJmxName () + " finishes its running invocations. " +
this.runningHomeInvocations + " current home invocations and " +
this.runningInvocations + " current remote invocations.");
purgeRunningInvocations ();
if (isDebugEnabled) log.debug ("... Done: no more remote invocations currently running in this container.");
}
// Z implementation ----------------------------------------------
// AbstractInterceptor overrides ---------------------------------------------------
public void create() throws Exception {
super.create ();
this.allowInvocations = false;
this.allowRemoteInvocations = false;
this.isDebugEnabled = log.isDebugEnabled ();
ejbModuleName = ejbModule.getServiceName().toString();
// we register our inner-class to retrieve STATE notifications from our container
//
AttributeChangeNotificationFilter filter = new AttributeChangeNotificationFilter ();
filter.enableAttribute ("State");
this.container.getServer ().
addNotificationListener (this.container.getEjbModule ().getServiceName (),
new CleanShutdownInterceptor.StateChangeListener (),
filter,
null);
// we need a way to find all CleanShutDownInterceptor of an EjbModule
//
ejbModule.putModuleData ("CleanShutDownInterceptor-" + this.container.getServiceName ().toString (), this);
}
public void start() throws Exception {
super.start();
this.allowInvocations = true;
this.allowRemoteInvocations = true;
}
public void stop() {
super.stop ();
this.log.debug ("Stopping container " + container.getJmxName () + ". " +
this.runningHomeInvocations + " current home invocations and " +
this.runningInvocations + " current remote invocations.");
forbidInvocations ();
}
public void destroy() {
super.destroy ();
this.log.debug ("Destroying container " + container.getJmxName ().toString () + ". " +
this.runningHomeInvocations + " current home invocations and " +
this.runningInvocations + " current remote invocations.");
forbidInvocations() ;
}
public Object invokeHome (Invocation mi)
throws Exception
{
if (this.allowInvocations)
{
String origin = getOrigin (mi);
boolean isAppLocalCall = ejbModuleName.equals (origin);
if (!this.allowRemoteInvocations && !isAppLocalCall)
// it is a remote call and they are currently forbidden!
//
{
if (isDebugEnabled) log.debug ("Refusing a remote home invocation. here= " + ejbModuleName + "; Origin= " + origin);
throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
"This application does not accept remote calls any more");
}
// we need to acquire the read lock. If we cannot directly, it means
// that the stop/destroy call has gotten the write lock in the meantime
//
try
{
if (!isAppLocalCall) // we only consider remote calls => every local originates from a remote!
{
if (!rwLock.readLock().tryLock(readAcquireTimeMs, TimeUnit.MILLISECONDS))
throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
"Container is shuting down on this node (timeout)");
}
}
catch (java.lang.InterruptedException ie)
{
Thread.currentThread().interrupt();
throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
"Container is shuting down on this node");
}
runningHomeInvocations++;
try
{
if (!isAppLocalCall)
setOrigin (mi);
return this.getNext ().invokeHome (mi);
}
catch (GenericClusteringException gce)
{
// a gce exception has be thrown somewhere else: we need to modify its flag
// and forward it. We could add optimisations at this level by having some
// "idempotent" flag at the container level
//
gce.setCompletionStatus (gce.COMPLETED_MAYBE);
throw gce;
}
finally
{
if (!isAppLocalCall)
revertOrigin (mi, origin);
runningHomeInvocations--;
if (!isAppLocalCall) // we only consider remote calls => every local originates from a remote!
rwLock.readLock().unlock();
}
}
else
throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
"Container is not allowing invocations as it failed to start or is shutting down");
}
public Object invoke (Invocation mi)
throws Exception
{
if (this.allowInvocations)
{
String origin = getOrigin (mi);
boolean isAppLocalCall = ejbModuleName.equals (origin);
if (!this.allowRemoteInvocations && !isAppLocalCall)
// it is a remote call and they are currently forbidden!
//
{
if (isDebugEnabled) log.debug ("Refusing a remote invocation");
throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
"This application does not accept remote calls any more");
}
// we need to acquire the read lock. If we cannot directly, it means
// that the stop/destroy call has gotten the write lock in the meantime
//
try
{
if (!isAppLocalCall) // we only consider remote calls => every local originates from a remote!
{
if (!rwLock.readLock ().tryLock(readAcquireTimeMs, TimeUnit.MILLISECONDS))
throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
"Container is shuting down on this node (timeout)");
}
}
catch (java.lang.InterruptedException ie)
{
Thread.currentThread().interrupt();
throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
"Container is shuting down on this node");
}
runningInvocations++;
try
{
if (!isAppLocalCall)
setOrigin (mi);
return this.getNext ().invoke (mi);
}
catch (GenericClusteringException gce)
{
// a gce exception has be thrown somewhere else: we need to modify its flag
// and forward it. We could add optimisations at this level by having some
// "idempotent" flag at the container level
//
gce.setCompletionStatus (gce.COMPLETED_MAYBE);
throw gce;
}
finally
{
if (!isAppLocalCall)
revertOrigin (mi, origin);
runningInvocations--;
if (!isAppLocalCall) // we only consider remote calls => every local originates from a remote!
rwLock.readLock().unlock();
}
}
else
throw new GenericClusteringException (GenericClusteringException.COMPLETED_NO,
"Container is not allowing invocations as it failed to start or is shutting down");
}
public Container getContainer ()
{
return this.container;
}
/** This callback is set by the container so that the plugin may access it
*
* @param con The container using this plugin.
*/
public void setContainer (Container con)
{
this.container = con;
if (con != null)
this.ejbModule = con.getEjbModule ();
else
this.ejbModule = null;
}
// Package protected ---------------------------------------------
// Protected -----------------------------------------------------
protected void forbidInvocations ()
{
this.allowInvocations = false;
purgeRunningInvocations();
}
protected void purgeRunningInvocations ()
{
try
{
if (this.rwLock.writeLock().tryLock(shutdownTimeout, TimeUnit.MILLISECONDS))
this.rwLock.writeLock().unlock();
else
log.info ("Possible running invocations not terminated " +
"while leaving the container. Home: " + runningHomeInvocations +
". Remote: " + runningInvocations + ".");
}
catch (Exception e)
{
log.info ("Exception while waiting for running invocations " +
"to leave container. Home: " + runningHomeInvocations +
". Remote: " + runningInvocations + ".", e);
}
finally
{
}
}
protected String getOrigin (Invocation mi)
{
String value = currentModule.get();
if (log.isTraceEnabled())
log.trace ("GET_ORIGIN: " + value + " in " + this.container.getServiceName ().toString ());
return value;
}
protected void setOrigin (Invocation mi)
{
currentModule.set(this.ejbModuleName);
}
protected void revertOrigin (Invocation mi, String origin)
{
if (log.isTraceEnabled()) log.trace ("Crossing ejbModule border from " + this.ejbModuleName + " to " + origin);
currentModule.set (origin);
}
// Private -------------------------------------------------------
protected void containerIsAboutToStop ()
{
// Distinction between between JMX proxies and underlying services seems
// to lead to a 2nd STOPPING event; ignore it or we get an NPE
if (ejbModule == null)
{
log.debug("Received STOPPING notification after container already set to null; ignoring");
return;
}
log.debug ("Container about to stop: disabling HA-RMI access to bean from interceptor");
// This is bad: we should have some kind of code (manager) associated
// with this ejbModule. We mimic this by electing the first ProxyFactoryHA
// as a manager
//
boolean iAmTheManager = !Boolean.TRUE.equals (ejbModule.getModuleData ("ShutdownInterceptorElected"));
if (iAmTheManager)
{
ejbModule.putModuleData ("ShutdownInterceptorElected", Boolean.TRUE);
if (isDebugEnabled) log.debug ("Container is about to stop and I am the manager of the first step: blocking remote calls");
// in a first step, all interceptors must refuse/redirect remote invocations
//
Collection containers = ejbModule.getContainers ();
Iterator containersIter = containers.iterator ();
while (containersIter.hasNext ())
{
Container otherContainer = (Container)containersIter.next ();
CleanShutdownInterceptor inter = (CleanShutdownInterceptor)
ejbModule.getModuleData ("CleanShutDownInterceptor-" + otherContainer.getServiceName ().toString ());
if (inter == null)
{
log.debug ("Found an EJB that doesnt have a clean-shutdown interceptor: " + otherContainer.getJmxName ());
}
else
{
inter.onlyAllowLocalInvocations ();
}
}
}
else
{
if (isDebugEnabled) log.debug ("Container is about to stop but I am not the manager: I don't manage the first step of the process.");
}
// in a second step, all container, manager or not, will wait that no more invocation
// are running through
// The cycling around other interceptor is managed by the JMX callbacks, not by us
//
waitForNoMoreInvocations ();
}
// Inner classes -------------------------------------------------
class StateChangeListener implements NotificationListener
{
public void handleNotification (Notification notification, java.lang.Object handback)
{
if (notification instanceof AttributeChangeNotification)
{
AttributeChangeNotification notif = (AttributeChangeNotification) notification;
int value = ((Integer)notif.getNewValue()).intValue ();
// Start management is handled by the ProxyFactoryHA, not here
if (value == ServiceMBean.STOPPING)
{
containerIsAboutToStop ();
}
}
}
}
}