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 * "krad.uif.lifecycle.asynchronous.minThreads". 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 * "krad.uif.lifecycle.asynchronous.maxThreads". 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 * "krad.uif.lifecycle.asynchronous.timeout". 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}