View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.krad.uif.lifecycle;
17  
18  import java.util.Deque;
19  import java.util.IdentityHashMap;
20  import java.util.LinkedList;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.Queue;
24  import java.util.concurrent.Callable;
25  import java.util.concurrent.ConcurrentLinkedQueue;
26  import java.util.concurrent.LinkedBlockingDeque;
27  import java.util.concurrent.ThreadFactory;
28  import java.util.concurrent.ThreadPoolExecutor;
29  import java.util.concurrent.TimeUnit;
30  
31  import org.apache.log4j.Logger;
32  import org.kuali.rice.core.api.config.property.ConfigContext;
33  import org.kuali.rice.core.api.exception.RiceRuntimeException;
34  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
35  import org.kuali.rice.krad.uif.freemarker.LifecycleRenderingContext;
36  import org.kuali.rice.krad.uif.service.ViewHelperService;
37  import org.kuali.rice.krad.uif.util.LifecycleElement;
38  import org.kuali.rice.krad.uif.util.ProcessLogger;
39  import org.kuali.rice.krad.uif.util.RecycleUtils;
40  import org.kuali.rice.krad.uif.view.DefaultExpressionEvaluator;
41  import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
42  import org.kuali.rice.krad.uif.view.ExpressionEvaluatorFactory;
43  import org.kuali.rice.krad.util.GlobalVariables;
44  import org.kuali.rice.krad.util.KRADConstants;
45  
46  /**
47   * Static utility class for handling executor configuration and spreading {@link ViewLifecycle}
48   * across multiple threads.
49   * 
50   * @author Kuali Rice Team (rice.collab@kuali.org)
51   */
52  public final class AsynchronousViewLifecycleProcessor extends ViewLifecycleProcessorBase {
53  
54      private static final Logger LOG = Logger.getLogger(AsynchronousViewLifecycleProcessor.class);
55  
56      private static final ThreadFactory LIFECYCLE_THREAD_FACTORY = new LifecycleThreadFactory();
57  
58      private static final ThreadPoolExecutor LIFECYCLE_EXECUTOR = new ThreadPoolExecutor(
59              getMinThreads(), getMaxThreads(), getTimeout(), TimeUnit.MILLISECONDS,
60              new LinkedBlockingDeque<Runnable>(), LIFECYCLE_THREAD_FACTORY);
61  
62      private static final Deque<AsynchronousLifecyclePhase> PENDING_PHASE_QUEUE =
63              new LinkedList<AsynchronousLifecyclePhase>();
64  
65      private static final ThreadLocal<AsynchronousLifecyclePhase> ACTIVE_PHASE =
66              new ThreadLocal<AsynchronousLifecyclePhase>();
67      
68      private static final Map<LifecycleElement, AsynchronousLifecyclePhase> BUSY_ELEMENTS =
69              new IdentityHashMap<LifecycleElement, AsynchronousLifecyclePhase>();
70  
71      private static Integer minThreads;
72      private static Integer maxThreads;
73      private static Long timeout;
74  
75      private final Queue<LifecycleRenderingContext> renderingContextPool =
76              ViewLifecycle.isRenderInLifecycle() ? new ConcurrentLinkedQueue<LifecycleRenderingContext>() : null;
77      private final Queue<ExpressionEvaluator> expressionEvaluatorPool =
78              new ConcurrentLinkedQueue<ExpressionEvaluator>();
79  
80      private Throwable error;
81  
82      /**
83       * Gets the minimum number of lifecycle worker threads to maintain.
84       * 
85       * <p>
86       * This value is controlled by the configuration parameter
87       * &quot;krad.uif.lifecycle.asynchronous.minThreads&quot;.
88       * </p>
89       * 
90       * @return minimum number of worker threads to maintain
91       */
92      public static int getMinThreads() {
93          if (minThreads == null) {
94              String propStr = ConfigContext.getCurrentContextConfig().getProperty(
95                      KRADConstants.ConfigParameters.KRAD_VIEW_LIFECYCLE_MINTHREADS);
96              minThreads = propStr == null ? 4 : Integer.parseInt(propStr);
97          }
98  
99          return minThreads;
100     }
101 
102     /**
103      * Gets the maximum number of lifecycle worker threads to maintain.
104      * 
105      * <p>
106      * This value is controlled by the configuration parameter
107      * &quot;krad.uif.lifecycle.asynchronous.maxThreads&quot;.
108      * </p>
109      * 
110      * @return maximum number of worker threads to maintain
111      */
112     public static int getMaxThreads() {
113         if (maxThreads == null) {
114             String propStr = ConfigContext.getCurrentContextConfig().getProperty(
115                     KRADConstants.ConfigParameters.KRAD_VIEW_LIFECYCLE_MAXTHREADS);
116             maxThreads = propStr == null ? 48 : Integer.parseInt(propStr);
117         }
118 
119         return maxThreads;
120     }
121 
122     /**
123      * Gets the time, in milliseconds, to wait for a initial phase to process.
124      * 
125      * <p>
126      * This value is controlled by the configuration parameter
127      * &quot;krad.uif.lifecycle.asynchronous.timeout&quot;.
128      * </p>
129      * 
130      * @return time in milliseconds to wait for the initial phase to process
131      */
132     public static long getTimeout() {
133         if (timeout == null) {
134             String propStr = ConfigContext.getCurrentContextConfig().getProperty(
135                     KRADConstants.ConfigParameters.KRAD_VIEW_LIFECYCLE_TIMEOUT);
136             timeout = propStr == null ? 30000 : Long.parseLong(propStr);
137         }
138 
139         return timeout;
140     }
141 
142     /**
143      * Constructor.
144      * 
145      * @param lifecycle The lifecycle to process.
146      */
147     AsynchronousViewLifecycleProcessor(ViewLifecycle lifecycle) {
148         super(lifecycle);
149     }
150 
151     /**
152      * Thread factory for lifecycle processing.
153      * 
154      * @author Kuali Rice Team (rice.collab@kuali.org)
155      */
156     private static class LifecycleThreadFactory implements ThreadFactory {
157 
158         private static final ThreadGroup GROUP = new ThreadGroup("krad-lifecycle-group");
159 
160         private int sequenceNumber = 0;
161 
162         @Override
163         public Thread newThread(Runnable r) {
164             return new Thread(GROUP, r, "krad-lifecycle("
165                     + Integer.toString(++sequenceNumber) + ")");
166         }
167     }
168 
169     /**
170      * {@inheritDoc}
171      */
172     @Override
173     public ViewLifecyclePhase getActivePhase() {
174         AsynchronousLifecyclePhase aphase = ACTIVE_PHASE.get();
175 
176         if (aphase == null) {
177             throw new IllegalStateException("No phase worker is active on this thread");
178         }
179 
180         ViewLifecyclePhase phase = aphase.phase;
181         if (phase == null) {
182             throw new IllegalStateException("No lifecycle phase is active on this thread");
183         }
184 
185         return phase;
186     }
187 
188     /**
189      * {@inheritDoc}
190      */
191     @Override
192     void setActivePhase(ViewLifecyclePhase phase) {
193         AsynchronousLifecyclePhase aphase = ACTIVE_PHASE.get();
194 
195         if (aphase == null) {
196             throw new IllegalStateException("No phase worker is active on this thread");
197         }
198         
199         if (phase == null) {
200             // Ignore null setting, asychronous state is controlled by aphase.
201             return;
202         }
203 
204         if (aphase.phase != phase) {
205             throw new IllegalStateException(
206                     "Another lifecycle phase is already active on this thread "
207                             + aphase.phase + ", setting " + phase);
208         }
209 
210         aphase.phase = phase;
211     };
212 
213     /**
214      * {@inheritDoc}
215      */
216     @Override
217     public LifecycleRenderingContext getRenderingContext() {
218         if (!ViewLifecycle.isRenderInLifecycle()) {
219             return null;
220         }
221 
222         AsynchronousLifecyclePhase aphase = ACTIVE_PHASE.get();
223 
224         if (aphase == null) {
225             throw new IllegalStateException("No phase worker is active on this thread");
226         }
227 
228         // If a rendering context has already been assigned to this phase, return it.
229         LifecycleRenderingContext renderContext = aphase.renderingContext;
230         if (renderContext != null) {
231             return renderContext;
232         }
233 
234         // Get a reusable rendering context from a pool private to the current lifecycle. 
235         renderContext = renderingContextPool.poll();
236         if (renderContext == null) {
237             // Create a new rendering context if a pooled instance is not available.
238             ViewLifecycle lifecycle = getLifecycle();
239             renderContext = new LifecycleRenderingContext(
240                     lifecycle.model, lifecycle.request, lifecycle.response);
241         }
242 
243         // Ensure that all view templates have been imported on the new/reused context
244         List<String> viewTemplates = ViewLifecycle.getView().getViewTemplates();
245         synchronized (viewTemplates) {
246             for (String viewTemplate : viewTemplates) {
247                 renderContext.importTemplate(viewTemplate);
248             }
249         }
250 
251         // Assign the rendering context to the current thread.
252         aphase.renderingContext = renderContext;
253         return renderContext;
254     }
255 
256     /**
257      * {@inheritDoc}
258      */
259     @Override
260     public ExpressionEvaluator getExpressionEvaluator() {
261         AsynchronousLifecyclePhase aphase = ACTIVE_PHASE.get();
262 
263         // If a rendering context has already been assigned to this phase, return it.
264         ExpressionEvaluator expressionEvaluator = aphase == null ? null : aphase.expressionEvaluator;
265         if (expressionEvaluator != null) {
266             return expressionEvaluator;
267         }
268 
269         // Get a reusable expression evaluator from a pool private to the current lifecycle. 
270         expressionEvaluator = expressionEvaluatorPool.poll();
271         if (expressionEvaluator == null) {
272             // Create a new expression evaluator if a pooled instance is not available.
273             ExpressionEvaluatorFactory expressionEvaluatorFactory;
274             ViewHelperService helper = ViewLifecycle.getHelper();
275             if (helper != null) {
276                 expressionEvaluatorFactory = helper.getExpressionEvaluatorFactory();
277             } else {
278                 expressionEvaluatorFactory = KRADServiceLocatorWeb.getExpressionEvaluatorFactory();
279             }
280 
281             if (expressionEvaluatorFactory == null) {
282                 expressionEvaluator = new DefaultExpressionEvaluator();
283             } else {
284                 expressionEvaluator = expressionEvaluatorFactory.createExpressionEvaluator();
285             }
286 
287             if (ViewLifecycle.isActive()) {
288                 try {
289                     expressionEvaluator.initializeEvaluationContext(ViewLifecycle.getModel());
290                 } catch (IllegalStateException e) {
291                     // Model is unavailable - may happen in unit test environments
292                     LOG.warn("Model is not available", e);
293                 }
294             }
295         }
296 
297         // Assign the rendering context to the current thread.
298         if (aphase != null) {
299             aphase.expressionEvaluator = expressionEvaluator;
300         }
301         
302         return expressionEvaluator;
303     }
304 
305     /**
306      * {@inheritDoc}
307      */
308     @Override
309     public void pushPendingPhase(ViewLifecyclePhase phase) {
310         AsynchronousLifecyclePhase aphase = getAsynchronousPhase(phase);
311         if (phase.getStartViewStatus().equals(phase.getElement().getViewStatus())) {
312             synchronized (BUSY_ELEMENTS) {
313                 BUSY_ELEMENTS.put(phase.getElement(), aphase);
314             }
315         }
316 
317         synchronized (PENDING_PHASE_QUEUE) {
318             PENDING_PHASE_QUEUE.push(aphase);
319             PENDING_PHASE_QUEUE.notify();
320         }
321 
322         spawnWorkers();
323     }
324 
325     /**
326      * {@inheritDoc}
327      */
328     @Override
329     public void offerPendingPhase(ViewLifecyclePhase phase) {
330         AsynchronousLifecyclePhase aphase = getAsynchronousPhase(phase);
331         if (phase.getStartViewStatus().equals(phase.getElement().getViewStatus())) {
332             synchronized (BUSY_ELEMENTS) {
333                 BUSY_ELEMENTS.put(phase.getElement(), aphase);
334             }
335         }
336 
337         synchronized (PENDING_PHASE_QUEUE) {
338             PENDING_PHASE_QUEUE.offer(aphase);
339             PENDING_PHASE_QUEUE.notify();
340         }
341 
342         spawnWorkers();
343     }
344 
345     /**
346      * {@inheritDoc}
347      * <p>This method should only be called a single time by the controlling thread in order to wait
348      * for all pending phases to be performed, and should not be called by any worker threads.</p>
349      */
350     @Override
351     public void performPhase(ViewLifecyclePhase initialPhase) {
352         if (error != null) {
353             throw new RiceRuntimeException("Error performing view lifecycle", error);
354         }
355 
356         long now = System.currentTimeMillis();
357         try {
358             AsynchronousLifecyclePhase aphase = getAsynchronousPhase(initialPhase);
359             aphase.initial = true;
360 
361             synchronized (PENDING_PHASE_QUEUE) {
362                 PENDING_PHASE_QUEUE.offer(aphase);
363                 PENDING_PHASE_QUEUE.notify();
364             }
365 
366             spawnWorkers();
367 
368             while (System.currentTimeMillis() - now < getTimeout() &&
369                     error == null && !initialPhase.isComplete()) {
370                 synchronized (initialPhase) {
371                     // Double-check lock
372                     if (!initialPhase.isComplete()) {
373                         LOG.info("Waiting for view lifecycle " + initialPhase);
374                         initialPhase.wait(Math.min(5000L, getTimeout()));
375                     }
376                 }
377             }
378 
379             if (error != null) {
380                 throw new IllegalStateException("Error in lifecycle", error);
381             }
382 
383             if (!initialPhase.isComplete()) {
384                 error = new IllegalStateException("Time out waiting for lifecycle");
385                 throw (IllegalStateException) error; 
386             }
387 
388         } catch (InterruptedException e) {
389             throw new IllegalStateException("Interrupted waiting for view lifecycle", e);
390         }
391     }
392 
393     /**
394      * Gets a new context wrapper for processing a lifecycle phase using the same lifecycle and
395      * thread context as the current thread.
396      * 
397      * @param phase The lifecycle phase.
398      * @return context wrapper for processing the phase
399      */
400     private AsynchronousLifecyclePhase getAsynchronousPhase(ViewLifecyclePhase phase) {
401         AsynchronousLifecyclePhase rv = RecycleUtils.getRecycledInstance(AsynchronousLifecyclePhase.class);
402         if (rv == null) {
403             rv = new AsynchronousLifecyclePhase();
404         }
405 
406         rv.processor = this;
407         rv.globalVariables = GlobalVariables.getCurrentGlobalVariables();
408         rv.phase = phase;
409 
410         return rv;
411     }
412 
413     /**
414      * Recycles a phase context after processing.
415      * 
416      * @param aphase phase context previously acquired using
417      *        {@link #getAsynchronousPhase(ViewLifecyclePhase)}
418      */
419     private static void recyclePhase(AsynchronousLifecyclePhase aphase) {
420         if (aphase.initial) {
421             return;
422         }
423 
424         assert aphase.renderingContext == null;
425         aphase.processor = null;
426         aphase.phase = null;
427         aphase.globalVariables = null;
428         aphase.expressionEvaluator = null;
429         RecycleUtils.recycle(aphase);
430     }
431 
432     /**
433      * Spawns new worker threads if needed.
434      */
435     private static void spawnWorkers() {
436         int active = LIFECYCLE_EXECUTOR.getActiveCount();
437         if (active < LIFECYCLE_EXECUTOR.getCorePoolSize() ||
438                 (active * 16 < PENDING_PHASE_QUEUE.size() &&
439                 active < LIFECYCLE_EXECUTOR.getMaximumPoolSize())) {
440             LIFECYCLE_EXECUTOR.submit(new AsynchronousLifecycleWorker());
441         }
442     }
443 
444     /**
445      * Private context wrapper for forwarding lifecycle state to worker threads.
446      * 
447      * @author Kuali Rice Team (rice.collab@kuali.org)
448      */
449     private static class AsynchronousLifecyclePhase {
450         private boolean initial;
451         private GlobalVariables globalVariables;
452         private AsynchronousViewLifecycleProcessor processor;
453         private ViewLifecyclePhase phase;
454         private LifecycleRenderingContext renderingContext;
455         private ExpressionEvaluator expressionEvaluator;
456     }
457 
458     /**
459      * Encapsulates lifecycle phase worker activity.
460      * 
461      * @author Kuali Rice Team (rice.collab@kuali.org)
462      */
463     private static class PhaseWorkerCall implements Callable<Void> {
464 
465         @Override
466         public Void call() throws Exception {
467             while (!PENDING_PHASE_QUEUE.isEmpty()) {
468                 AsynchronousLifecyclePhase aphase;
469                 synchronized (PENDING_PHASE_QUEUE) {
470                     aphase = PENDING_PHASE_QUEUE.poll();
471                 }
472                 
473                 if (aphase == null) {
474                     continue;
475                 }
476 
477                 AsynchronousViewLifecycleProcessor processor = aphase.processor;
478                 ViewLifecyclePhase phase = aphase.phase;
479 
480                 if (processor.error != null) {
481                     synchronized (phase) {
482                         phase.notifyAll();
483                     }
484 
485                     continue;
486                 }
487 
488                 LifecycleElement element = phase.getElement();
489                 AsynchronousLifecyclePhase busyPhase = BUSY_ELEMENTS.get(element);
490                 if (busyPhase != null && busyPhase != aphase) {
491                     // Another phase is already active on this component, requeue
492                     synchronized (PENDING_PHASE_QUEUE) {
493                         PENDING_PHASE_QUEUE.offer(aphase);
494                     }
495                     
496                     continue;
497                 }
498 
499                 try {
500                     assert ACTIVE_PHASE.get() == null;
501                     ACTIVE_PHASE.set(aphase);
502                     ViewLifecycle.setProcessor(aphase.processor);
503                     GlobalVariables.injectGlobalVariables(aphase.globalVariables);
504 
505                     phase.run();
506 
507                 } catch (Throwable t) {
508                     processor.error = t;
509 
510                     ViewLifecyclePhase topPhase = phase;
511                     while (topPhase.getPredecessor() != null) {
512                         topPhase = topPhase.getPredecessor();
513                     }
514                     
515                     synchronized (topPhase) {
516                         topPhase.notifyAll();
517                     }
518                 } finally {
519                     ACTIVE_PHASE.remove();
520                     LifecycleRenderingContext renderingContext = aphase.renderingContext;
521                     aphase.renderingContext = null;
522                     if (renderingContext != null && aphase.processor != null) {
523                         aphase.processor.renderingContextPool.offer(renderingContext);
524                     }
525 
526                     ExpressionEvaluator expressionEvaluator = aphase.expressionEvaluator;
527                     aphase.expressionEvaluator = null;
528                     if (expressionEvaluator != null && aphase.processor != null) {
529                         aphase.processor.expressionEvaluatorPool.offer(expressionEvaluator);
530                     }
531 
532                     synchronized (BUSY_ELEMENTS) {
533                         BUSY_ELEMENTS.remove(element);
534                     }
535                     GlobalVariables.popGlobalVariables();
536                     ViewLifecycle.setProcessor(null);
537                 }
538 
539                 recyclePhase(aphase);
540             }
541             return null;
542         }
543 
544     }
545 
546     /**
547      * Worker process to submit to the executor. Wraps {@link PhaseWorkerCall} in a process logger
548      * for tracing activity.
549      * 
550      * @author Kuali Rice Team (rice.collab@kuali.org)
551      */
552     private static class AsynchronousLifecycleWorker implements Runnable {
553 
554         @Override
555         public void run() {
556             try {
557                 PhaseWorkerCall call = new PhaseWorkerCall();
558                 do {
559                     if (PENDING_PHASE_QUEUE.isEmpty()) {
560                         synchronized (PENDING_PHASE_QUEUE) {
561                             PENDING_PHASE_QUEUE.wait(15000L);
562                         }
563                     } else if (ViewLifecycle.isTrace()) {
564                         ProcessLogger.follow(
565                                 "view-lifecycle", "KRAD lifecycle worker", call);
566                     } else {
567                         call.call();
568                     }
569                 } while (LIFECYCLE_EXECUTOR.getActiveCount() <= getMinThreads());
570             } catch (Throwable t) {
571                 LOG.fatal("Fatal error in View Lifecycle worker", t);
572             }
573         }
574 
575     }
576 
577 }