/* * 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.lang.reflect.Method; import java.util.TimerTask; import javax.ejb.EJBException; import javax.transaction.RollbackException; import javax.transaction.Status; import javax.transaction.Synchronization; import javax.transaction.Transaction; import org.jboss.ejb.BeanLock; import org.jboss.ejb.Container; import org.jboss.ejb.EntityCache; import org.jboss.ejb.EntityContainer; import org.jboss.ejb.EntityEnterpriseContext; import org.jboss.ejb.GlobalTxEntityMap; import org.jboss.invocation.Invocation; import org.jboss.metadata.ConfigurationMetaData; import org.jboss.util.NestedRuntimeException; /** * The role of this interceptor is to synchronize the state of the cache with * the underlying storage. It does this with the ejbLoad and ejbStore * semantics of the EJB specification. In the presence of a transaction this * is triggered by transaction demarcation. It registers a callback with the * underlying transaction monitor through the JTA interfaces. If there is no * transaction the policy is to store state upon returning from invocation. * The synchronization polices A,B,C of the specification are taken care of * here. * * <p><b>WARNING: critical code</b>, get approval from senior developers * before changing. * * @author <a href="mailto:marc.fleury@jboss.org">Marc Fleury</a> * @author <a href="mailto:Scott.Stark@jboss.org">Scott Stark</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @version $Revision: 81030 $ */ public class EntitySynchronizationInterceptor extends AbstractInterceptor { /** Task for refreshing contexts */ private ValidContextsRefresher vcr; /** * The current commit option. */ protected int commitOption; /** * The refresh rate for commit option d */ protected long optionDRefreshRate; /** * The container of this interceptor. */ protected EntityContainer container; public Container getContainer() { return container; } public void setContainer(Container container) { this.container = (EntityContainer) container; } public void create() throws Exception { try { ConfigurationMetaData configuration = container.getBeanMetaData().getContainerConfiguration(); commitOption = configuration.getCommitOption(); optionDRefreshRate = configuration.getOptionDRefreshRate(); } catch(Exception e) { log.warn(e.getMessage()); } } public void start() { try { //start up the validContexts thread if commit option D if (commitOption == ConfigurationMetaData.D_COMMIT_OPTION) { vcr = new ValidContextsRefresher(); LRUEnterpriseContextCachePolicy.tasksTimer.schedule(vcr, optionDRefreshRate, optionDRefreshRate); log.debug("Scheduled a cache flush every " + optionDRefreshRate/1000 + " seconds"); } } catch(Exception e) { vcr = null; log.warn("problem scheduling valid contexts refresher", e); } } public void stop() { if (vcr != null) { TimerTask temp = vcr; vcr = null; temp.cancel(); } } protected Synchronization createSynchronization(Transaction tx, EntityEnterpriseContext ctx) { return new InstanceSynchronization(tx, ctx); } /** * Register a transaction synchronization callback with a context. */ protected void register(EntityEnterpriseContext ctx, Transaction tx) { boolean trace = log.isTraceEnabled(); if(trace) log.trace("register, ctx=" + ctx + ", tx=" + tx); EntityContainer ctxContainer = null; try { ctxContainer = (EntityContainer)ctx.getContainer(); if(!ctx.hasTxSynchronization()) { // Create a new synchronization Synchronization synch = createSynchronization(tx, ctx); // We want to be notified when the transaction commits tx.registerSynchronization(synch); ctx.hasTxSynchronization(true); } //mark it dirty in global tx entity map if it is not read only if(!ctxContainer.isReadOnly()) { ctx.getTxAssociation().scheduleSync(tx, ctx); } } catch(RollbackException e) { // The state in the instance is to be discarded, we force a reload of state synchronized(ctx) { ctx.setValid(false); ctx.hasTxSynchronization(false); ctx.setTransaction(null); ctx.setTxAssociation(GlobalTxEntityMap.NONE); } throw new EJBException(e); } catch(Throwable t) { // If anything goes wrong with the association remove the ctx-tx association ctx.hasTxSynchronization(false); ctx.setTxAssociation(GlobalTxEntityMap.NONE); if(t instanceof RuntimeException) throw (RuntimeException)t; else if(t instanceof Error) throw (Error)t; else if(t instanceof Exception) throw new EJBException((Exception)t); else throw new NestedRuntimeException(t); } } public Object invokeHome(Invocation mi) throws Exception { EntityEnterpriseContext ctx = (EntityEnterpriseContext)mi.getEnterpriseContext(); Transaction tx = mi.getTransaction(); Object rtn = getNext().invokeHome(mi); // An anonymous context was sent in, so if it has an id it is a real instance now if(ctx.getId() != null) { // it doesn't need to be read, but it might have been changed from the db already. ctx.setValid(true); if(tx != null) { BeanLock lock = container.getLockManager().getLock(ctx.getCacheKey()); try { lock.schedule(mi); register(ctx, tx); // Set tx lock.endInvocation(mi); } finally { container.getLockManager().removeLockRef(lock.getId()); } } } return rtn; } public Object invoke(Invocation mi) throws Exception { // We are going to work with the context a lot EntityEnterpriseContext ctx = (EntityEnterpriseContext)mi.getEnterpriseContext(); // The Tx coming as part of the Method Invocation Transaction tx = mi.getTransaction(); if(log.isTraceEnabled()) log.trace("invoke called for ctx " + ctx + ", tx=" + tx); if(!ctx.isValid()) { container.getPersistenceManager().loadEntity(ctx); ctx.setValid(true); } // mark the context as read only if this is a readonly method and the context // was not already readonly boolean didSetReadOnly = false; if(!ctx.isReadOnly() && (container.isReadOnly() || container.getBeanMetaData().isMethodReadOnly(mi.getMethod()))) { ctx.setReadOnly(true); didSetReadOnly = true; } // So we can go on with the invocation // Invocation with a running Transaction try { if(tx != null && tx.getStatus() != Status.STATUS_NO_TRANSACTION) { // readonly does not synchronize, lock or belong with transaction. boolean isReadOnly = container.isReadOnly(); if(isReadOnly == false) { Method method = mi.getMethod(); if(method != null) isReadOnly = container.getBeanMetaData().isMethodReadOnly(method.getName()); } try { if(isReadOnly == false) { // register the wrapper with the transaction monitor (but only // register once). The transaction demarcation will trigger the // storage operations register(ctx, tx); } //Invoke down the chain Object retVal = getNext().invoke(mi); // Register again as a finder in the middle of a method // will de-register this entity, and then the rest of the method can // change fields which will never be stored if(isReadOnly == false) { // register the wrapper with the transaction monitor (but only // register once). The transaction demarcation will trigger the // storage operations register(ctx, tx); } // return the return value return retVal; } finally { // We were read-only and the context wasn't already synchronized, tidyup the cache if(isReadOnly && ctx.hasTxSynchronization() == false) { switch(commitOption) { // Keep instance active, but invalidate state case ConfigurationMetaData.B_COMMIT_OPTION: // Invalidate state (there might be other points of entry) ctx.setValid(false); break; // Invalidate everything AND Passivate instance case ConfigurationMetaData.C_COMMIT_OPTION: try { // FIXME: We cannot passivate here, because previous // interceptors work with the context, in particular // the re-entrance interceptor is doing lock counting // Just remove it from the cache if(ctx.getId() != null) container.getInstanceCache().remove(ctx.getId()); } catch(Exception e) { log.debug("Exception releasing context", e); } break; } } } } else { // No tx try { Object result = getNext().invoke(mi); // Store after each invocation -- not on exception though, or removal // And skip reads too ("get" methods) if(ctx.getId() != null && !container.isReadOnly()) { container.invokeEjbStore(ctx); container.storeEntity(ctx); } return result; } catch(Exception e) { // Exception - force reload on next call ctx.setValid(false); throw e; } finally { switch(commitOption) { // Keep instance active, but invalidate state case ConfigurationMetaData.B_COMMIT_OPTION: // Invalidate state (there might be other points of entry) ctx.setValid(false); break; // Invalidate everything AND Passivate instance case ConfigurationMetaData.C_COMMIT_OPTION: try { // Do not call release if getId() is null. This means that // the entity has been removed from cache. // release will schedule a passivation and this removed ctx // could be put back into the cache! // This is necessary because we have no lock, we // don't want to return an instance to the pool that is // being used if(ctx.getId() != null) container.getInstanceCache().remove(ctx.getId()); } catch(Exception e) { log.debug("Exception releasing context", e); } break; } } } } finally { // if we marked the context as read only we need to reset it if(didSetReadOnly) { ctx.setReadOnly(false); } } } protected class InstanceSynchronization implements Synchronization { /** * The transaction we follow. */ protected Transaction tx; /** * The context we manage. */ protected EntityEnterpriseContext ctx; /** * The context lock */ protected BeanLock lock; /** * Create a new instance synchronization instance. */ InstanceSynchronization(Transaction tx, EntityEnterpriseContext ctx) { this.tx = tx; this.ctx = ctx; this.lock = container.getLockManager().getLock(ctx.getCacheKey()); } public void beforeCompletion() { //synchronization is handled by GlobalTxEntityMap. } public void afterCompletion(int status) { boolean trace = log.isTraceEnabled(); // This is an independent point of entry. We need to make sure the // thread is associated with the right context class loader ClassLoader oldCl = SecurityActions.getContextClassLoader(); boolean setCl = !oldCl.equals(container.getClassLoader()); if(setCl) { SecurityActions.setContextClassLoader(container.getClassLoader()); } container.pushENC(); int commitOption = ctx.isPassivateAfterCommit() ? ConfigurationMetaData.C_COMMIT_OPTION : EntitySynchronizationInterceptor.this.commitOption; lock.sync(); // The context is no longer synchronized on the TX ctx.hasTxSynchronization(false); ctx.setTxAssociation(GlobalTxEntityMap.NONE); ctx.setTransaction(null); try { try { // If rolled back -> invalidate instance if(status == Status.STATUS_ROLLEDBACK) { // remove from the cache container.getInstanceCache().remove(ctx.getCacheKey()); } else { switch(commitOption) { // Keep instance cached after tx commit case ConfigurationMetaData.A_COMMIT_OPTION: case ConfigurationMetaData.D_COMMIT_OPTION: // The state is still valid (only point of access is us) ctx.setValid(true); break; // Keep instance active, but invalidate state case ConfigurationMetaData.B_COMMIT_OPTION: // Invalidate state (there might be other points of entry) ctx.setValid(false); break; // Invalidate everything AND Passivate instance case ConfigurationMetaData.C_COMMIT_OPTION: try { // We weren't removed, passivate // Here we own the lock, so we don't try to passivate // we just passivate if(ctx.getId() != null) { container.getInstanceCache().remove(ctx.getId()); container.getPersistenceManager().passivateEntity(ctx); } // If we get this far, we return to the pool container.getInstancePool().free(ctx); } catch(Exception e) { log.debug("Exception releasing context", e); } break; } } } finally { if(trace) log.trace("afterCompletion, clear tx for ctx=" + ctx + ", tx=" + tx); lock.endTransaction(tx); if(trace) log.trace("afterCompletion, sent notify on TxLock for ctx=" + ctx); } } // synchronized(lock) finally { lock.releaseSync(); container.getLockManager().removeLockRef(lock.getId()); container.popENC(); if(setCl) { SecurityActions.setContextClassLoader(oldCl); } } } } /** * Flushes the cache according to the optiond refresh rate. */ class ValidContextsRefresher extends TimerTask { public ValidContextsRefresher() { } public void run() { // Guard against NPE at shutdown if (container == null) { cancel(); return; } if(log.isTraceEnabled()) log.trace("Flushing the valid contexts " + container.getBeanMetaData().getEjbName()); EntityCache cache = (EntityCache) container.getInstanceCache(); try { if(cache != null) cache.flush(); } catch (Throwable t) { log.debug("Ignored error while trying to flush() entity cache", t); } } } }