001/**
002 * Copyright 2005-2018 The Kuali Foundation
003 *
004 * Licensed under the Educational Community License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.opensource.org/licenses/ecl2.php
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.kuali.rice.krad.uif.lifecycle;
017
018import java.util.Deque;
019import java.util.IdentityHashMap;
020import java.util.LinkedList;
021import java.util.List;
022import java.util.Map;
023import java.util.Queue;
024import java.util.concurrent.Callable;
025import java.util.concurrent.ConcurrentLinkedQueue;
026import java.util.concurrent.LinkedBlockingDeque;
027import java.util.concurrent.ThreadFactory;
028import java.util.concurrent.ThreadPoolExecutor;
029import java.util.concurrent.TimeUnit;
030
031import org.apache.log4j.Logger;
032import org.kuali.rice.core.api.config.property.ConfigContext;
033import org.kuali.rice.core.api.exception.RiceRuntimeException;
034import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
035import org.kuali.rice.krad.uif.freemarker.LifecycleRenderingContext;
036import org.kuali.rice.krad.uif.service.ViewHelperService;
037import org.kuali.rice.krad.uif.util.LifecycleElement;
038import org.kuali.rice.krad.uif.util.ProcessLogger;
039import org.kuali.rice.krad.uif.util.RecycleUtils;
040import org.kuali.rice.krad.uif.view.DefaultExpressionEvaluator;
041import org.kuali.rice.krad.uif.view.ExpressionEvaluator;
042import org.kuali.rice.krad.uif.view.ExpressionEvaluatorFactory;
043import org.kuali.rice.krad.util.GlobalVariables;
044import org.kuali.rice.krad.util.KRADConstants;
045
046/**
047 * Static utility class for handling executor configuration and spreading {@link ViewLifecycle}
048 * across multiple threads.
049 * 
050 * @author Kuali Rice Team (rice.collab@kuali.org)
051 */
052public final class AsynchronousViewLifecycleProcessor extends ViewLifecycleProcessorBase {
053
054    private static final Logger LOG = Logger.getLogger(AsynchronousViewLifecycleProcessor.class);
055
056    private static final ThreadFactory LIFECYCLE_THREAD_FACTORY = new LifecycleThreadFactory();
057
058    private static final ThreadPoolExecutor LIFECYCLE_EXECUTOR = new ThreadPoolExecutor(
059            getMinThreads(), getMaxThreads(), getTimeout(), TimeUnit.MILLISECONDS,
060            new LinkedBlockingDeque<Runnable>(), LIFECYCLE_THREAD_FACTORY);
061
062    private static final Deque<AsynchronousLifecyclePhase> PENDING_PHASE_QUEUE =
063            new LinkedList<AsynchronousLifecyclePhase>();
064
065    private static final ThreadLocal<AsynchronousLifecyclePhase> ACTIVE_PHASE =
066            new ThreadLocal<AsynchronousLifecyclePhase>();
067    
068    private static final Map<LifecycleElement, AsynchronousLifecyclePhase> BUSY_ELEMENTS =
069            new IdentityHashMap<LifecycleElement, AsynchronousLifecyclePhase>();
070
071    private static Integer minThreads;
072    private static Integer maxThreads;
073    private static Long timeout;
074
075    private final Queue<LifecycleRenderingContext> renderingContextPool =
076            ViewLifecycle.isRenderInLifecycle() ? new ConcurrentLinkedQueue<LifecycleRenderingContext>() : null;
077    private final Queue<ExpressionEvaluator> expressionEvaluatorPool =
078            new ConcurrentLinkedQueue<ExpressionEvaluator>();
079
080    private Throwable error;
081
082    /**
083     * Gets the minimum number of lifecycle worker threads to maintain.
084     * 
085     * <p>
086     * This value is controlled by the configuration parameter
087     * &quot;krad.uif.lifecycle.asynchronous.minThreads&quot;.
088     * </p>
089     * 
090     * @return minimum number of worker threads to maintain
091     */
092    public static int getMinThreads() {
093        if (minThreads == null) {
094            String propStr = null;
095            if (ConfigContext.getCurrentContextConfig() != null) {
096                propStr = ConfigContext.getCurrentContextConfig().getProperty(
097                        KRADConstants.ConfigParameters.KRAD_VIEW_LIFECYCLE_MINTHREADS);
098            }
099
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}