/* * 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.timer; import org.apache.openejb.ApplicationException; import org.apache.openejb.BeanContext; import org.apache.openejb.InterfaceType; import org.apache.openejb.OpenEJBException; import org.apache.openejb.OpenEJBRuntimeException; import org.apache.openejb.RpcContainer; import org.apache.openejb.core.BaseContext; import org.apache.openejb.core.timer.quartz.PatchedStdJDBCDelegate; import org.apache.openejb.core.transaction.TransactionType; import org.apache.openejb.loader.SystemInstance; import org.apache.openejb.monitoring.LocalMBeanServer; import org.apache.openejb.quartz.JobBuilder; import org.apache.openejb.quartz.JobDataMap; import org.apache.openejb.quartz.JobDetail; import org.apache.openejb.quartz.Scheduler; import org.apache.openejb.quartz.SchedulerException; import org.apache.openejb.quartz.Trigger; import org.apache.openejb.quartz.TriggerKey; import org.apache.openejb.quartz.impl.StdSchedulerFactory; import org.apache.openejb.quartz.impl.jdbcjobstore.JobStoreSupport; import org.apache.openejb.quartz.impl.jdbcjobstore.StdJDBCDelegate; import org.apache.openejb.quartz.impl.triggers.AbstractTrigger; import org.apache.openejb.quartz.listeners.SchedulerListenerSupport; import org.apache.openejb.quartz.simpl.RAMJobStore; import org.apache.openejb.resource.quartz.QuartzResourceAdapter; import org.apache.openejb.spi.ContainerSystem; import org.apache.openejb.util.LogCategory; import org.apache.openejb.util.Logger; import org.apache.openejb.util.SetAccessible; import javax.ejb.EJBContext; import javax.ejb.EJBException; import javax.ejb.ScheduleExpression; import javax.ejb.Timer; import javax.ejb.TimerConfig; import javax.transaction.Status; import javax.transaction.SystemException; import javax.transaction.TransactionManager; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Map; import java.util.Properties; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; public class EjbTimerServiceImpl implements EjbTimerService, Serializable { private static final long serialVersionUID = 1L; private static final Logger log = Logger.getInstance(LogCategory.TIMER, "org.apache.openejb.util.resources"); public static final String QUARTZ_JMX = "org.apache.openejb.quartz.scheduler.jmx.export"; public static final String QUARTZ_MAKE_SCHEDULER_THREAD_DAEMON = "org.apache.openejb.quartz.scheduler.makeSchedulerThreadDaemon"; public static final String OPENEJB_TIMEOUT_JOB_NAME = "OPENEJB_TIMEOUT_JOB"; public static final String OPENEJB_TIMEOUT_JOB_GROUP_NAME = "OPENEJB_TIMEOUT_GROUP"; public static final String EJB_TIMER_RETRY_ATTEMPTS = "EjbTimer.RetryAttempts"; public static final String OPENEJB_QUARTZ_USE_TCCL = "openejb.quartz.use-TCCL"; private boolean transacted; private int retryAttempts; private transient TransactionManager transactionManager; private transient BeanContext deployment; private transient TimerStore timerStore; private transient Scheduler scheduler; public EjbTimerServiceImpl(final BeanContext deployment, final TimerStore timerStore) { this(deployment, getDefaultTransactionManager(), timerStore, -1); log.isDebugEnabled(); // touch logger to force it to be initialized } public static TransactionManager getDefaultTransactionManager() { return SystemInstance.get().getComponent(TransactionManager.class); } public EjbTimerServiceImpl(final BeanContext deployment, final TransactionManager transactionManager, final TimerStore timerStore, final int retryAttempts) { this.deployment = deployment; this.transactionManager = transactionManager; this.timerStore = timerStore; final TransactionType transactionType = deployment.getTransactionType(deployment.getEjbTimeout()); this.transacted = transactionType == TransactionType.Required || transactionType == TransactionType.RequiresNew; this.retryAttempts = retryAttempts; if (retryAttempts < 0) { this.retryAttempts = deployment.getOptions().get(EJB_TIMER_RETRY_ATTEMPTS, 1); } } private void writeObject(final ObjectOutputStream out) throws IOException { out.writeUTF(deployment.getDeploymentID().toString()); out.writeBoolean(transacted); out.writeInt(retryAttempts); } private void readObject(final ObjectInputStream in) throws IOException { final String dId = in.readUTF(); transacted = in.readBoolean(); retryAttempts = in.readInt(); deployment = SystemInstance.get().getComponent(ContainerSystem.class).getBeanContext(dId); transactionManager = getDefaultTransactionManager(); timerStore = deployment.getEjbTimerService().getTimerStore(); scheduler = (Scheduler) Proxy.newProxyInstance(deployment.getClassLoader(), new Class<?>[]{Scheduler.class}, new LazyScheduler(deployment)); } public static synchronized Scheduler getDefaultScheduler(final BeanContext deployment) { Scheduler scheduler = deployment.get(Scheduler.class); if (scheduler != null) { boolean valid; try { valid = !scheduler.isShutdown(); } catch (final Exception ignored) { valid = false; } if (valid) { return scheduler; } } Scheduler thisScheduler; synchronized (deployment.getId()) { // should be done only once so no perf issues scheduler = deployment.get(Scheduler.class); if (scheduler != null) { return scheduler; } final Properties properties = new Properties(); int quartzProps = 0; quartzProps += putAll(properties, SystemInstance.get().getProperties()); quartzProps += putAll(properties, deployment.getModuleContext().getAppContext().getProperties()); quartzProps += putAll(properties, deployment.getModuleContext().getProperties()); quartzProps += putAll(properties, deployment.getProperties()); // custom config -> don't use default/global scheduler // if one day we want to keep a global config for a global scheduler (SystemInstance.get().getProperties()) we'll need to manage resume/pause etc correctly by app // since we have a scheduler by ejb today in such a case we don't need final boolean newInstance = quartzProps > 0; final SystemInstance systemInstance = SystemInstance.get(); scheduler = systemInstance.getComponent(Scheduler.class); if (scheduler == null || newInstance) { final boolean useTccl = "true".equalsIgnoreCase(properties.getProperty(OPENEJB_QUARTZ_USE_TCCL, "false")); defaultQuartzConfiguration(properties, deployment, newInstance, useTccl); try { // start in container context to avoid thread leaks final ClassLoader oldCl = Thread.currentThread().getContextClassLoader(); if (useTccl) { Thread.currentThread().setContextClassLoader(deployment.getClassLoader()); } else { Thread.currentThread().setContextClassLoader(EjbTimerServiceImpl.class.getClassLoader()); } try { thisScheduler = new StdSchedulerFactory(properties).getScheduler(); thisScheduler.start(); } finally { Thread.currentThread().setContextClassLoader(oldCl); } //durability is configured with true, which means that the job will be kept in the store even if no trigger is attached to it. //Currently, all the EJB beans share with the same job instance final JobDetail job = JobBuilder.newJob(EjbTimeoutJob.class) .withIdentity(OPENEJB_TIMEOUT_JOB_NAME, OPENEJB_TIMEOUT_JOB_GROUP_NAME) .storeDurably(true) .requestRecovery(false) .build(); thisScheduler.addJob(job, true); } catch (final SchedulerException e) { throw new OpenEJBRuntimeException("Fail to initialize the default scheduler", e); } if (!newInstance) { systemInstance.setComponent(Scheduler.class, thisScheduler); } } else { thisScheduler = scheduler; } deployment.set(Scheduler.class, thisScheduler); } return thisScheduler; } private static void defaultQuartzConfiguration(final Properties properties, final BeanContext deployment, final boolean newInstance, final boolean tccl) { final String defaultThreadPool = DefaultTimerThreadPoolAdapter.class.getName(); if (!properties.containsKey(StdSchedulerFactory.PROP_THREAD_POOL_CLASS)) { properties.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, defaultThreadPool); } if (!properties.containsKey(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME)) { properties.put(StdSchedulerFactory.PROP_SCHED_INSTANCE_NAME, "OpenEJB-TimerService-Scheduler"); } if (!properties.containsKey(StdSchedulerFactory.PROP_SCHED_SKIP_UPDATE_CHECK)) { properties.put(StdSchedulerFactory.PROP_SCHED_SKIP_UPDATE_CHECK, "true"); } if (!properties.containsKey("org.terracotta.quartz.skipUpdateCheck")) { properties.put("org.terracotta.quartz.skipUpdateCheck", "true"); } if (!properties.containsKey(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN)) { properties.put(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN, "true"); } if (!properties.containsKey(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN_WITH_WAIT)) { properties.put(StdSchedulerFactory.PROP_SCHED_INTERRUPT_JOBS_ON_SHUTDOWN_WITH_WAIT, "true"); } if (!properties.containsKey(QUARTZ_MAKE_SCHEDULER_THREAD_DAEMON)) { properties.put(QUARTZ_MAKE_SCHEDULER_THREAD_DAEMON, "true"); } if (!properties.containsKey(QUARTZ_JMX) && LocalMBeanServer.isJMXActive()) { properties.put(QUARTZ_JMX, "true"); } if (!properties.containsKey(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID)) { if (!newInstance) { properties.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, "OpenEJB"); } else { properties.setProperty(StdSchedulerFactory.PROP_SCHED_INSTANCE_ID, deployment.getDeploymentID().toString()); } } if (!tccl) { final String driverDelegate = properties.getProperty("org.apache.openejb.quartz.jobStore.driverDelegateClass"); if (driverDelegate != null && StdJDBCDelegate.class.getName().equals(driverDelegate)) { properties.put("org.apache.openejb.quartz.jobStore.driverDelegateClass", PatchedStdJDBCDelegate.class.getName()); } else if (driverDelegate != null) { log.info("You use " + driverDelegate + " driver delegate with quartz, ensure it doesn't use ObjectInputStream otherwise your custom TimerData can induce some issues"); } // adding our custom persister if (properties.containsKey("org.apache.openejb.quartz.jobStore.class") && !properties.containsKey("org.apache.openejb.quartz.jobStore.driverDelegateInitString")) { try { final Class<?> clazz = EjbTimerServiceImpl.class.getClassLoader().loadClass(properties.getProperty("org.apache.openejb.quartz.jobStore.class")); if (JobStoreSupport.class.isAssignableFrom(clazz)) { properties.put("org.apache.openejb.quartz.jobStore.driverDelegateInitString", "triggerPersistenceDelegateClasses=" + EJBCronTriggerPersistenceDelegate.class.getName()); } } catch (final Throwable th) { // no-op } } } if (defaultThreadPool.equals(properties.get(StdSchedulerFactory.PROP_THREAD_POOL_CLASS))) { if (properties.containsKey("org.apache.openejb.quartz.threadPool.threadCount") && !properties.containsKey(DefaultTimerThreadPoolAdapter.OPENEJB_TIMER_POOL_SIZE)) { log.info("Found property 'org.apache.openejb.quartz.threadPool.threadCount' for default thread pool, please use '" + DefaultTimerThreadPoolAdapter.OPENEJB_TIMER_POOL_SIZE + "' instead"); properties.put(DefaultTimerThreadPoolAdapter.OPENEJB_TIMER_POOL_SIZE, properties.getProperty("org.apache.openejb.quartz.threadPool.threadCount")); } if (properties.containsKey("org.quartz.threadPool.threadCount") && !properties.containsKey(DefaultTimerThreadPoolAdapter.OPENEJB_TIMER_POOL_SIZE)) { log.info("Found property 'org.quartz.threadPool.threadCount' for default thread pool, please use '" + DefaultTimerThreadPoolAdapter.OPENEJB_TIMER_POOL_SIZE + "' instead"); properties.put(DefaultTimerThreadPoolAdapter.OPENEJB_TIMER_POOL_SIZE, properties.getProperty("org.quartz.threadPool.threadCount")); } } // to ensure we can shutdown correctly, default doesn't support such a configuration if (!properties.getProperty(StdSchedulerFactory.PROP_JOB_STORE_CLASS, RAMJobStore.class.getName()).equals(RAMJobStore.class.getName())) { properties.put("org.apache.openejb.quartz.jobStore.makeThreadsDaemons", properties.getProperty("org.apache.openejb.quartz.jobStore.makeThreadsDaemon", "true")); } } private static int putAll(final Properties a, final Properties b) { int number = 0; for (final Map.Entry<Object, Object> entry : b.entrySet()) { final String key = entry.getKey().toString(); if (key.startsWith("org.quartz.") || key.startsWith("org.apache.openejb.quartz.") || key.startsWith("openejb.quartz.") || DefaultTimerThreadPoolAdapter.OPENEJB_TIMER_POOL_SIZE.equals(key) || "org.terracotta.quartz.skipUpdateCheck".equals(key)) { number++; } final Object value = entry.getValue(); if (String.class.isInstance(value)) { if (!key.startsWith("org.quartz")) { a.put(key, value); } else { a.put("org.apache.openejb.quartz" + key.substring("org.quartz".length()), value); } } } return number; } @Override public void stop() { cleanTimerData(); shutdownMyScheduler(); } private void cleanTimerData() { if (timerStore == null || scheduler == null || deployment == null) { return; } final Collection<TimerData> timerDatas = timerStore.getTimers(deployment.getDeploymentID().toString()); if (timerDatas == null) { return; } for (final TimerData data : timerDatas) { final Trigger trigger = data.getTrigger(); if (trigger == null) { continue; } final TriggerKey key = trigger.getKey(); try { data.stop(); } catch (final EJBException ignored) { log.warning("An error occured deleting trigger '" + key + "' on bean " + deployment.getDeploymentID()); } } } private void shutdownMyScheduler() { if (scheduler == null) { return; } boolean defaultScheduler = false; final Scheduler ds = SystemInstance.get().getComponent(Scheduler.class); try { // == is the faster way to test, we rely on name (key in quartz registry) only for serialization defaultScheduler = ds == scheduler || scheduler.getSchedulerName().equals(ds.getSchedulerName()); } catch (final Exception e) { // no-op: default should be fine } // if specific instance if (!defaultScheduler) { shutdown(scheduler); } } public static void shutdown() { shutdown(SystemInstance.get().getComponent(Scheduler.class)); } private static void shutdown(final Scheduler s) throws OpenEJBRuntimeException { try { if (null != s && !s.isShutdown() && s.isStarted()) { try { s.pauseAll(); } catch (final SchedulerException e) { // no-op } long timeout = SystemInstance.get().getOptions().get(QuartzResourceAdapter.OPENEJB_QUARTZ_TIMEOUT, 10000L); if (timeout < 1000L) { timeout = 1000L; } final CountDownLatch shutdownWait = new CountDownLatch(1); final AtomicReference<Throwable> ex = new AtomicReference<Throwable>(); String n = "Unknown"; try { n = s.getSchedulerName(); } catch (final SchedulerException e) { log.warning("EjbTimerService scheduler has no name"); } final String name = n; Thread stopThread = new Thread(name + " shutdown wait") { @Override public void run() { try { s.getListenerManager().addSchedulerListener(new SchedulerListenerSupport() { @Override public void schedulerShutdown() { shutdownWait.countDown(); } }); //Shutdown, but give running jobs a chance to complete. //User scheduled jobs should really implement InterruptableJob s.shutdown(true); } catch (final Throwable e) { ex.set(e); shutdownWait.countDown(); } } }; stopThread.setDaemon(true); stopThread.start(); boolean stopped = false; try { stopped = shutdownWait.await(timeout, TimeUnit.MILLISECONDS); } catch (final InterruptedException e) { //Ignore } try { if (!stopped || !s.isShutdown()) { stopThread = new Thread(name + " shutdown forced") { @Override public void run() { try { //Force a shutdown without waiting for jobs to complete. s.shutdown(false); log.warning("Forced " + name + " shutdown - Jobs may be incomplete"); } catch (final Throwable e) { ex.set(e); } } }; stopThread.setDaemon(true); stopThread.start(); try { //Give the forced shutdown a chance to complete stopThread.join(timeout); } catch (final InterruptedException e) { //Ignore } } } catch (final Throwable e) { ex.set(e); } final Throwable t = ex.get(); if (null != t) { throw new OpenEJBRuntimeException("Unable to shutdown " + name + " scheduler", t); } } } catch (final SchedulerException e) { //Ignore - This can only be a shutdown issue that we have no control over. } } @Override public void start() throws TimerStoreException { if (isStarted()) { return; } scheduler = getDefaultScheduler(deployment); // load saved timers final Collection<TimerData> timerDatas = timerStore.loadTimers(this, (String) deployment.getDeploymentID()); // schedule the saved timers for (final TimerData timerData : timerDatas) { initializeNewTimer(timerData); } } public TransactionManager getTransactionManager() { return transactionManager; } /** * Called from TimerData and start when a timer should be scheduled with the java.util.Timer. * * @param timerData the timer to schedule */ public void schedule(final TimerData timerData) throws TimerStoreException { start(); if (scheduler == null) { throw new TimerStoreException("Scheduler is not configured properly"); } timerData.setScheduler(scheduler); final Trigger trigger = timerData.getTrigger(); if (null == trigger) { try { if (!scheduler.isShutdown()) { log.warning("Failed to schedule: " + timerData.getInfo()); } } catch (final SchedulerException e) { //Ignore } } final AbstractTrigger<?> atrigger; if (trigger instanceof AbstractTrigger) { // is the case atrigger = (AbstractTrigger<?>) trigger; atrigger.setJobName(OPENEJB_TIMEOUT_JOB_NAME); atrigger.setJobGroup(OPENEJB_TIMEOUT_JOB_GROUP_NAME); } else { throw new OpenEJBRuntimeException("the trigger was not an AbstractTrigger - Should not be possible: " + trigger); } final JobDataMap triggerDataMap = trigger.getJobDataMap(); triggerDataMap.put(EjbTimeoutJob.EJB_TIMERS_SERVICE, this); triggerDataMap.put(EjbTimeoutJob.TIMER_DATA, timerData); try { final TriggerKey triggerKey = new TriggerKey(atrigger.getName(), atrigger.getGroup()); if (!scheduler.checkExists(triggerKey)) { scheduler.scheduleJob(trigger); } else if (Trigger.TriggerState.PAUSED.equals(scheduler.getTriggerState(triggerKey))) { // redeployment // more consistent in the semantic than a resume but resume would maybe be more relevant here scheduler.unscheduleJob(triggerKey); scheduler.scheduleJob(trigger); } } catch (final Exception e) { //TODO Any other actions we could do ? log.error("Could not schedule timer " + timerData, e); } } /** * Call back from TimerData and ejbTimeout when a timer has been cancelled (or is complete) and should be removed from stores. * * @param timerData the timer that was cancelled */ public void cancelled(final TimerData timerData) { // make sure it was removed from the store timerStore.removeTimer(timerData.getId()); } /** * Returns a timerData to the TimerStore, if a cancel() is rolled back. * * @param timerData the timer to be returned to the timer store */ public void addTimerData(final TimerData timerData) { try { timerStore.addTimerData(timerData); } catch (final Exception e) { log.warning("Could not add timer of type " + timerData.getType().name() + " due to " + e.getMessage()); } } @Override public Timer getTimer(final long timerId) { final TimerData timerData = timerStore.getTimer((String) deployment.getDeploymentID(), timerId); if (timerData != null) { return timerData.getTimer(); } else { return null; } } @Override public Collection<Timer> getTimers(final Object primaryKey) throws IllegalStateException { checkState(); final Collection<Timer> timers = new ArrayList<Timer>(); for (final TimerData timerData : timerStore.getTimers((String) deployment.getDeploymentID())) { // if (!CalendarTimerData.class.isInstance(timerData) || !CalendarTimerData.class.cast(timerData).isAutoCreated()) { timers.add(timerData.getTimer()); // } } return timers; } @Override public Timer createTimer(final Object primaryKey, final Method timeoutMethod, final long duration, final TimerConfig timerConfig) throws IllegalArgumentException, IllegalStateException, EJBException { if (duration < 0) { throw new IllegalArgumentException("duration is negative: " + duration); } checkState(); final Date expiration = new Date(System.currentTimeMillis() + duration); try { final TimerData timerData = timerStore.createSingleActionTimer(this, (String) deployment.getDeploymentID(), primaryKey, timeoutMethod, expiration, timerConfig); initializeNewTimer(timerData); return timerData.getTimer(); } catch (final TimerStoreException e) { throw new EJBException(e); } } @Override public Timer createTimer(final Object primaryKey, final Method timeoutMethod, final long initialDuration, final long intervalDuration, final TimerConfig timerConfig) throws IllegalArgumentException, IllegalStateException, EJBException { if (initialDuration < 0) { throw new IllegalArgumentException("initialDuration is negative: " + initialDuration); } if (intervalDuration < 0) { throw new IllegalArgumentException("intervalDuration is negative: " + intervalDuration); } checkState(); final Date initialExpiration = new Date(System.currentTimeMillis() + initialDuration); try { final TimerData timerData = timerStore.createIntervalTimer(this, (String) deployment.getDeploymentID(), primaryKey, timeoutMethod, initialExpiration, intervalDuration, timerConfig); initializeNewTimer(timerData); return timerData.getTimer(); } catch (final TimerStoreException e) { throw new EJBException(e); } } @Override public Timer createTimer(final Object primaryKey, final Method timeoutMethod, final Date expiration, final TimerConfig timerConfig) throws IllegalArgumentException, IllegalStateException, EJBException { if (expiration == null) { throw new IllegalArgumentException("expiration is null"); } if (expiration.getTime() < 0) { throw new IllegalArgumentException("expiration is negative: " + expiration.getTime()); } checkState(); try { final TimerData timerData = timerStore.createSingleActionTimer(this, (String) deployment.getDeploymentID(), primaryKey, timeoutMethod, expiration, timerConfig); initializeNewTimer(timerData); return timerData.getTimer(); } catch (final TimerStoreException e) { throw new EJBException(e); } } @Override public Timer createTimer(final Object primaryKey, final Method timeoutMethod, final Date initialExpiration, final long intervalDuration, final TimerConfig timerConfig) throws IllegalArgumentException, IllegalStateException, EJBException { if (initialExpiration == null) { throw new IllegalArgumentException("initialExpiration is null"); } if (initialExpiration.getTime() < 0) { throw new IllegalArgumentException("initialExpiration is negative: " + initialExpiration.getTime()); } if (intervalDuration < 0) { throw new IllegalArgumentException("intervalDuration is negative: " + intervalDuration); } checkState(); try { final TimerData timerData = timerStore.createIntervalTimer(this, (String) deployment.getDeploymentID(), primaryKey, timeoutMethod, initialExpiration, intervalDuration, timerConfig); initializeNewTimer(timerData); return timerData.getTimer(); } catch (final TimerStoreException e) { throw new EJBException(e); } } @Override public Timer createTimer(final Object primaryKey, final Method timeoutMethod, final ScheduleExpression scheduleExpression, final TimerConfig timerConfig) { if (scheduleExpression == null) { throw new IllegalArgumentException("scheduleExpression is null"); } //TODO add more schedule expression validation logic ? checkState(); try { final TimerData timerData = timerStore.createCalendarTimer(this, (String) deployment.getDeploymentID(), primaryKey, timeoutMethod, scheduleExpression, timerConfig, false); initializeNewTimer(timerData); return timerData.getTimer(); } catch (final TimerStoreException e) { throw new EJBException(e); } } @Override public TimerStore getTimerStore() { return timerStore; } @Override public boolean isStarted() { return scheduler != null; } public Scheduler getScheduler() { return scheduler; } private void initializeNewTimer(final TimerData timerData) { // mark this as a new timer... when the transaction completes it will schedule the timer timerData.newTimer(); } /** * Insure that timer methods can be invoked for the current operation on this Context. */ private void checkState() throws IllegalStateException { final BaseContext context = (BaseContext) deployment.get(EJBContext.class); context.doCheck(BaseContext.Call.timerMethod); } /** * This method calls the ejbTimeout method and starts a transaction if the timeout is transacted. * <p/> * This method will retry failed ejbTimeout calls until retryAttempts is exceeded. * * @param timerData the timer to call. */ @SuppressWarnings("ReturnInsideFinallyBlock") public void ejbTimeout(final TimerData timerData) { final Thread thread = Thread.currentThread(); final ClassLoader loader = thread.getContextClassLoader(); // container loader try { Timer timer = getTimer(timerData.getId()); // quartz can be backed by some advanced config (jdbc for instance) if (timer == null && timerStore instanceof MemoryTimerStore && timerData.getTimer() != null) { try { timerStore.addTimerData(timerData); timer = timerData.getTimer(); // TODO: replace memoryjobstore by the db one? } catch (final TimerStoreException e) { // shouldn't occur } // return; } for (int tries = 0; tries < 1 + retryAttempts; tries++) { boolean retry = false; // if transacted, begin the transaction if (transacted) { try { transactionManager.begin(); } catch (final Exception e) { log.warning("Exception occured while starting container transaction", e); return; } } // call the timeout method try { final RpcContainer container = (RpcContainer) deployment.getContainer(); if (container == null) { return; } final Method ejbTimeout = timerData.getTimeoutMethod(); if (ejbTimeout == null) { return; } // if app registered Synchronization we need it for commit()/rollback() // so forcing it and not relying on container for it thread.setContextClassLoader(deployment.getClassLoader() != null ? deployment.getClassLoader() : loader); SetAccessible.on(ejbTimeout); container.invoke(deployment.getDeploymentID(), InterfaceType.TIMEOUT, ejbTimeout.getDeclaringClass(), ejbTimeout, new Object[]{timer}, timerData.getPrimaryKey()); } catch (final RuntimeException e) { retry = true; // exception from a timer does not necessairly mean failure log.warning("RuntimeException from ejbTimeout on " + deployment.getDeploymentID(), e); try { transactionManager.setRollbackOnly(); } catch (final SystemException e1) { log.warning("Exception occured while setting RollbackOnly for container transaction", e1); } } catch (final OpenEJBException e) { retry = true; if (ApplicationException.class.isInstance(e)) { // we don't want to pollute logs log.debug("Exception from ejbTimeout on " + deployment.getDeploymentID(), e); } else { log.warning("Exception from ejbTimeout on " + deployment.getDeploymentID(), e); } if (transacted) { try { transactionManager.setRollbackOnly(); } catch (final SystemException e1) { log.warning("Exception occured while setting RollbackOnly for container transaction", e1); } } } finally { try { if (!transacted) { if (!retry) { return; } } else if (transactionManager.getStatus() == Status.STATUS_ACTIVE) { transactionManager.commit(); return; } else { // tx was marked rollback, so roll it back and retry. transactionManager.rollback(); } } catch (final Exception e) { log.warning("Exception occured while completing container transaction", e); } } } log.warning("Failed to execute ejbTimeout on " + timerData.getDeploymentId() + " successfully within " + retryAttempts + " attempts"); } catch (final RuntimeException e) { log.warning("RuntimeException occured while calling ejbTimeout", e); throw e; } catch (final Error e) { log.warning("Error occured while calling ejbTimeout", e); throw e; } finally { thread.setContextClassLoader(loader); // clean up the timer store //TODO shall we do all this via Quartz listener ??? if (timerData.getType() == TimerType.SingleAction) { timerStore.removeTimer(timerData.getId()); timerData.setExpired(true); } else if (timerData.getType() == TimerType.Calendar && timerData.getNextTimeout() == null) { timerStore.removeTimer(timerData.getId()); timerData.setExpired(true); } else { timerStore.updateIntervalTimer(timerData); } } } /** * The timer task registered with the java.util.Timer. The run method of this class * simply adds an execution of the ejbTimeout method to the thread pool. It is * important to use the thread pool, since the java.util.Timer is single threaded. */ /*private class EjbTimeoutTimerTask extends TimerTask { private final TimerData timerData; public EjbTimeoutTimerTask(TimerData timerData) { this.timerData = timerData; } public void run() { threadPool.execute(new Runnable() { public void run() { ejbTimeout(timerData); } }); } }*/ private static class LazyScheduler implements InvocationHandler { private final BeanContext ejb; public LazyScheduler(final BeanContext deployment) { ejb = deployment; } @Override public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { return method.invoke(getDefaultScheduler(ejb), args); } } }