1 /*
2 * JBoss, Home of Professional Open Source
3 * Copyright 2005, JBoss Inc., and individual contributors as indicated
4 * by the @authors tag. See the copyright.txt in the distribution for a
5 * full listing of individual contributors.
6 *
7 * This is free software; you can redistribute it and/or modify it
8 * under the terms of the GNU Lesser General Public License as
9 * published by the Free Software Foundation; either version 2.1 of
10 * the License, or (at your option) any later version.
11 *
12 * This software is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 * Lesser General Public License for more details.
16 *
17 * You should have received a copy of the GNU Lesser General Public
18 * License along with this software; if not, write to the Free
19 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
20 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
21 */
22 package org.jboss.ejb.plugins;
23
24 import java.lang.reflect.Method;
25 import java.util.TimerTask;
26
27 import javax.ejb.EJBException;
28 import javax.transaction.RollbackException;
29 import javax.transaction.Status;
30 import javax.transaction.Synchronization;
31 import javax.transaction.Transaction;
32
33 import org.jboss.ejb.BeanLock;
34 import org.jboss.ejb.Container;
35 import org.jboss.ejb.EntityCache;
36 import org.jboss.ejb.EntityContainer;
37 import org.jboss.ejb.EntityEnterpriseContext;
38 import org.jboss.ejb.GlobalTxEntityMap;
39 import org.jboss.invocation.Invocation;
40 import org.jboss.metadata.ConfigurationMetaData;
41 import org.jboss.util.NestedRuntimeException;
42
43 /**
44 * The role of this interceptor is to synchronize the state of the cache with
45 * the underlying storage. It does this with the ejbLoad and ejbStore
46 * semantics of the EJB specification. In the presence of a transaction this
47 * is triggered by transaction demarcation. It registers a callback with the
48 * underlying transaction monitor through the JTA interfaces. If there is no
49 * transaction the policy is to store state upon returning from invocation.
50 * The synchronization polices A,B,C of the specification are taken care of
51 * here.
52 *
53 * <p><b>WARNING: critical code</b>, get approval from senior developers
54 * before changing.
55 *
56 * @author <a href="mailto:marc.fleury@jboss.org">Marc Fleury</a>
57 * @author <a href="mailto:Scott.Stark@jboss.org">Scott Stark</a>
58 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
59 * @version $Revision: 59204 $
60 */
61 public class EntitySynchronizationInterceptor extends AbstractInterceptor
62 {
63 /** Task for refreshing contexts */
64 private ValidContextsRefresher vcr;
65
66 /**
67 * The current commit option.
68 */
69 protected int commitOption;
70
71 /**
72 * The refresh rate for commit option d
73 */
74 protected long optionDRefreshRate;
75
76 /**
77 * The container of this interceptor.
78 */
79 protected EntityContainer container;
80
81 public Container getContainer()
82 {
83 return container;
84 }
85
86 public void setContainer(Container container)
87 {
88 this.container = (EntityContainer) container;
89 }
90
91 public void create()
92 throws Exception
93 {
94
95 try
96 {
97 ConfigurationMetaData configuration = container.getBeanMetaData().getContainerConfiguration();
98 commitOption = configuration.getCommitOption();
99 optionDRefreshRate = configuration.getOptionDRefreshRate();
100 }
101 catch(Exception e)
102 {
103 log.warn(e.getMessage());
104 }
105 }
106
107 public void start()
108 {
109 try
110 {
111 //start up the validContexts thread if commit option D
112 if (commitOption == ConfigurationMetaData.D_COMMIT_OPTION)
113 {
114 vcr = new ValidContextsRefresher();
115 LRUEnterpriseContextCachePolicy.tasksTimer.schedule(vcr, optionDRefreshRate, optionDRefreshRate);
116 log.debug("Scheduled a cache flush every " + optionDRefreshRate/1000 + " seconds");
117 }
118 }
119 catch(Exception e)
120 {
121 vcr = null;
122 log.warn("problem scheduling valid contexts refresher", e);
123 }
124 }
125
126 public void stop()
127 {
128 if (vcr != null)
129 {
130 TimerTask temp = vcr;
131 vcr = null;
132 temp.cancel();
133 }
134 }
135
136 protected Synchronization createSynchronization(Transaction tx, EntityEnterpriseContext ctx)
137 {
138 return new InstanceSynchronization(tx, ctx);
139 }
140
141 /**
142 * Register a transaction synchronization callback with a context.
143 */
144 protected void register(EntityEnterpriseContext ctx, Transaction tx)
145 {
146 boolean trace = log.isTraceEnabled();
147 if(trace)
148 log.trace("register, ctx=" + ctx + ", tx=" + tx);
149
150 EntityContainer ctxContainer = null;
151 try
152 {
153 ctxContainer = (EntityContainer)ctx.getContainer();
154 if(!ctx.hasTxSynchronization())
155 {
156 // Create a new synchronization
157 Synchronization synch = createSynchronization(tx, ctx);
158
159 // We want to be notified when the transaction commits
160 tx.registerSynchronization(synch);
161
162 ctx.hasTxSynchronization(true);
163 }
164 //mark it dirty in global tx entity map if it is not read only
165 if(!ctxContainer.isReadOnly())
166 {
167 ctx.getTxAssociation().scheduleSync(tx, ctx);
168 }
169 }
170 catch(RollbackException e)
171 {
172 // The state in the instance is to be discarded, we force a reload of state
173 synchronized(ctx)
174 {
175 ctx.setValid(false);
176 ctx.hasTxSynchronization(false);
177 ctx.setTransaction(null);
178 ctx.setTxAssociation(GlobalTxEntityMap.NONE);
179 }
180 throw new EJBException(e);
181 }
182 catch(Throwable t)
183 {
184 // If anything goes wrong with the association remove the ctx-tx association
185 ctx.hasTxSynchronization(false);
186 ctx.setTxAssociation(GlobalTxEntityMap.NONE);
187 if(t instanceof RuntimeException)
188 throw (RuntimeException)t;
189 else if(t instanceof Error)
190 throw (Error)t;
191 else if(t instanceof Exception)
192 throw new EJBException((Exception)t);
193 else
194 throw new NestedRuntimeException(t);
195 }
196 }
197
198 public Object invokeHome(Invocation mi) throws Exception
199 {
200 EntityEnterpriseContext ctx = (EntityEnterpriseContext)mi.getEnterpriseContext();
201 Transaction tx = mi.getTransaction();
202
203 Object rtn = getNext().invokeHome(mi);
204
205 // An anonymous context was sent in, so if it has an id it is a real instance now
206 if(ctx.getId() != null)
207 {
208
209 // it doesn't need to be read, but it might have been changed from the db already.
210 ctx.setValid(true);
211
212 if(tx != null)
213 {
214 BeanLock lock = container.getLockManager().getLock(ctx.getCacheKey());
215 try
216 {
217 lock.schedule(mi);
218 register(ctx, tx); // Set tx
219 lock.endInvocation(mi);
220 }
221 finally
222 {
223 container.getLockManager().removeLockRef(lock.getId());
224 }
225 }
226 }
227 return rtn;
228 }
229
230 public Object invoke(Invocation mi) throws Exception
231 {
232 // We are going to work with the context a lot
233 EntityEnterpriseContext ctx = (EntityEnterpriseContext)mi.getEnterpriseContext();
234
235 // The Tx coming as part of the Method Invocation
236 Transaction tx = mi.getTransaction();
237
238 if(log.isTraceEnabled())
239 log.trace("invoke called for ctx " + ctx + ", tx=" + tx);
240
241 if(!ctx.isValid())
242 {
243 container.getPersistenceManager().loadEntity(ctx);
244 ctx.setValid(true);
245 }
246
247 // mark the context as read only if this is a readonly method and the context
248 // was not already readonly
249 boolean didSetReadOnly = false;
250 if(!ctx.isReadOnly() &&
251 (container.isReadOnly() ||
252 container.getBeanMetaData().isMethodReadOnly(mi.getMethod())))
253 {
254 ctx.setReadOnly(true);
255 didSetReadOnly = true;
256 }
257
258 // So we can go on with the invocation
259
260 // Invocation with a running Transaction
261 try
262 {
263 if(tx != null && tx.getStatus() != Status.STATUS_NO_TRANSACTION)
264 {
265 // readonly does not synchronize, lock or belong with transaction.
266 boolean isReadOnly = container.isReadOnly();
267 if(isReadOnly == false)
268 {
269 Method method = mi.getMethod();
270 if(method != null)
271 isReadOnly = container.getBeanMetaData().isMethodReadOnly(method.getName());
272 }
273 try
274 {
275 if(isReadOnly == false)
276 {
277 // register the wrapper with the transaction monitor (but only
278 // register once). The transaction demarcation will trigger the
279 // storage operations
280 register(ctx, tx);
281 }
282
283 //Invoke down the chain
284 Object retVal = getNext().invoke(mi);
285
286 // Register again as a finder in the middle of a method
287 // will de-register this entity, and then the rest of the method can
288 // change fields which will never be stored
289 if(isReadOnly == false)
290 {
291 // register the wrapper with the transaction monitor (but only
292 // register once). The transaction demarcation will trigger the
293 // storage operations
294 register(ctx, tx);
295 }
296
297 // return the return value
298 return retVal;
299 }
300 finally
301 {
302 // We were read-only and the context wasn't already synchronized, tidyup the cache
303 if(isReadOnly && ctx.hasTxSynchronization() == false)
304 {
305 switch(commitOption)
306 {
307 // Keep instance active, but invalidate state
308 case ConfigurationMetaData.B_COMMIT_OPTION:
309 // Invalidate state (there might be other points of entry)
310 ctx.setValid(false);
311 break;
312
313 // Invalidate everything AND Passivate instance
314 case ConfigurationMetaData.C_COMMIT_OPTION:
315 try
316 {
317 // FIXME: We cannot passivate here, because previous
318 // interceptors work with the context, in particular
319 // the re-entrance interceptor is doing lock counting
320 // Just remove it from the cache
321 if(ctx.getId() != null)
322 container.getInstanceCache().remove(ctx.getId());
323 }
324 catch(Exception e)
325 {
326 log.debug("Exception releasing context", e);
327 }
328 break;
329 }
330 }
331 }
332 }
333 else
334 {
335 // No tx
336 try
337 {
338 Object result = getNext().invoke(mi);
339
340 // Store after each invocation -- not on exception though, or removal
341 // And skip reads too ("get" methods)
342 if(ctx.getId() != null && !container.isReadOnly())
343 {
344 container.invokeEjbStore(ctx);
345 container.storeEntity(ctx);
346 }
347
348 return result;
349 }
350 catch(Exception e)
351 {
352 // Exception - force reload on next call
353 ctx.setValid(false);
354 throw e;
355 }
356 finally
357 {
358 switch(commitOption)
359 {
360 // Keep instance active, but invalidate state
361 case ConfigurationMetaData.B_COMMIT_OPTION:
362 // Invalidate state (there might be other points of entry)
363 ctx.setValid(false);
364 break;
365
366 // Invalidate everything AND Passivate instance
367 case ConfigurationMetaData.C_COMMIT_OPTION:
368 try
369 {
370 // Do not call release if getId() is null. This means that
371 // the entity has been removed from cache.
372 // release will schedule a passivation and this removed ctx
373 // could be put back into the cache!
374 // This is necessary because we have no lock, we
375 // don't want to return an instance to the pool that is
376 // being used
377 if(ctx.getId() != null)
378 container.getInstanceCache().remove(ctx.getId());
379 }
380 catch(Exception e)
381 {
382 log.debug("Exception releasing context", e);
383 }
384 break;
385 }
386 }
387 }
388 }
389 finally
390 {
391 // if we marked the context as read only we need to reset it
392 if(didSetReadOnly)
393 {
394 ctx.setReadOnly(false);
395 }
396 }
397 }
398
399 protected class InstanceSynchronization
400 implements Synchronization
401 {
402 /**
403 * The transaction we follow.
404 */
405 protected Transaction tx;
406
407 /**
408 * The context we manage.
409 */
410 protected EntityEnterpriseContext ctx;
411
412 /**
413 * The context lock
414 */
415 protected BeanLock lock;
416
417 /**
418 * Create a new instance synchronization instance.
419 */
420 InstanceSynchronization(Transaction tx, EntityEnterpriseContext ctx)
421 {
422 this.tx = tx;
423 this.ctx = ctx;
424 this.lock = container.getLockManager().getLock(ctx.getCacheKey());
425 }
426
427 public void beforeCompletion()
428 {
429 //synchronization is handled by GlobalTxEntityMap.
430 }
431
432 public void afterCompletion(int status)
433 {
434 boolean trace = log.isTraceEnabled();
435
436 // This is an independent point of entry. We need to make sure the
437 // thread is associated with the right context class loader
438 ClassLoader oldCl = SecurityActions.getContextClassLoader();
439 boolean setCl = !oldCl.equals(container.getClassLoader());
440 if(setCl)
441 {
442 SecurityActions.setContextClassLoader(container.getClassLoader());
443 }
444 container.pushENC();
445
446 int commitOption = ctx.isPassivateAfterCommit() ?
447 ConfigurationMetaData.C_COMMIT_OPTION : EntitySynchronizationInterceptor.this.commitOption;
448
449 lock.sync();
450 // The context is no longer synchronized on the TX
451 ctx.hasTxSynchronization(false);
452 ctx.setTxAssociation(GlobalTxEntityMap.NONE);
453 ctx.setTransaction(null);
454 try
455 {
456 try
457 {
458 // If rolled back -> invalidate instance
459 if(status == Status.STATUS_ROLLEDBACK)
460 {
461 // remove from the cache
462 container.getInstanceCache().remove(ctx.getCacheKey());
463 }
464 else
465 {
466 switch(commitOption)
467 {
468 // Keep instance cached after tx commit
469 case ConfigurationMetaData.A_COMMIT_OPTION:
470 case ConfigurationMetaData.D_COMMIT_OPTION:
471 // The state is still valid (only point of access is us)
472 ctx.setValid(true);
473 break;
474
475 // Keep instance active, but invalidate state
476 case ConfigurationMetaData.B_COMMIT_OPTION:
477 // Invalidate state (there might be other points of entry)
478 ctx.setValid(false);
479 break;
480 // Invalidate everything AND Passivate instance
481 case ConfigurationMetaData.C_COMMIT_OPTION:
482 try
483 {
484 // We weren't removed, passivate
485 // Here we own the lock, so we don't try to passivate
486 // we just passivate
487 if(ctx.getId() != null)
488 {
489 container.getInstanceCache().remove(ctx.getId());
490 container.getPersistenceManager().passivateEntity(ctx);
491 }
492 // If we get this far, we return to the pool
493 container.getInstancePool().free(ctx);
494 }
495 catch(Exception e)
496 {
497 log.debug("Exception releasing context", e);
498 }
499 break;
500 }
501 }
502 }
503 finally
504 {
505 if(trace)
506 log.trace("afterCompletion, clear tx for ctx=" + ctx + ", tx=" + tx);
507 lock.endTransaction(tx);
508
509 if(trace)
510 log.trace("afterCompletion, sent notify on TxLock for ctx=" + ctx);
511 }
512 } // synchronized(lock)
513 finally
514 {
515 lock.releaseSync();
516 container.getLockManager().removeLockRef(lock.getId());
517 container.popENC();
518 if(setCl)
519 {
520 SecurityActions.setContextClassLoader(oldCl);
521 }
522 }
523 }
524
525 }
526
527 /**
528 * Flushes the cache according to the optiond refresh rate.
529 */
530 class ValidContextsRefresher extends TimerTask
531 {
532 public ValidContextsRefresher()
533 {
534 }
535
536 public void run()
537 {
538 // Guard against NPE at shutdown
539 if (container == null)
540 {
541 cancel();
542 return;
543 }
544
545 if(log.isTraceEnabled())
546 log.trace("Flushing the valid contexts " + container.getBeanMetaData().getEjbName());
547
548 EntityCache cache = (EntityCache) container.getInstanceCache();
549 try
550 {
551 if(cache != null)
552 cache.flush();
553 }
554 catch (Throwable t)
555 {
556 log.debug("Ignored error while trying to flush() entity cache", t);
557 }
558 }
559 }
560 }