/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. */ package org.rhq.enterprise.server.common; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Hashtable; import javax.interceptor.AroundInvoke; import javax.interceptor.InvocationContext; import com.arjuna.ats.arjuna.common.Uid; import com.arjuna.ats.arjuna.coordinator.BasicAction; import com.arjuna.ats.arjuna.coordinator.CheckedAction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.rhq.core.util.exception.ThrowableUtil; /** * This EJB3 interceptor's job is to install a new JBossTM CheckedAction * on the request thread's current transaction. * * The reason for this is technical, but in short, this allows SLSB methods * to be notified when a transaction has timed out so the SLSB can abort * what it is doing if it deems appropriate. * * The current thread's transaction will get a new CheckedAction that will * simply interrupt all threads that are currently associated with it. * * @author John Mazzitelli */ public class TransactionInterruptInterceptor { private static Log LOG = LogFactory.getLog(TransactionInterruptInterceptor.class); @AroundInvoke public Object addCheckedActionToTransactionManager(InvocationContext invocation_context) throws Exception { BasicAction currentTx = null; CheckedAction previousCheckedAction = null; try { currentTx = BasicAction.Current(); // Don't bother doing anything if the thread is currently not in a transaction. // But if it is in a tx, then install our new CheckedAction unless the method // does not want to be told about the transaction timeout (it tells us this // via the InterruptOnTransactionTimeout(false) annotation). if (currentTx != null) { Method method = invocation_context.getMethod(); InterruptOnTransactionTimeout anno = method.getAnnotation(InterruptOnTransactionTimeout.class); boolean interrupt = (anno != null) ? anno.value() : InterruptOnTransactionTimeout.DEFAULT_VALUE; TransactionInterruptCheckedAction newCheckedAction = new TransactionInterruptCheckedAction(interrupt); previousCheckedAction = setCheckedAction(currentTx, newCheckedAction); } } catch (Throwable t) { LOG.warn("Failure - if the transaction is aborted, its threads cannot be notified. Cause: " + ThrowableUtil.getAllMessages(t)); } try { return invocation_context.proceed(); } finally { if (currentTx != null && previousCheckedAction != null) { try { setCheckedAction(currentTx, previousCheckedAction); } catch (Exception e) { // paranoia - this should never happen, but ignore it if it does, keep the request going } } } } private CheckedAction setCheckedAction(BasicAction currentTx, CheckedAction newCheckedAction) { // In JBossAS 4.2.3, this BasicAction.setCheckedAction was a public method. // Backward compatibility was broken in JBossAS 7 when they limited the scope down to protected // and it no longer returns the old checked action. // We have to do the reflection trick to do what we want now. try { Class<?> clazz = BasicAction.class; Field checkedActionField = clazz.getDeclaredField("_checkedAction"); Method setMethod = clazz.getDeclaredMethod("setCheckedAction", CheckedAction.class); checkedActionField.setAccessible(true); setMethod.setAccessible(true); CheckedAction oldCheckedAction = (CheckedAction) checkedActionField.get(currentTx); setMethod.invoke(currentTx, newCheckedAction); return oldCheckedAction; } catch (Exception e) { LOG.warn("failed to set a new CheckedAction on the current tx: " + e); throw new RuntimeException("failed to set a new CheckedAction on the current tx: ", e); } } public class TransactionInterruptCheckedAction extends CheckedAction { private final boolean interruptThreads; public TransactionInterruptCheckedAction(boolean interrupt) { this.interruptThreads = interrupt; } @Override public synchronized void check(boolean isCommit, Uid actUid, Hashtable list) { try { // we only interrupt threads if we are not committing and we were told to interrupt threads boolean interrupt = this.interruptThreads && !isCommit; String template = "Transaction [" + actUid + "] is " + ((isCommit) ? "committing" : "aborting") + " with active thread [{0}]. interrupting=[" + interrupt + ']'; for (Object item : list.values()) { Thread thread = (Thread) item; // Show the full stacks to give a good indication of where the threads currently are; // if we are configured to not show this information, just log a single-line warning String logMsg = template.replace("{0}", thread.getName()); if (LOG.isInfoEnabled()) { try { Throwable t = new Throwable("STACK TRACE OF ACTIVE THREAD IN TERMINATING TRANSACTION"); t.setStackTrace(thread.getStackTrace()); LOG.info(logMsg, t); } catch (Exception e) { LOG.warn(logMsg); // paranoia - just in case throwable API doesn't behave } } else { LOG.warn(logMsg); } if (interrupt) { thread.interrupt(); } } } catch (Exception e) { LOG.warn(this.getClass() + ": check failed", e); } return; // don't bother calling super.check() - all it does is log warnings but is essentially a no-op } } }