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