001: /*
002: * JBoss, Home of Professional Open Source.
003: * Copyright 2006, Red Hat Middleware LLC, and individual contributors
004: * as indicated by the @author tags. See the copyright.txt file in the
005: * distribution for a full listing of individual contributors.
006: *
007: * This is free software; you can redistribute it and/or modify it
008: * under the terms of the GNU Lesser General Public License as
009: * published by the Free Software Foundation; either version 2.1 of
010: * the License, or (at your option) any later version.
011: *
012: * This software is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: *
017: * You should have received a copy of the GNU Lesser General Public
018: * License along with this software; if not, write to the Free
019: * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020: * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021: */
022: package org.jboss.ejb.plugins;
023:
024: import java.lang.reflect.Method;
025: import java.util.TimerTask;
026:
027: import javax.ejb.EJBException;
028: import javax.transaction.RollbackException;
029: import javax.transaction.Status;
030: import javax.transaction.Synchronization;
031: import javax.transaction.Transaction;
032:
033: import org.jboss.ejb.BeanLock;
034: import org.jboss.ejb.Container;
035: import org.jboss.ejb.EntityCache;
036: import org.jboss.ejb.EntityContainer;
037: import org.jboss.ejb.EntityEnterpriseContext;
038: import org.jboss.ejb.GlobalTxEntityMap;
039: import org.jboss.invocation.Invocation;
040: import org.jboss.metadata.ConfigurationMetaData;
041: import org.jboss.util.NestedRuntimeException;
042:
043: /**
044: * The role of this interceptor is to synchronize the state of the cache with
045: * the underlying storage. It does this with the ejbLoad and ejbStore
046: * semantics of the EJB specification. In the presence of a transaction this
047: * is triggered by transaction demarcation. It registers a callback with the
048: * underlying transaction monitor through the JTA interfaces. If there is no
049: * transaction the policy is to store state upon returning from invocation.
050: * The synchronization polices A,B,C of the specification are taken care of
051: * here.
052: *
053: * <p><b>WARNING: critical code</b>, get approval from senior developers
054: * before changing.
055: *
056: * @author <a href="mailto:marc.fleury@jboss.org">Marc Fleury</a>
057: * @author <a href="mailto:Scott.Stark@jboss.org">Scott Stark</a>
058: * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
059: * @version $Revision: 57209 $
060: */
061: public class EntitySynchronizationInterceptor extends
062: AbstractInterceptor {
063: /** Task for refreshing contexts */
064: private ValidContextsRefresher vcr;
065:
066: /**
067: * The current commit option.
068: */
069: protected int commitOption;
070:
071: /**
072: * The refresh rate for commit option d
073: */
074: protected long optionDRefreshRate;
075:
076: /**
077: * The container of this interceptor.
078: */
079: protected EntityContainer container;
080:
081: public Container getContainer() {
082: return container;
083: }
084:
085: public void setContainer(Container container) {
086: this .container = (EntityContainer) container;
087: }
088:
089: public void create() throws Exception {
090:
091: try {
092: ConfigurationMetaData configuration = container
093: .getBeanMetaData().getContainerConfiguration();
094: commitOption = configuration.getCommitOption();
095: optionDRefreshRate = configuration.getOptionDRefreshRate();
096: } catch (Exception e) {
097: log.warn(e.getMessage());
098: }
099: }
100:
101: public void start() {
102: try {
103: //start up the validContexts thread if commit option D
104: if (commitOption == ConfigurationMetaData.D_COMMIT_OPTION) {
105: vcr = new ValidContextsRefresher();
106: LRUEnterpriseContextCachePolicy.tasksTimer.schedule(
107: vcr, optionDRefreshRate, optionDRefreshRate);
108: log.debug("Scheduled a cache flush every "
109: + optionDRefreshRate / 1000 + " seconds");
110: }
111: } catch (Exception e) {
112: vcr = null;
113: log.warn("problem scheduling valid contexts refresher", e);
114: }
115: }
116:
117: public void stop() {
118: if (vcr != null) {
119: TimerTask temp = vcr;
120: vcr = null;
121: temp.cancel();
122: }
123: }
124:
125: protected Synchronization createSynchronization(Transaction tx,
126: EntityEnterpriseContext ctx) {
127: return new InstanceSynchronization(tx, ctx);
128: }
129:
130: /**
131: * Register a transaction synchronization callback with a context.
132: */
133: protected void register(EntityEnterpriseContext ctx, Transaction tx) {
134: boolean trace = log.isTraceEnabled();
135: if (trace)
136: log.trace("register, ctx=" + ctx + ", tx=" + tx);
137:
138: EntityContainer ctxContainer = null;
139: try {
140: ctxContainer = (EntityContainer) ctx.getContainer();
141: if (!ctx.hasTxSynchronization()) {
142: // Create a new synchronization
143: Synchronization synch = createSynchronization(tx, ctx);
144:
145: // We want to be notified when the transaction commits
146: tx.registerSynchronization(synch);
147:
148: ctx.hasTxSynchronization(true);
149: }
150: //mark it dirty in global tx entity map if it is not read only
151: if (!ctxContainer.isReadOnly()) {
152: ctx.getTxAssociation().scheduleSync(tx, ctx);
153: }
154: } catch (RollbackException e) {
155: // The state in the instance is to be discarded, we force a reload of state
156: synchronized (ctx) {
157: ctx.setValid(false);
158: ctx.hasTxSynchronization(false);
159: ctx.setTransaction(null);
160: ctx.setTxAssociation(GlobalTxEntityMap.NONE);
161: }
162: throw new EJBException(e);
163: } catch (Throwable t) {
164: // If anything goes wrong with the association remove the ctx-tx association
165: ctx.hasTxSynchronization(false);
166: ctx.setTxAssociation(GlobalTxEntityMap.NONE);
167: if (t instanceof RuntimeException)
168: throw (RuntimeException) t;
169: else if (t instanceof Error)
170: throw (Error) t;
171: else if (t instanceof Exception)
172: throw new EJBException((Exception) t);
173: else
174: throw new NestedRuntimeException(t);
175: }
176: }
177:
178: public Object invokeHome(Invocation mi) throws Exception {
179: EntityEnterpriseContext ctx = (EntityEnterpriseContext) mi
180: .getEnterpriseContext();
181: Transaction tx = mi.getTransaction();
182:
183: Object rtn = getNext().invokeHome(mi);
184:
185: // An anonymous context was sent in, so if it has an id it is a real instance now
186: if (ctx.getId() != null) {
187:
188: // it doesn't need to be read, but it might have been changed from the db already.
189: ctx.setValid(true);
190:
191: if (tx != null) {
192: BeanLock lock = container.getLockManager().getLock(
193: ctx.getCacheKey());
194: try {
195: lock.schedule(mi);
196: register(ctx, tx); // Set tx
197: lock.endInvocation(mi);
198: } finally {
199: container.getLockManager().removeLockRef(
200: lock.getId());
201: }
202: }
203: }
204: return rtn;
205: }
206:
207: public Object invoke(Invocation mi) throws Exception {
208: // We are going to work with the context a lot
209: EntityEnterpriseContext ctx = (EntityEnterpriseContext) mi
210: .getEnterpriseContext();
211:
212: // The Tx coming as part of the Method Invocation
213: Transaction tx = mi.getTransaction();
214:
215: if (log.isTraceEnabled())
216: log.trace("invoke called for ctx " + ctx + ", tx=" + tx);
217:
218: if (!ctx.isValid()) {
219: container.getPersistenceManager().loadEntity(ctx);
220: ctx.setValid(true);
221: }
222:
223: // mark the context as read only if this is a readonly method and the context
224: // was not already readonly
225: boolean didSetReadOnly = false;
226: if (!ctx.isReadOnly()
227: && (container.isReadOnly() || container
228: .getBeanMetaData().isMethodReadOnly(
229: mi.getMethod()))) {
230: ctx.setReadOnly(true);
231: didSetReadOnly = true;
232: }
233:
234: // So we can go on with the invocation
235:
236: // Invocation with a running Transaction
237: try {
238: if (tx != null
239: && tx.getStatus() != Status.STATUS_NO_TRANSACTION) {
240: // readonly does not synchronize, lock or belong with transaction.
241: boolean isReadOnly = container.isReadOnly();
242: if (isReadOnly == false) {
243: Method method = mi.getMethod();
244: if (method != null)
245: isReadOnly = container.getBeanMetaData()
246: .isMethodReadOnly(method.getName());
247: }
248: try {
249: if (isReadOnly == false) {
250: // register the wrapper with the transaction monitor (but only
251: // register once). The transaction demarcation will trigger the
252: // storage operations
253: register(ctx, tx);
254: }
255:
256: //Invoke down the chain
257: Object retVal = getNext().invoke(mi);
258:
259: // Register again as a finder in the middle of a method
260: // will de-register this entity, and then the rest of the method can
261: // change fields which will never be stored
262: if (isReadOnly == false) {
263: // register the wrapper with the transaction monitor (but only
264: // register once). The transaction demarcation will trigger the
265: // storage operations
266: register(ctx, tx);
267: }
268:
269: // return the return value
270: return retVal;
271: } finally {
272: // We were read-only and the context wasn't already synchronized, tidyup the cache
273: if (isReadOnly
274: && ctx.hasTxSynchronization() == false) {
275: switch (commitOption) {
276: // Keep instance active, but invalidate state
277: case ConfigurationMetaData.B_COMMIT_OPTION:
278: // Invalidate state (there might be other points of entry)
279: ctx.setValid(false);
280: break;
281:
282: // Invalidate everything AND Passivate instance
283: case ConfigurationMetaData.C_COMMIT_OPTION:
284: try {
285: // FIXME: We cannot passivate here, because previous
286: // interceptors work with the context, in particular
287: // the re-entrance interceptor is doing lock counting
288: // Just remove it from the cache
289: if (ctx.getId() != null)
290: container.getInstanceCache()
291: .remove(ctx.getId());
292: } catch (Exception e) {
293: log.debug(
294: "Exception releasing context",
295: e);
296: }
297: break;
298: }
299: }
300: }
301: } else {
302: // No tx
303: try {
304: Object result = getNext().invoke(mi);
305:
306: // Store after each invocation -- not on exception though, or removal
307: // And skip reads too ("get" methods)
308: if (ctx.getId() != null && !container.isReadOnly()) {
309: container.invokeEjbStore(ctx);
310: container.storeEntity(ctx);
311: }
312:
313: return result;
314: } catch (Exception e) {
315: // Exception - force reload on next call
316: ctx.setValid(false);
317: throw e;
318: } finally {
319: switch (commitOption) {
320: // Keep instance active, but invalidate state
321: case ConfigurationMetaData.B_COMMIT_OPTION:
322: // Invalidate state (there might be other points of entry)
323: ctx.setValid(false);
324: break;
325:
326: // Invalidate everything AND Passivate instance
327: case ConfigurationMetaData.C_COMMIT_OPTION:
328: try {
329: // Do not call release if getId() is null. This means that
330: // the entity has been removed from cache.
331: // release will schedule a passivation and this removed ctx
332: // could be put back into the cache!
333: // This is necessary because we have no lock, we
334: // don't want to return an instance to the pool that is
335: // being used
336: if (ctx.getId() != null)
337: container.getInstanceCache().remove(
338: ctx.getId());
339: } catch (Exception e) {
340: log.debug("Exception releasing context", e);
341: }
342: break;
343: }
344: }
345: }
346: } finally {
347: // if we marked the context as read only we need to reset it
348: if (didSetReadOnly) {
349: ctx.setReadOnly(false);
350: }
351: }
352: }
353:
354: protected class InstanceSynchronization implements Synchronization {
355: /**
356: * The transaction we follow.
357: */
358: protected Transaction tx;
359:
360: /**
361: * The context we manage.
362: */
363: protected EntityEnterpriseContext ctx;
364:
365: /**
366: * The context lock
367: */
368: protected BeanLock lock;
369:
370: /**
371: * Create a new instance synchronization instance.
372: */
373: InstanceSynchronization(Transaction tx,
374: EntityEnterpriseContext ctx) {
375: this .tx = tx;
376: this .ctx = ctx;
377: this .lock = container.getLockManager().getLock(
378: ctx.getCacheKey());
379: }
380:
381: public void beforeCompletion() {
382: //synchronization is handled by GlobalTxEntityMap.
383: }
384:
385: public void afterCompletion(int status) {
386: boolean trace = log.isTraceEnabled();
387:
388: // This is an independent point of entry. We need to make sure the
389: // thread is associated with the right context class loader
390: ClassLoader oldCl = SecurityActions.getContextClassLoader();
391: boolean setCl = !oldCl.equals(container.getClassLoader());
392: if (setCl) {
393: SecurityActions.setContextClassLoader(container
394: .getClassLoader());
395: }
396:
397: int commitOption = ctx.isPassivateAfterCommit() ? ConfigurationMetaData.C_COMMIT_OPTION
398: : EntitySynchronizationInterceptor.this .commitOption;
399:
400: lock.sync();
401: // The context is no longer synchronized on the TX
402: ctx.hasTxSynchronization(false);
403: ctx.setTxAssociation(GlobalTxEntityMap.NONE);
404: ctx.setTransaction(null);
405: try {
406: try {
407: // If rolled back -> invalidate instance
408: if (status == Status.STATUS_ROLLEDBACK) {
409: // remove from the cache
410: container.getInstanceCache().remove(
411: ctx.getCacheKey());
412: } else {
413: switch (commitOption) {
414: // Keep instance cached after tx commit
415: case ConfigurationMetaData.A_COMMIT_OPTION:
416: case ConfigurationMetaData.D_COMMIT_OPTION:
417: // The state is still valid (only point of access is us)
418: ctx.setValid(true);
419: break;
420:
421: // Keep instance active, but invalidate state
422: case ConfigurationMetaData.B_COMMIT_OPTION:
423: // Invalidate state (there might be other points of entry)
424: ctx.setValid(false);
425: break;
426: // Invalidate everything AND Passivate instance
427: case ConfigurationMetaData.C_COMMIT_OPTION:
428: try {
429: // We weren't removed, passivate
430: // Here we own the lock, so we don't try to passivate
431: // we just passivate
432: if (ctx.getId() != null) {
433: container.getInstanceCache()
434: .remove(ctx.getId());
435: container.getPersistenceManager()
436: .passivateEntity(ctx);
437: }
438: // If we get this far, we return to the pool
439: container.getInstancePool().free(ctx);
440: } catch (Exception e) {
441: log.debug(
442: "Exception releasing context",
443: e);
444: }
445: break;
446: }
447: }
448: } finally {
449: if (trace)
450: log.trace("afterCompletion, clear tx for ctx="
451: + ctx + ", tx=" + tx);
452: lock.endTransaction(tx);
453:
454: if (trace)
455: log
456: .trace("afterCompletion, sent notify on TxLock for ctx="
457: + ctx);
458: }
459: } // synchronized(lock)
460: finally {
461: lock.releaseSync();
462: container.getLockManager().removeLockRef(lock.getId());
463: if (setCl) {
464: SecurityActions.setContextClassLoader(oldCl);
465: }
466: }
467: }
468:
469: }
470:
471: /**
472: * Flushes the cache according to the optiond refresh rate.
473: */
474: class ValidContextsRefresher extends TimerTask {
475: public ValidContextsRefresher() {
476: }
477:
478: public void run() {
479: // Guard against NPE at shutdown
480: if (container == null) {
481: cancel();
482: return;
483: }
484:
485: if (log.isTraceEnabled())
486: log.trace("Flushing the valid contexts "
487: + container.getBeanMetaData().getEjbName());
488:
489: EntityCache cache = (EntityCache) container
490: .getInstanceCache();
491: try {
492: if (cache != null)
493: cache.flush();
494: } catch (Throwable t) {
495: log
496: .debug(
497: "Ignored error while trying to flush() entity cache",
498: t);
499: }
500: }
501: }
502: }
|