/*
* 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.openejb.core.stateless;
import org.apache.openejb.ApplicationException;
import org.apache.openejb.BeanContext;
import org.apache.openejb.OpenEJBException;
import org.apache.openejb.SystemException;
import org.apache.openejb.cdi.CdiEjbBean;
import org.apache.openejb.core.InstanceContext;
import org.apache.openejb.core.Operation;
import org.apache.openejb.core.ThreadContext;
import org.apache.openejb.core.interceptor.InterceptorData;
import org.apache.openejb.core.interceptor.InterceptorInstance;
import org.apache.openejb.core.interceptor.InterceptorStack;
import org.apache.openejb.core.timer.TimerServiceWrapper;
import org.apache.openejb.loader.Options;
import org.apache.openejb.monitoring.LocalMBeanServer;
import org.apache.openejb.monitoring.ManagedMBean;
import org.apache.openejb.monitoring.ObjectNameBuilder;
import org.apache.openejb.monitoring.StatsInterceptor;
import org.apache.openejb.spi.SecurityService;
import org.apache.openejb.util.DaemonThreadFactory;
import org.apache.openejb.util.Duration;
import org.apache.openejb.util.LogCategory;
import org.apache.openejb.util.Logger;
import org.apache.openejb.util.PassthroughFactory;
import org.apache.openejb.util.Pool;
import org.apache.xbean.recipe.ObjectRecipe;
import org.apache.xbean.recipe.Option;
import javax.ejb.ConcurrentAccessTimeoutException;
import javax.ejb.EJBContext;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.naming.Context;
import javax.naming.NamingException;
import java.io.Flushable;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
public class StatelessInstanceManager {
private static final Logger logger = Logger.getInstance(LogCategory.OPENEJB, "org.apache.openejb.util.resources");
private static final Method removeSessionBeanMethod;
static { // initialize it only once
Method foundRemoveMethod;
try {
foundRemoveMethod = SessionBean.class.getDeclaredMethod("ejbRemove");
} catch (final NoSuchMethodException e) {
foundRemoveMethod = null;
}
removeSessionBeanMethod = foundRemoveMethod;
}
private final Duration accessTimeout;
private final Duration closeTimeout;
private final SecurityService securityService;
private final Pool.Builder poolBuilder;
private final ThreadPoolExecutor executor;
private final ScheduledExecutorService scheduledExecutor;
public StatelessInstanceManager(final SecurityService securityService,
final Duration accessTimeout, final Duration closeTimeout,
final Pool.Builder poolBuilder, final int callbackThreads,
final ScheduledExecutorService ses) {
this.securityService = securityService;
this.accessTimeout = accessTimeout;
this.closeTimeout = closeTimeout;
this.poolBuilder = poolBuilder;
this.scheduledExecutor = ses;
if (ScheduledThreadPoolExecutor.class.isInstance(ses) && !ScheduledThreadPoolExecutor.class.cast(ses).getRemoveOnCancelPolicy()) {
ScheduledThreadPoolExecutor.class.cast(ses).setRemoveOnCancelPolicy(true);
}
if (accessTimeout.getUnit() == null) {
accessTimeout.setUnit(TimeUnit.MILLISECONDS);
}
final int qsize = callbackThreads > 1 ? callbackThreads - 1 : 1;
final ThreadFactory threadFactory = new DaemonThreadFactory("StatelessPool.worker.");
this.executor = new ThreadPoolExecutor(
callbackThreads, callbackThreads * 2,
1L, TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>(qsize), threadFactory);
this.executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
@Override
public void rejectedExecution(final Runnable r, final ThreadPoolExecutor tpe) {
if (null == r || null == tpe || tpe.isShutdown() || tpe.isTerminated() || tpe.isTerminating()) {
return;
}
try {
if (!tpe.getQueue().offer(r, 20, TimeUnit.SECONDS)) {
logger.warning("Executor failed to run asynchronous process: " + r);
}
} catch (final InterruptedException e) {
//Ignore
}
}
});
}
private final class StatelessSupplier implements Pool.Supplier<Instance> {
private final BeanContext beanContext;
private StatelessSupplier(final BeanContext beanContext) {
this.beanContext = beanContext;
}
@Override
public void discard(final Instance instance, final Pool.Event reason) {
final ThreadContext ctx = new ThreadContext(beanContext, null);
final ThreadContext oldCallContext = ThreadContext.enter(ctx);
try {
freeInstance(ctx, instance);
} finally {
ThreadContext.exit(oldCallContext);
}
}
@Override
public Instance create() {
final ThreadContext ctx = new ThreadContext(beanContext, null);
final ThreadContext oldCallContext = ThreadContext.enter(ctx);
try {
return createInstance(ctx, ctx.getBeanContext());
} catch (final OpenEJBException e) {
logger.error("Unable to fill pool: for deployment '" + beanContext.getDeploymentID() + "'", e);
} finally {
ThreadContext.exit(oldCallContext);
}
return null;
}
}
public void destroy() {
if (executor != null) {
executor.shutdown();
try {
if (!executor.awaitTermination(10000, MILLISECONDS)) {
java.util.logging.Logger.getLogger(this.getClass().getName()).log(Level.WARNING, getClass().getSimpleName() + " pool timeout expired");
}
} catch (final InterruptedException e) {
Thread.interrupted();
}
}
if (scheduledExecutor != null) {
scheduledExecutor.shutdown();
try {
if (!scheduledExecutor.awaitTermination(10000, MILLISECONDS)) {
java.util.logging.Logger.getLogger(this.getClass().getName()).log(Level.WARNING, getClass().getSimpleName() + " pool timeout expired");
}
} catch (final InterruptedException e) {
Thread.interrupted();
}
}
}
/**
* Removes an instance from the pool and returns it for use
* by the container in business methods.
* <p/>
* If the pool is at it's limit the StrictPooling flag will
* cause this thread to wait.
* <p/>
* If StrictPooling is not enabled this method will create a
* new stateless bean instance performing all required injection
* and callbacks before returning it in a method ready state.
*
* @param callContext ThreadContext
* @return Object
* @throws OpenEJBException
*/
public Instance getInstance(final ThreadContext callContext) throws OpenEJBException {
final BeanContext beanContext = callContext.getBeanContext();
final Data data = (Data) beanContext.getContainerData();
Instance instance = null;
try {
final Pool<Instance>.Entry entry = data.poolPop();
if (entry != null) {
instance = entry.get();
instance.setPoolEntry(entry);
}
} catch (final TimeoutException e) {
final String msg = "No instances available in Stateless Session Bean pool. Waited " + data.accessTimeout.toString();
final ConcurrentAccessTimeoutException timeoutException = new ConcurrentAccessTimeoutException(msg);
timeoutException.fillInStackTrace();
throw new ApplicationException(timeoutException);
} catch (final InterruptedException e) {
Thread.interrupted();
throw new OpenEJBException("Unexpected Interruption of current thread: ", e);
}
if (null == instance) {
instance = createInstance(callContext, beanContext);
}
return instance;
}
private Instance createInstance(final ThreadContext callContext, final BeanContext beanContext) throws ApplicationException {
try {
final InstanceContext context = beanContext.newInstance();
return new Instance(context.getBean(), context.getInterceptors(), context.getCreationalContext());
} catch (Throwable e) {
if (e instanceof InvocationTargetException) {
e = ((InvocationTargetException) e).getTargetException();
}
final String t = "The bean instance " + beanContext.getDeploymentID() + " threw a system exception:" + e;
logger.error(t, e);
throw new ApplicationException(new RemoteException("Cannot obtain a free instance.", e));
}
}
/**
* All instances are removed from the pool in getInstance(...). They are only
* returned by the StatelessContainer via this method under two circumstances.
* <p/>
* 1. The business method returns normally
* 2. The business method throws an application exception
* <p/>
* Instances are not returned to the pool if the business method threw a system
* exception.
*
* @param callContext ThreadContext
* @param bean Object
* @throws OpenEJBException
*/
public void poolInstance(final ThreadContext callContext, final Object bean) throws OpenEJBException {
if (bean == null) {
throw new SystemException("Invalid arguments");
}
final Instance instance = Instance.class.cast(bean);
final BeanContext beanContext = callContext.getBeanContext();
final Data data = (Data) beanContext.getContainerData();
final Pool<Instance> pool = data.getPool();
if (instance.getPoolEntry() != null) {
pool.push(instance.getPoolEntry());
} else {
pool.push(instance);
}
}
/**
* This method is called to release the semaphore in case of the business method
* throwing a system exception
*
* @param callContext ThreadContext
* @param bean Object
*/
public void discardInstance(final ThreadContext callContext, final Object bean) throws SystemException {
if (bean == null) {
throw new SystemException("Invalid arguments");
}
final Instance instance = Instance.class.cast(bean);
final BeanContext beanContext = callContext.getBeanContext();
final Data data = (Data) beanContext.getContainerData();
if (null != data) {
final Pool<Instance> pool = data.getPool();
pool.discard(instance.getPoolEntry());
}
}
@SuppressWarnings("unchecked")
private void freeInstance(final ThreadContext callContext, final Instance instance) {
try {
callContext.setCurrentOperation(Operation.PRE_DESTROY);
final BeanContext beanContext = callContext.getBeanContext();
final Method remove = instance.bean instanceof SessionBean ? removeSessionBeanMethod : null;
final List<InterceptorData> callbackInterceptors = beanContext.getCallbackInterceptors();
final InterceptorStack interceptorStack = new InterceptorStack(instance.bean, remove, Operation.PRE_DESTROY, callbackInterceptors, instance.interceptors);
final CdiEjbBean<Object> bean = beanContext.get(CdiEjbBean.class);
if (bean != null) { // TODO: see if it should be called before or after next call
bean.getInjectionTarget().preDestroy(instance.bean);
}
interceptorStack.invoke();
if (instance.creationalContext != null) {
instance.creationalContext.release();
}
} catch (final Throwable re) {
logger.error("The bean instance " + instance + " threw a system exception:" + re, re);
}
}
@SuppressWarnings("unchecked")
public void deploy(final BeanContext beanContext) throws OpenEJBException {
final Options options = new Options(beanContext.getProperties());
final Duration accessTimeout = getDuration(
options,
"AccessTimeout",
getDuration(options, "Timeout", this.accessTimeout, TimeUnit.MILLISECONDS), // default timeout
TimeUnit.MILLISECONDS
);
final Duration closeTimeout = getDuration(options, "CloseTimeout", this.closeTimeout, TimeUnit.MINUTES);
final ObjectRecipe recipe = PassthroughFactory.recipe(new Pool.Builder(poolBuilder));
recipe.allow(Option.CASE_INSENSITIVE_FACTORY);
recipe.allow(Option.CASE_INSENSITIVE_PROPERTIES);
recipe.allow(Option.IGNORE_MISSING_PROPERTIES);
recipe.setAllProperties(beanContext.getProperties());
final Pool.Builder builder = (Pool.Builder) recipe.create();
setDefault(builder.getMaxAge(), TimeUnit.HOURS);
setDefault(builder.getIdleTimeout(), TimeUnit.MINUTES);
setDefault(builder.getInterval(), TimeUnit.MINUTES);
final StatelessSupplier supplier = new StatelessSupplier(beanContext);
builder.setSupplier(supplier);
builder.setExecutor(executor);
builder.setScheduledExecutor(scheduledExecutor);
final Data data = new Data(builder.build(), accessTimeout, closeTimeout);
beanContext.setContainerData(data);
beanContext.set(EJBContext.class, data.sessionContext);
try {
final Context context = beanContext.getJndiEnc();
context.bind("comp/EJBContext", data.sessionContext);
context.bind("comp/WebServiceContext", new EjbWsContext(data.sessionContext));
context.bind("comp/TimerService", new TimerServiceWrapper());
} catch (final NamingException e) {
throw new OpenEJBException("Failed to bind EJBContext/WebServiceContext/TimerService", e);
}
final int min = builder.getMin();
final long maxAge = builder.getMaxAge().getTime(TimeUnit.MILLISECONDS);
final double maxAgeOffset = builder.getMaxAgeOffset();
final ObjectNameBuilder jmxName = new ObjectNameBuilder("openejb.management");
jmxName.set("J2EEServer", "openejb");
jmxName.set("J2EEApplication", null);
jmxName.set("EJBModule", beanContext.getModuleID());
jmxName.set("StatelessSessionBean", beanContext.getEjbName());
jmxName.set("name", beanContext.getEjbName());
final MBeanServer server = LocalMBeanServer.get();
// Create stats interceptor
if (StatsInterceptor.isStatsActivated()) {
StatsInterceptor stats = null;
for (final InterceptorInstance interceptor : beanContext.getUserAndSystemInterceptors()) {
if (interceptor.getInterceptor() instanceof StatsInterceptor) {
stats = (StatsInterceptor) interceptor.getInterceptor();
}
}
if (stats == null) { // normally useless
stats = new StatsInterceptor(beanContext.getBeanClass());
beanContext.addFirstSystemInterceptor(stats);
}
// register the invocation stats interceptor
try {
final ObjectName objectName = jmxName.set("j2eeType", "Invocations").build();
if (server.isRegistered(objectName)) {
server.unregisterMBean(objectName);
}
server.registerMBean(new ManagedMBean(stats), objectName);
data.add(objectName);
} catch (final Exception e) {
logger.error("Unable to register MBean ", e);
}
}
// register the pool
try {
final ObjectName objectName = jmxName.set("j2eeType", "Pool").build();
if (server.isRegistered(objectName)) {
server.unregisterMBean(objectName);
}
server.registerMBean(new ManagedMBean(data.pool), objectName);
data.add(objectName);
} catch (final Exception e) {
logger.error("Unable to register MBean ", e);
}
// Finally, fill the pool and start it
if (!options.get("BackgroundStartup", false) && min > 0) {
final ExecutorService es = Executors.newFixedThreadPool(min);
for (int i = 0; i < min; i++) {
es.submit(new InstanceCreatorRunnable(maxAge, i, min, maxAgeOffset, data, supplier));
}
es.shutdown();
try {
es.awaitTermination(5, TimeUnit.MINUTES);
} catch (final InterruptedException e) {
logger.error("can't fill the stateless pool", e);
}
}
data.getPool().start();
}
private void setDefault(final Duration duration, final TimeUnit unit) {
if (duration.getUnit() == null) {
duration.setUnit(unit);
}
}
private Duration getDuration(final Options options, final String property, final Duration defaultValue, final TimeUnit defaultUnit) {
final String s = options.get(property, defaultValue.toString());
final Duration duration = new Duration(s);
if (duration.getUnit() == null) {
duration.setUnit(defaultUnit);
}
return duration;
}
public void undeploy(final BeanContext beanContext) {
final Data data = (Data) beanContext.getContainerData();
if (data == null) {
return;
}
final MBeanServer server = LocalMBeanServer.get();
for (final ObjectName objectName : data.jmxNames) {
try {
server.unregisterMBean(objectName);
} catch (final Exception e) {
logger.error("Unable to unregister MBean " + objectName);
}
}
try {
if (!data.closePool()) {
logger.error("Timed-out waiting for stateless pool to close: for deployment '" + beanContext.getDeploymentID() + "'");
}
} catch (final InterruptedException e) {
Thread.interrupted();
}
beanContext.setContainerData(null);
}
private final class Data {
private final Pool<Instance> pool;
private final Duration accessTimeout;
private final Duration closeTimeout;
private final List<ObjectName> jmxNames = new ArrayList<ObjectName>();
private final SessionContext sessionContext;
private Data(final Pool<Instance> pool, final Duration accessTimeout, final Duration closeTimeout) {
this.pool = pool;
this.accessTimeout = accessTimeout;
this.closeTimeout = closeTimeout;
this.sessionContext = new StatelessContext(securityService, new Flushable() {
@Override
public void flush() throws IOException {
getPool().flush();
}
});
}
public Duration getAccessTimeout() {
return accessTimeout;
}
public Pool<Instance>.Entry poolPop() throws InterruptedException, TimeoutException {
return pool.pop(accessTimeout.getTime(), accessTimeout.getUnit());
}
public Pool<Instance> getPool() {
return pool;
}
public boolean closePool() throws InterruptedException {
return pool.close(closeTimeout.getTime(), closeTimeout.getUnit());
}
public ObjectName add(final ObjectName name) {
jmxNames.add(name);
return name;
}
}
private final class InstanceCreatorRunnable implements Runnable {
private final long maxAge;
private final long iteration;
private final double maxAgeOffset;
private final long min;
private final Data data;
private final StatelessSupplier supplier;
private InstanceCreatorRunnable(final long maxAge, final long iteration, final long min, final double maxAgeOffset, final Data data, final StatelessSupplier supplier) {
this.maxAge = maxAge;
this.iteration = iteration;
this.min = min;
this.maxAgeOffset = maxAgeOffset;
this.data = data;
this.supplier = supplier;
}
@Override
public void run() {
final Instance obj = supplier.create();
if (obj != null) {
final long offset = maxAge > 0 ? (long) (maxAge / maxAgeOffset * min * iteration) % maxAge : 0l;
data.getPool().add(obj, offset);
}
}
}
}