--- a/java/org/apache/catalina/Container.java +++ a/java/org/apache/catalina/Container.java @@ -467,4 +467,21 @@ public interface Container extends Lifecycle { * that the request/response still appears in the correct access logs. */ public AccessLog getAccessLog(); + + + /** + * Returns the number of threads available for starting and stopping any + * children associated with this container. This allows start/stop calls to + * children to be processed in parallel. + */ + public int getStartStopThreads(); + + + /** + * Sets the number of threads available for starting and stopping any + * children associated with this container. This allows start/stop calls to + * children to be processed in parallel. + * @param startStopThreads The new number of threads to be used + */ + public void setStartStopThreads(int startStopThreads); } --- a/java/org/apache/catalina/Host.java +++ a/java/org/apache/catalina/Host.java @@ -17,6 +17,7 @@ package org.apache.catalina; import java.io.File; +import java.util.concurrent.ExecutorService; import java.util.regex.Pattern; @@ -180,9 +181,16 @@ public interface Host extends Container { public void setDeployIgnore(String deployIgnore); - // --------------------------------------------------------- Public Methods + /** + * Return the executor that is used for starting and stopping contexts. This + * is primarily for use by components deploying contexts that want to do + * this in a multi-threaded manner. + */ + public ExecutorService getStartStopExecutor(); + // --------------------------------------------------------- Public Methods + /** * Add an alias name that should be mapped to this same Host. * --- a/java/org/apache/catalina/core/ContainerBase.java +++ a/java/org/apache/catalina/core/ContainerBase.java @@ -26,6 +26,13 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; import javax.management.ObjectName; import javax.naming.directory.DirContext; @@ -60,7 +67,8 @@ import org.apache.tomcat.util.res.StringManager; /** * Abstract implementation of the Container interface, providing common * functionality required by nearly every implementation. Classes extending - * this base class must may implement a replacement for invoke(). + * this base class must implement getInfo(), and may implement + * a replacement for invoke(). *

* All subclasses of this abstract base class will include support for a * Pipeline object that defines the processing to be performed for each request @@ -273,8 +281,55 @@ public abstract class ContainerBase extends LifecycleMBeanBase protected volatile AccessLog accessLog = null; private volatile boolean accessLogScanComplete = false; + + /** + * The number of threads available to process start and stop events for any + * children associated with this container. + */ + private int startStopThreads = 1; + protected ThreadPoolExecutor startStopExecutor; + // ------------------------------------------------------------- Properties + @Override + public int getStartStopThreads() { + return startStopThreads; + } + + /** + * Handles the special values. + */ + private int getStartStopThreadsInternal() { + int result = getStartStopThreads(); + + // Positive values are unchanged + if (result > 0) { + return result; + } + + // Zero == Runtime.getRuntime().availableProcessors() + // -ve == Runtime.getRuntime().availableProcessors() + value + // These two are the same + result = Runtime.getRuntime().availableProcessors() + result; + if (result < 1) { + result = 1; + } + return result; + } + + @Override + public void setStartStopThreads(int startStopThreads) { + this.startStopThreads = startStopThreads; + + // Use local copies to ensure thread safety + ThreadPoolExecutor executor = startStopExecutor; + if (executor != null) { + int newThreads = getStartStopThreadsInternal(); + executor.setMaximumPoolSize(newThreads); + executor.setCorePoolSize(newThreads); + } + } + /** * Get the delay between the invocation of the backgroundProcess method on @@ -989,6 +1044,19 @@ public abstract class ContainerBase extends LifecycleMBeanBase } + @Override + protected void initInternal() throws LifecycleException { + BlockingQueue startStopQueue = + new LinkedBlockingQueue(); + startStopExecutor = new ThreadPoolExecutor( + getStartStopThreadsInternal(), + getStartStopThreadsInternal(), 10, TimeUnit.SECONDS, + startStopQueue); + startStopExecutor.allowCoreThreadTimeOut(true); + super.initInternal(); + } + + /** * Start this component and implement the requirements * of {@link org.apache.catalina.util.LifecycleBase#startInternal()}. @@ -1017,8 +1085,24 @@ public abstract class ContainerBase extends LifecycleMBeanBase // Start our child containers, if any Container children[] = findChildren(); + List> results = new ArrayList>(); for (int i = 0; i < children.length; i++) { - children[i].start(); + results.add(startStopExecutor.submit(new StartChild(children[i]))); + } + + boolean fail = false; + for (Future result : results) { + try { + result.get(); + } catch (Exception e) { + log.error(sm.getString("containerBase.threadedStartFailed"), e); + fail = true; + } + + } + if (fail) { + throw new LifecycleException( + sm.getString("containerBase.threadedStartFailed")); } // Start the Valves in our pipeline (including the basic), if any @@ -1057,8 +1141,23 @@ public abstract class ContainerBase extends LifecycleMBeanBase // Stop our child containers, if any Container children[] = findChildren(); + List> results = new ArrayList>(); for (int i = 0; i < children.length; i++) { - children[i].stop(); + results.add(startStopExecutor.submit(new StopChild(children[i]))); + } + + boolean fail = false; + for (Future result : results) { + try { + result.get(); + } catch (Exception e) { + log.error(sm.getString("containerBase.threadedStopFailed"), e); + fail = true; + } + } + if (fail) { + throw new LifecycleException( + sm.getString("containerBase.threadedStopFailed")); } // Stop our subordinate components, if any @@ -1101,6 +1200,8 @@ public abstract class ContainerBase extends LifecycleMBeanBase parent.removeChild(this); } + startStopExecutor.shutdownNow(); + super.destroyInternal(); } @@ -1400,4 +1501,37 @@ public abstract class ContainerBase extends LifecycleMBeanBase } } } + + + // ----------------------------- Inner classes used with start/stop Executor + + private static class StartChild implements Callable { + + private Container child; + + public StartChild(Container child) { + this.child = child; + } + + @Override + public Void call() throws LifecycleException { + child.start(); + return null; + } + } + + private static class StopChild implements Callable { + + private Container child; + + public StopChild(Container child) { + this.child = child; + } + + @Override + public Void call() throws LifecycleException { + child.stop(); + return null; + } + } } --- a/java/org/apache/catalina/core/LocalStrings.properties +++ a/java/org/apache/catalina/core/LocalStrings.properties @@ -53,6 +53,8 @@ aprListener.sslInit=Failed to initialize the SSLEngine. aprListener.tcnValid=Loaded APR based Apache Tomcat Native library {0}. aprListener.flags=APR capabilities: IPv6 [{0}], sendfile [{1}], accept filters [{2}], random [{3}]. asyncContextImpl.requestEnded=The request associated with the AsyncContext has already completed processing. +containerBase.threadedStartFailed=A child container failed during start +containerBase.threadedStopFailed=A child container failed during stop containerBase.backgroundProcess.cluster=Exception processing cluster {0} background process containerBase.backgroundProcess.loader=Exception processing loader {0} background process containerBase.backgroundProcess.manager=Exception processing manager {0} background process --- a/java/org/apache/catalina/core/StandardContext.java +++ a/java/org/apache/catalina/core/StandardContext.java @@ -37,7 +37,6 @@ import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.TreeMap; -import java.util.concurrent.Callable; import java.util.concurrent.atomic.AtomicLong; import javax.management.ListenerNotFoundException; @@ -118,7 +117,6 @@ import org.apache.tomcat.JarScanner; import org.apache.tomcat.util.ExceptionUtils; import org.apache.tomcat.util.modeler.Registry; import org.apache.tomcat.util.scan.StandardJarScanner; -import org.apache.tomcat.util.threads.DedicatedThreadExecutor; /** * Standard implementation of the Context interface. Each @@ -160,6 +158,13 @@ public class StandardContext extends ContainerBase /** + * The descriptive information string for this implementation. + */ + private static final String info = + "org.apache.catalina.core.StandardContext/1.0"; + + + /** * Array containing the safe characters set. */ protected static URLEncoder urlEncoder; @@ -5182,9 +5187,7 @@ public class StandardContext extends ContainerBase } } - DedicatedThreadExecutor temporaryExecutor = new DedicatedThreadExecutor(); try { - // Create context attributes that will be required if (ok) { getServletContext().setAttribute( @@ -5209,22 +5212,7 @@ public class StandardContext extends ContainerBase // Configure and call application event listeners if (ok) { - // we do it in a dedicated thread for memory leak protection, in - // case the Listeners registers some ThreadLocals that they - // forget to cleanup - Boolean listenerStarted = - temporaryExecutor.execute(new Callable() { - @Override - public Boolean call() throws Exception { - ClassLoader old = bindThread(); - try { - return Boolean.valueOf(listenerStart()); - } finally { - unbindThread(old); - } - } - }); - if (!listenerStarted.booleanValue()) { + if (!listenerStart()) { log.error( "Error listenerStart"); ok = false; } @@ -5245,22 +5233,7 @@ public class StandardContext extends ContainerBase // Configure and call application filters if (ok) { - // we do it in a dedicated thread for memory leak protection, in - // case the Filters register some ThreadLocals that they forget - // to cleanup - Boolean filterStarted = - temporaryExecutor.execute(new Callable() { - @Override - public Boolean call() throws Exception { - ClassLoader old = bindThread(); - try { - return Boolean.valueOf(filterStart()); - } finally { - unbindThread(old); - } - } - }); - if (!filterStarted.booleanValue()) { + if (!filterStart()) { log.error("Error filterStart"); ok = false; } @@ -5268,27 +5241,12 @@ public class StandardContext extends ContainerBase // Load and initialize all "load on startup" servlets if (ok) { - // we do it in a dedicated thread for memory leak protection, in - // case the Servlets register some ThreadLocals that they forget - // to cleanup - temporaryExecutor.execute(new Callable() { - @Override - public Void call() throws Exception { - ClassLoader old = bindThread(); - try { - loadOnStartup(findChildren()); - return null; - } finally { - unbindThread(old); - } - } - }); + loadOnStartup(findChildren()); } } finally { // Unbinding thread unbindThread(oldCCL); - temporaryExecutor.shutdown(); } // Set available status depending upon startup success @@ -5428,61 +5386,28 @@ public class StandardContext extends ContainerBase // Stop our child containers, if any final Container[] children = findChildren(); - // we do it in a dedicated thread for memory leak protection, in - // case some webapp code registers some ThreadLocals that they - // forget to cleanup - // TODO Figure out why DedicatedThreadExecutor hangs randomly in the - // unit tests if used here - RunnableWithLifecycleException stop = - new RunnableWithLifecycleException() { - @Override - public void run() { - ClassLoader old = bindThread(); - try { - for (int i = 0; i < children.length; i++) { - try { - children[i].stop(); - } catch (LifecycleException e) { - le = e; - return; - } - } - // Stop our filters - filterStop(); + ClassLoader old = bindThread(); + try { + for (int i = 0; i < children.length; i++) { + children[i].stop(); + } - // Stop ContainerBackgroundProcessor thread - threadStop(); + // Stop our filters + filterStop(); - if (manager != null && manager instanceof Lifecycle && - ((Lifecycle) manager).getState().isAvailable()) { - try { - ((Lifecycle) manager).stop(); - } catch (LifecycleException e) { - le = e; - return; - } - } + // Stop ContainerBackgroundProcessor thread + threadStop(); - // Stop our application listeners - listenerStop(); - }finally{ - unbindThread(old); - } + if (manager != null && manager instanceof Lifecycle && + ((Lifecycle) manager).getState().isAvailable()) { + ((Lifecycle) manager).stop(); } - }; - Thread t = new Thread(stop); - t.setName("stop children - " + getObjectName().toString()); - t.start(); - try { - t.join(); - } catch (InterruptedException e) { - // Shouldn't happen - throw new LifecycleException(e); - } - if (stop.getLifecycleException() != null) { - throw stop.getLifecycleException(); + // Stop our application listeners + listenerStop(); + } finally{ + unbindThread(old); } // Finalize our character set mapper --- a/java/org/apache/catalina/core/StandardHost.java +++ a/java/org/apache/catalina/core/StandardHost.java @@ -24,6 +24,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; import java.util.WeakHashMap; +import java.util.concurrent.ExecutorService; import java.util.regex.Pattern; import org.apache.catalina.Container; @@ -140,6 +141,12 @@ public class StandardHost extends ContainerBase implements Host { private String errorReportValveClass = "org.apache.catalina.valves.ErrorReportValve"; + /** + * The descriptive information string for this implementation. + */ + private static final String info = + "org.apache.catalina.core.StandardHost/1.0"; + /** * Unpack WARs property. @@ -177,6 +184,11 @@ public class StandardHost extends ContainerBase implements Host { // ------------------------------------------------------------- Properties + @Override + public ExecutorService getStartStopExecutor() { + return startStopExecutor; + } + /** * Return the application root for this Host. This can be an absolute --- a/java/org/apache/catalina/core/mbeans-descriptors.xml +++ a/java/org/apache/catalina/core/mbeans-descriptors.xml @@ -1011,6 +1011,10 @@ description="Will children be started automatically when they are added." type="boolean"/> + + + + Digester we will use to process web application - * context files. - */ - protected static Digester contextDigester = null; - /** * The set of Authenticators that we know how to configure. The key is @@ -143,32 +137,6 @@ public class ContextConfig /** - * The Digesters available to process web deployment descriptor - * files. - */ - protected static Digester[] webDigesters = new Digester[4]; - - - /** - * The Digesters available to process web fragment deployment - * descriptor files. - */ - protected static Digester[] webFragmentDigesters = new Digester[4]; - - - /** - * The Rules used to parse the web.xml - */ - protected static WebRuleSet webRuleSet = new WebRuleSet(false); - - - /** - * The Rules used to parse the web-fragment.xml - */ - protected static WebRuleSet webFragmentRuleSet = new WebRuleSet(true); - - - /** * Deployment count. */ protected static long deploymentCount = 0L; @@ -236,12 +204,14 @@ public class ContextConfig * deployment descriptor files. */ protected Digester webDigester = null; + protected WebRuleSet webRuleSet = null; /** * The Digester we will use to process web fragment * deployment descriptor files. */ protected Digester webFragmentDigester = null; + protected WebRuleSet webFragmentRuleSet = null; // ------------------------------------------------------------- Properties @@ -486,60 +456,21 @@ public class ContextConfig /** - * Create (if necessary) and return a Digester configured to process the + * Create and return a Digester configured to process the * web application deployment descriptor (web.xml). */ public void createWebXmlDigester(boolean namespaceAware, boolean validation) { - if (!namespaceAware && !validation) { - if (webDigesters[0] == null) { - webDigesters[0] = DigesterFactory.newDigester(validation, - namespaceAware, webRuleSet); - webFragmentDigesters[0] = DigesterFactory.newDigester(validation, - namespaceAware, webFragmentRuleSet); - webDigesters[0].getParser(); - webFragmentDigesters[0].getParser(); - } - webDigester = webDigesters[0]; - webFragmentDigester = webFragmentDigesters[0]; - - } else if (!namespaceAware && validation) { - if (webDigesters[1] == null) { - webDigesters[1] = DigesterFactory.newDigester(validation, - namespaceAware, webRuleSet); - webFragmentDigesters[1] = DigesterFactory.newDigester(validation, - namespaceAware, webFragmentRuleSet); - webDigesters[1].getParser(); - webFragmentDigesters[1].getParser(); - } - webDigester = webDigesters[1]; - webFragmentDigester = webFragmentDigesters[1]; - - } else if (namespaceAware && !validation) { - if (webDigesters[2] == null) { - webDigesters[2] = DigesterFactory.newDigester(validation, - namespaceAware, webRuleSet); - webFragmentDigesters[2] = DigesterFactory.newDigester(validation, - namespaceAware, webFragmentRuleSet); - webDigesters[2].getParser(); - webFragmentDigesters[2].getParser(); - } - webDigester = webDigesters[2]; - webFragmentDigester = webFragmentDigesters[2]; + webRuleSet = new WebRuleSet(false); + webDigester = DigesterFactory.newDigester(validation, + namespaceAware, webRuleSet); + webDigester.getParser(); - } else { - if (webDigesters[3] == null) { - webDigesters[3] = DigesterFactory.newDigester(validation, - namespaceAware, webRuleSet); - webFragmentDigesters[3] = DigesterFactory.newDigester(validation, - namespaceAware, webFragmentRuleSet); - webDigesters[3].getParser(); - webFragmentDigesters[3].getParser(); - } - webDigester = webDigesters[3]; - webFragmentDigester = webFragmentDigesters[3]; - } + webFragmentRuleSet = new WebRuleSet(true); + webFragmentDigester = DigesterFactory.newDigester(validation, + namespaceAware, webFragmentRuleSet); + webFragmentDigester.getParser(); } @@ -577,7 +508,7 @@ public class ContextConfig /** * Process the default configuration file, if it exists. */ - protected void contextConfig() { + protected void contextConfig(Digester digester) { // Open the default context.xml file, if it exists if( defaultContextXml==null && context instanceof StandardContext ) { @@ -596,7 +527,7 @@ public class ContextConfig if (defaultContextFile.exists()) { try { URL defaultContextUrl = defaultContextFile.toURI().toURL(); - processContextConfig(defaultContextUrl); + processContextConfig(digester, defaultContextUrl); } catch (MalformedURLException e) { log.error(sm.getString( "contextConfig.badUrl", defaultContextFile), e); @@ -608,7 +539,7 @@ public class ContextConfig if (hostContextFile.exists()) { try { URL hostContextUrl = hostContextFile.toURI().toURL(); - processContextConfig(hostContextUrl); + processContextConfig(digester, hostContextUrl); } catch (MalformedURLException e) { log.error(sm.getString( "contextConfig.badUrl", hostContextFile), e); @@ -616,7 +547,7 @@ public class ContextConfig } } if (context.getConfigFile() != null) { - processContextConfig(context.getConfigFile()); + processContextConfig(digester, context.getConfigFile()); } } @@ -625,7 +556,7 @@ public class ContextConfig /** * Process a context.xml. */ - protected void processContextConfig(URL contextXml) { + protected void processContextConfig(Digester digester, URL contextXml) { if (log.isDebugEnabled()) { log.debug("Processing context [" + context.getName() @@ -653,45 +584,43 @@ public class ContextConfig if (source == null) { return; } - synchronized (contextDigester) { - try { - source.setByteStream(stream); - contextDigester.setClassLoader(this.getClass().getClassLoader()); - contextDigester.setUseContextClassLoader(false); - contextDigester.push(context.getParent()); - contextDigester.push(context); - XmlErrorHandler errorHandler = new XmlErrorHandler(); - contextDigester.setErrorHandler(errorHandler); - contextDigester.parse(source); - if (errorHandler.getWarnings().size() > 0 || - errorHandler.getErrors().size() > 0) { - errorHandler.logFindings(log, contextXml.toString()); - ok = false; - } - if (log.isDebugEnabled()) { - log.debug("Successfully processed context [" + context.getName() - + "] configuration file [" + contextXml + "]"); - } - } catch (SAXParseException e) { - log.error(sm.getString("contextConfig.contextParse", - context.getName()), e); - log.error(sm.getString("contextConfig.defaultPosition", - "" + e.getLineNumber(), - "" + e.getColumnNumber())); - ok = false; - } catch (Exception e) { - log.error(sm.getString("contextConfig.contextParse", - context.getName()), e); + + try { + source.setByteStream(stream); + digester.setClassLoader(this.getClass().getClassLoader()); + digester.setUseContextClassLoader(false); + digester.push(context.getParent()); + digester.push(context); + XmlErrorHandler errorHandler = new XmlErrorHandler(); + digester.setErrorHandler(errorHandler); + digester.parse(source); + if (errorHandler.getWarnings().size() > 0 || + errorHandler.getErrors().size() > 0) { + errorHandler.logFindings(log, contextXml.toString()); ok = false; - } finally { - contextDigester.reset(); - try { - if (stream != null) { - stream.close(); - } - } catch (IOException e) { - log.error(sm.getString("contextConfig.contextClose"), e); + } + if (log.isDebugEnabled()) { + log.debug("Successfully processed context [" + context.getName() + + "] configuration file [" + contextXml + "]"); + } + } catch (SAXParseException e) { + log.error(sm.getString("contextConfig.contextParse", + context.getName()), e); + log.error(sm.getString("contextConfig.defaultPosition", + "" + e.getLineNumber(), + "" + e.getColumnNumber())); + ok = false; + } catch (Exception e) { + log.error(sm.getString("contextConfig.contextParse", + context.getName()), e); + ok = false; + } finally { + try { + if (stream != null) { + stream.close(); } + } catch (IOException e) { + log.error(sm.getString("contextConfig.contextClose"), e); } } } @@ -846,10 +775,8 @@ public class ContextConfig protected void init() { // Called from StandardContext.init() - if (contextDigester == null){ - contextDigester = createContextDigester(); - contextDigester.getParser(); - } + Digester contextDigester = createContextDigester(); + contextDigester.getParser(); if (log.isDebugEnabled()) { log.debug(sm.getString("contextConfig.init")); @@ -857,7 +784,7 @@ public class ContextConfig context.setConfigured(false); ok = true; - contextConfig(); + contextConfig(contextDigester); createWebXmlDigester(context.getXmlNamespaceAware(), context.getXmlValidation()); @@ -1416,8 +1343,9 @@ public class ContextConfig } // Parsing global web.xml is relatively expensive. Use a sync block to - // make sure it only happens once - synchronized (host) { + // make sure it only happens once. Use the pipeline since a lock will + // already be held on the host by another thread + synchronized (host.getPipeline()) { entry = hostWebXmlCache.get(host); if (entry != null && entry.getGlobalTimeStamp() == globalTimeStamp && entry.getHostTimeStamp() == hostTimeStamp) { @@ -1679,7 +1607,6 @@ public class ContextConfig return getWebXmlSource(defaultWebXml, getBaseDir()); } - /** * Identify the host web.xml to be used and obtain an input source for * it. @@ -1806,9 +1733,6 @@ public class ContextConfig XmlErrorHandler handler = new XmlErrorHandler(); - // Web digesters and rulesets are shared between contexts but are not - // thread safe. Whilst there should only be one thread at a time - // processing a config, play safe and sync. Digester digester; WebRuleSet ruleSet; if (fragment) { @@ -1819,41 +1743,36 @@ public class ContextConfig ruleSet = webRuleSet; } - // Sync on the ruleSet since the same ruleSet is shared across all four - // digesters - synchronized(ruleSet) { + digester.push(dest); + digester.setErrorHandler(handler); - digester.push(dest); - digester.setErrorHandler(handler); - - if(log.isDebugEnabled()) { - log.debug(sm.getString("contextConfig.applicationStart", - source.getSystemId())); - } + if(log.isDebugEnabled()) { + log.debug(sm.getString("contextConfig.applicationStart", + source.getSystemId())); + } - try { - digester.parse(source); + try { + digester.parse(source); - if (handler.getWarnings().size() > 0 || - handler.getErrors().size() > 0) { - ok = false; - handler.logFindings(log, source.getSystemId()); - } - } catch (SAXParseException e) { - log.error(sm.getString("contextConfig.applicationParse", - source.getSystemId()), e); - log.error(sm.getString("contextConfig.applicationPosition", - "" + e.getLineNumber(), - "" + e.getColumnNumber())); + if (handler.getWarnings().size() > 0 || + handler.getErrors().size() > 0) { ok = false; - } catch (Exception e) { - log.error(sm.getString("contextConfig.applicationParse", - source.getSystemId()), e); - ok = false; - } finally { - digester.reset(); - ruleSet.recycle(); - } + handler.logFindings(log, source.getSystemId()); + } + } catch (SAXParseException e) { + log.error(sm.getString("contextConfig.applicationParse", + source.getSystemId()), e); + log.error(sm.getString("contextConfig.applicationPosition", + "" + e.getLineNumber(), + "" + e.getColumnNumber())); + ok = false; + } catch (Exception e) { + log.error(sm.getString("contextConfig.applicationParse", + source.getSystemId()), e); + ok = false; + } finally { + digester.reset(); + ruleSet.recycle(); } } @@ -2244,7 +2163,8 @@ public class ContextConfig /** * process filter annotation and merge with existing one! - * FIXME: refactoring method to long and has redundant subroutines with processAnnotationWebServlet! + * FIXME: refactoring method too long and has redundant subroutines with + * processAnnotationWebServlet! * @param className * @param ae * @param fragment --- a/java/org/apache/catalina/startup/HostConfig.java +++ a/java/org/apache/catalina/startup/HostConfig.java @@ -14,8 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - package org.apache.catalina.startup; @@ -33,7 +31,11 @@ import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.regex.Matcher; @@ -138,8 +140,8 @@ public class HostConfig /** * Map of deployed applications. */ - protected HashMap deployed = - new HashMap(); + protected Map deployed = + new ConcurrentHashMap(); /** @@ -497,6 +499,10 @@ public class HostConfig ContextName cn = new ContextName(name); String baseName = cn.getBaseName(); + if (deploymentExists(baseName)) { + return; + } + // Deploy XML descriptors from configBase File xml = new File(configBase, baseName + ".xml"); if (xml.exists()) @@ -520,17 +526,29 @@ public class HostConfig if (files == null) return; + ExecutorService es = host.getStartStopExecutor(); + List> results = new ArrayList>(); + for (int i = 0; i < files.length; i++) { File contextXml = new File(configBase, files[i]); if (files[i].toLowerCase(Locale.ENGLISH).endsWith(".xml")) { ContextName cn = new ContextName(files[i]); - String name = cn.getName(); - if (isServiced(name)) + if (isServiced(cn.getName()) || deploymentExists(cn.getName())) continue; - deployDescriptor(cn, contextXml); + results.add( + es.submit(new DeployDescriptor(this, cn, contextXml))); + } + } + + for (Future result : results) { + try { + result.get(); + } catch (Exception e) { + log.error(sm.getString( + "hostConfig.deployDescriptor.threaded.error"), e); } } } @@ -541,9 +559,6 @@ public class HostConfig * @param contextXml */ protected void deployDescriptor(ContextName cn, File contextXml) { - if (deploymentExists(cn.getName())) { - return; - } DeployedApplication deployedApp = new DeployedApplication(cn.getName()); @@ -670,6 +685,9 @@ public class HostConfig if (files == null) return; + ExecutorService es = host.getStartStopExecutor(); + List> results = new ArrayList>(); + for (int i = 0; i < files.length; i++) { if (files[i].equalsIgnoreCase("META-INF")) @@ -690,10 +708,19 @@ public class HostConfig continue; } - if (isServiced(cn.getName())) + if (isServiced(cn.getName()) || deploymentExists(cn.getName())) continue; - deployWAR(cn, war); + results.add(es.submit(new DeployWar(this, cn, war))); + } + } + + for (Future result : results) { + try { + result.get(); + } catch (Exception e) { + log.error(sm.getString( + "hostConfig.deployWar.threaded.error"), e); } } } @@ -741,9 +768,6 @@ public class HostConfig */ protected void deployWAR(ContextName cn, File war) { - if (deploymentExists(cn.getName())) - return; - // Checking for a nested /META-INF/context.xml JarFile jar = null; JarEntry entry = null; @@ -935,6 +959,9 @@ public class HostConfig if (files == null) return; + ExecutorService es = host.getStartStopExecutor(); + List> results = new ArrayList>(); + for (int i = 0; i < files.length; i++) { if (files[i].equalsIgnoreCase("META-INF")) @@ -945,10 +972,19 @@ public class HostConfig if (dir.isDirectory()) { ContextName cn = new ContextName(files[i]); - if (isServiced(cn.getName())) + if (isServiced(cn.getName()) || deploymentExists(cn.getName())) continue; - deployDirectory(cn, dir); + results.add(es.submit(new DeployDirectory(this, cn, dir))); + } + } + + for (Future result : results) { + try { + result.get(); + } catch (Exception e) { + log.error(sm.getString( + "hostConfig.deployDir.threaded.error"), e); } } } @@ -960,9 +996,6 @@ public class HostConfig */ protected void deployDirectory(ContextName cn, File dir) { - if (deploymentExists(cn.getName())) - return; - DeployedApplication deployedApp = new DeployedApplication(cn.getName()); // Deploy the application in this directory @@ -1450,7 +1483,61 @@ public class HostConfig /** * Instant where the application was last put in service. */ - public long timestamp = System.currentTimeMillis(); + public long timestamp = System.currentTimeMillis(); + } + + private static class DeployDescriptor implements Runnable { + + private HostConfig config; + private ContextName cn; + private File descriptor; + + public DeployDescriptor(HostConfig config, ContextName cn, + File descriptor) { + this.config = config; + this.cn = cn; + this.descriptor= descriptor; + } + + @Override + public void run() { + config.deployDescriptor(cn, descriptor); + } + } + + private static class DeployWar implements Runnable { + + private HostConfig config; + private ContextName cn; + private File war; + + public DeployWar(HostConfig config, ContextName cn, File war) { + this.config = config; + this.cn = cn; + this.war = war; + } + + @Override + public void run() { + config.deployWAR(cn, war); + } } + private static class DeployDirectory implements Runnable { + + private HostConfig config; + private ContextName cn; + private File dir; + + public DeployDirectory(HostConfig config, ContextName cn, File dir) { + this.config = config; + this.cn = cn; + this.dir = dir; + } + + @Override + public void run() { + config.deployDirectory(cn, dir); + } + } } --- a/java/org/apache/catalina/startup/LocalStrings.properties +++ a/java/org/apache/catalina/startup/LocalStrings.properties @@ -80,11 +80,19 @@ hostConfig.context.restart=Error during context [{0}] restart hostConfig.createDirs=Unable to create directory for deployment: {0} hostConfig.deployDescriptor=Deploying configuration descriptor {0} hostConfig.deployDescriptor.error=Error deploying configuration descriptor {0} +hostConfig.deployDescriptor.threaded.error=Error waiting for multi-thread deployment of context descriptors to complete hostConfig.deployDescriptor.localDocBaseSpecified=A docBase {0} inside the host appBase has been specified, and will be ignored hostConfig.deployDir=Deploying web application directory {0} hostConfig.deployDir.error=Error deploying web application directory {0} +hostConfig.deployDir.threaded.error=Error waiting for multi-thread deployment of directories to completehostConfig.deployWar=Deploying web application archive {0} hostConfig.deployWar=Deploying web application archive {0} hostConfig.deployWar.error=Error deploying web application archive {0} +hostConfig.deployWar.threaded.error=Error waiting for multi-thread deployment of WAR files to complete +hostConfig.deploy.error=Exception while deploying web application directory {0} +hostConfig.deploying=Deploying discovered web applications +hostConfig.expand=Expanding web application archive {0} +hostConfig.expand.error=Exception while expanding web application archive {0} +hostConfig.expanding=Expanding discovered web application archives hostConfig.ignorePath=Ignoring path [{0}] in appBase for automatic deployment hostConfig.illegalWarName=The war name [{0}] is invalid. The archive will be ignored. hostConfig.jmx.register=Register context [{0}] failed --- a/java/org/apache/coyote/AbstractProtocol.java +++ a/java/org/apache/coyote/AbstractProtocol.java @@ -493,6 +493,12 @@ public abstract class AbstractProtocol implements ProtocolHandler, SocketStatus status) { P processor = connections.remove(socket.getSocket()); + if (getLog().isDebugEnabled()) { + getLog().debug("process() entry " + + "Socket: [" + logHashcode(socket.getSocket()) + "], " + + "Processor [" + logHashcode(processor) + "]"); + } + socket.setAsync(false); try { @@ -503,20 +509,51 @@ public abstract class AbstractProtocol implements ProtocolHandler, processor = createProcessor(); } + if (getLog().isDebugEnabled()) { + getLog().debug("process() gotProcessor " + + "Socket: [" + logHashcode(socket.getSocket()) + "], " + + "Processor [" + logHashcode(processor) + "]"); + } + initSsl(socket, processor); SocketState state = SocketState.CLOSED; do { if (processor.isAsync() || state == SocketState.ASYNC_END) { state = processor.asyncDispatch(status); + if (getLog().isDebugEnabled()) { + getLog().debug("process() asyncDispatch " + + "Socket: [" + logHashcode(socket.getSocket()) + "], " + + "Processor: [" + logHashcode(processor) + "], " + + "State: [" + state.toString() + "]"); + } } else if (processor.isComet()) { state = processor.event(status); + if (getLog().isDebugEnabled()) { + getLog().debug("process() event " + + "Socket: [" + logHashcode(socket.getSocket()) + "], " + + "Processor: [" + logHashcode(processor) + "], " + + "State: [" + state.toString() + "]"); + } } else { state = processor.process(socket); + if (getLog().isDebugEnabled()) { + getLog().debug("process() process " + + "Socket: [" + logHashcode(socket.getSocket()) + "], " + + "Processor: [" + logHashcode(processor) + "], " + + "State: [" + state.toString() + "]"); + } } if (state != SocketState.CLOSED && processor.isAsync()) { state = processor.asyncPostProcess(); + if (getLog().isDebugEnabled()) { + getLog().debug("process() asyncPostProcess " + + "Socket: [" + logHashcode(socket.getSocket()) + "], " + + "Processor: [" + logHashcode(processor) + "], " + + "State: [" + state.toString() + "]"); + } + } } while (state == SocketState.ASYNC_END); @@ -562,6 +599,14 @@ public abstract class AbstractProtocol implements ProtocolHandler, return SocketState.CLOSED; } + private String logHashcode (Object o) { + if (o == null) { + return "null"; + } else { + return Integer.toString(o.hashCode()); + } + } + protected abstract P createProcessor(); protected abstract void initSsl(SocketWrapper socket, P processor); protected abstract void longPoll(SocketWrapper socket, P processor); --- a/java/org/apache/tomcat/util/threads/DedicatedThreadExecutor.java +++ a/java/org/apache/tomcat/util/threads/DedicatedThreadExecutor.java @@ -1,137 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.tomcat.util.threads; - -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; - -/** - * A utility class to execute a {@link Callable} in a dedicated thread. - * It can be used either with an instance to reuse the same thread for each call - * to {@link #execute(Callable)} or with the static method - * {@link #executeInOwnThread(Callable)}. When using an instance, - * {@link #shutdown()} must be called when the instance is no longer needed to - * dispose of the dedicated thread. - */ -public class DedicatedThreadExecutor { - private final SingleThreadFactory singleThreadFactory = - new SingleThreadFactory(); - private final ExecutorService executorService = - Executors.newSingleThreadExecutor(singleThreadFactory); - - /** - * Executes the given {@link Callable} with the thread spawned for the - * current {@link DedicatedThreadExecutor} instance, and returns its result. - * - * @param - * the type of the returned value - * @param callable - * @return the completed result - */ - public V execute(final Callable callable) { - final Future futureTask = executorService.submit(callable); - - boolean interrupted = false; - V result; - while (true) { - try { - result = futureTask.get(); - break; - } catch (InterruptedException e) { - // keep waiting - interrupted = true; - } catch (ExecutionException e) { - throw new RuntimeException(e); - } - } - if (interrupted) { - // set interruption status so that the caller may react to it - Thread.currentThread().interrupt(); - } - return result; - } - - /** - * Stops the dedicated thread and waits for its death. - */ - public void shutdown() { - executorService.shutdown(); - if (singleThreadFactory.singleThread != null) { - boolean interrupted = false; - while (true) { - try { - singleThreadFactory.singleThread.join(); - singleThreadFactory.singleThread = null; - break; - } catch (InterruptedException e) { - // keep waiting - interrupted = true; - } - } - if (interrupted) { - // set interruption status so that the caller may react to it - Thread.currentThread().interrupt(); - } - } - } - - /** - * Executes the given {@link Callable} in a new thread and returns the - * result after the thread is stopped. - * - * @param - * @param callable - * @return the completed result - */ - public static V executeInOwnThread( - final Callable callable) { - DedicatedThreadExecutor executor = new DedicatedThreadExecutor(); - try { - return executor.execute(callable); - } finally { - executor.shutdown(); - } - - } - - // we use a ThreadFactory so that we can later call Thread.join(). - // Indeed, calling shutdown() on an ExecutorService will eventually stop the - // thread but it might still be alive when execute() returns (race - // condition). - // This can lead to false alarms about potential memory leaks because the - // thread may have a web application class loader for its context class - // loader. - private static class SingleThreadFactory implements ThreadFactory { - private volatile Thread singleThread; - - @Override - public Thread newThread(Runnable r) { - if (singleThread != null) { - throw new IllegalStateException( - "should not have been called more than once"); - } - singleThread = new Thread(r); - singleThread.setDaemon(true); - return singleThread; - } - - } -} --- a/test/org/apache/tomcat/util/threads/DedicatedThreadExecutorTest.java +++ a/test/org/apache/tomcat/util/threads/DedicatedThreadExecutorTest.java @@ -1,74 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.tomcat.util.threads; - -import java.util.concurrent.Callable; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertSame; - -import org.junit.Test; - -public class DedicatedThreadExecutorTest { - private Thread dedicatedThread; - - @Test - public void testExecute() { - final Thread testingThread = Thread.currentThread(); - DedicatedThreadExecutor executor = new DedicatedThreadExecutor(); - Long result = executor.execute(new Callable() { - @Override - public Long call() throws Exception { - dedicatedThread = Thread.currentThread(); - assertNotSame(testingThread, dedicatedThread); - return Long.valueOf(123); - } - }); - assertEquals(123, result.longValue()); - - //check that the same thread is reused - executor.execute(new Callable() { - @Override - public Void call() throws Exception { - assertSame(dedicatedThread, Thread.currentThread()); - return null; - } - }); - - executor.shutdown(); - assertFalse(dedicatedThread.isAlive()); - } - - @Test - public void testExecuteInOwnThread() { - final Thread testingThread = Thread.currentThread(); - Long result = - DedicatedThreadExecutor.executeInOwnThread(new Callable() { - @Override - public Long call() throws Exception { - dedicatedThread = Thread.currentThread(); - assertNotSame(testingThread, dedicatedThread); - return Long.valueOf(456); - } - }); - assertEquals(456, result.longValue()); - assertFalse(dedicatedThread.isAlive()); - } - -} --- a/webapps/docs/config/engine.xml +++ a/webapps/docs/config/engine.xml @@ -102,6 +102,17 @@ name.

+ +

The number of threads this Engine will use to start + child Host elements in parallel. The special + value of 0 will result in the value of + Runtime.getRuntime().availableProcessors() being used. + Negative values will result in + Runtime.getRuntime().availableProcessors() + value being + used unless this is less than 1 in which case 1 thread will be used. If + not specified, the default value of 1 will be used.

+
+ --- a/webapps/docs/config/host.xml +++ a/webapps/docs/config/host.xml @@ -188,6 +188,19 @@ virtual host.

+ +

The number of threads this Host will use to start + child Context elements in parallel. The same + thread pool will be used to deploy new + Contexts if automatic deployment is being + used. The special value of 0 will result in the value of + Runtime.getRuntime().availableProcessors() being used. + Negative values will result in + Runtime.getRuntime().availableProcessors() + value being + used unless this is less than 1 in which case 1 thread will be used. If + not specified, the default value of 1 will be used.

+
+