Author: pmouawad
Date: Wed Mar  9 12:23:31 2016
New Revision: 1734228

URL: http://svn.apache.org/viewvc?rev=1734228&view=rev
Log:
[Bug 52073] Embedded Resources Parallel download : Improve performances by 
avoiding shutdown of ThreadPoolExecutor at each sample
Based on PR by Benoit Wiart + the addition (blame me) of 
JMeterPoolingClientConnectionManager  (see mailing list mail I will send)
Bugzilla Id: 52073

Added:
    jmeter/trunk/src/protocol/http/org/apache/http/
    jmeter/trunk/src/protocol/http/org/apache/http/impl/
    jmeter/trunk/src/protocol/http/org/apache/http/impl/conn/
    
jmeter/trunk/src/protocol/http/org/apache/http/impl/conn/JMeterPoolingClientConnectionManager.java
   (with props)
    
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/ResourcesDownloader.java
   (with props)
Modified:
    jmeter/trunk/bin/jmeter.properties
    
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/control/CacheManager.java
    
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/HTTPHC4Impl.java
    
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/HTTPSamplerBase.java
    
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/MeasuringConnectionManager.java
    jmeter/trunk/xdocs/changes.xml

Modified: jmeter/trunk/bin/jmeter.properties
URL: 
http://svn.apache.org/viewvc/jmeter/trunk/bin/jmeter.properties?rev=1734228&r1=1734227&r2=1734228&view=diff
==============================================================================
--- jmeter/trunk/bin/jmeter.properties (original)
+++ jmeter/trunk/bin/jmeter.properties Wed Mar  9 12:23:31 2016
@@ -468,11 +468,18 @@ log_level.jorphan=INFO
 # Number of retries to attempt (default 0)
 #httpclient4.retrycount=0
 
-# Idle connection timeout (ms) to apply if the server does not send
+# Idle connection timeout (Milliseconds) to apply if the server does not send
 # Keep-Alive headers (default 0 = no Keep-Alive)
 #httpclient4.idletimeout=0
 # Note: this is currently an experimental fix
 
+# Check connections if the elapsed time (Milliseconds) since the last 
+# use of the connection exceed this value
+#httpclient4.validate_after_inactivity=2000
+
+# TTL (in Milliseconds) represents an absolute value. 
+# No matter what the connection will not be re-used beyond its TTL. 
+#httpclient4.time_to_live=2000
 #---------------------------------------------------------------------------
 # Apache HttpComponents HTTPClient configuration (HTTPClient 3.1)
 #---------------------------------------------------------------------------
@@ -992,8 +999,7 @@ beanshell.server.file=../extras/startup.
 #httpsampler.max_redirects=5
 # Maximum frame/iframe nesting depth (default 5)
 #httpsampler.max_frame_depth=5
-# Maximum await termination timeout (secs) when concurrent download embedded 
resources (default 60)
-#httpsampler.await_termination_timeout=60
+
 # Revert to BUG 51939 behaviour (no separate container for embedded resources) 
by setting the following false:
 #httpsampler.separate.container=true
 
@@ -1001,6 +1007,9 @@ beanshell.server.file=../extras/startup.
 # Parent sample will not be marked as failed 
 #httpsampler.ignore_failed_embedded_resources=false
 
+#keep alive time for the parallel download threads (in seconds)
+#httpsampler.parallel_download_thread_keepalive_inseconds=60
+
 # Don't keep the embedded resources response data : just keep the size and the 
md5
 # default to false
 #httpsampler.embedded_resources_use_md5=false

Added: 
jmeter/trunk/src/protocol/http/org/apache/http/impl/conn/JMeterPoolingClientConnectionManager.java
URL: 
http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/http/impl/conn/JMeterPoolingClientConnectionManager.java?rev=1734228&view=auto
==============================================================================
--- 
jmeter/trunk/src/protocol/http/org/apache/http/impl/conn/JMeterPoolingClientConnectionManager.java
 (added)
+++ 
jmeter/trunk/src/protocol/http/org/apache/http/impl/conn/JMeterPoolingClientConnectionManager.java
 Wed Mar  9 12:23:31 2016
@@ -0,0 +1,353 @@
+/*
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.http.impl.conn;
+
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.apache.http.annotation.ThreadSafe;
+import org.apache.http.conn.ClientConnectionManager;
+import org.apache.http.conn.ClientConnectionOperator;
+import org.apache.http.conn.ClientConnectionRequest;
+import org.apache.http.conn.ConnectionPoolTimeoutException;
+import org.apache.http.conn.DnsResolver;
+import org.apache.http.conn.ManagedClientConnection;
+import org.apache.http.conn.routing.HttpRoute;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.pool.ConnPoolControl;
+import org.apache.http.pool.PoolStats;
+import org.apache.http.util.Args;
+import org.apache.http.util.Asserts;
+
+/**
+ * Copy/Paste of {@link PoolingClientConnectionManager} with a slight 
modification 
+ * extracted from {@link PoolingHttpClientConnectionManager} to allow using 
+ * better validation mechanism introduced in 4.4
+ * TODO : Remove when full upgrade to new HttpClient 4.5.X API is finished
+ * @deprecated Will be removed in 3.1, DO NOT USE
+ */
+@Deprecated
+@ThreadSafe
+public class JMeterPoolingClientConnectionManager implements 
ClientConnectionManager, ConnPoolControl<HttpRoute> {
+
+    private static final int VALIDATE_AFTER_INACTIVITY_DEFAULT = 2000;
+
+    private final Log log = LogFactory.getLog(getClass());
+    
+    private final SchemeRegistry schemeRegistry;
+
+    private final HttpConnPool pool;
+
+    private final ClientConnectionOperator operator;
+    
+
+    /** the custom-configured DNS lookup mechanism. */
+    private final DnsResolver dnsResolver;
+
+    public JMeterPoolingClientConnectionManager(final SchemeRegistry schreg) {
+        this(schreg, -1, TimeUnit.MILLISECONDS);
+    }
+
+    public JMeterPoolingClientConnectionManager(final SchemeRegistry 
schreg,final DnsResolver dnsResolver) {
+        this(schreg, -1, TimeUnit.MILLISECONDS,dnsResolver);
+    }
+
+    public JMeterPoolingClientConnectionManager() {
+        this(SchemeRegistryFactory.createDefault());
+    }
+
+    public JMeterPoolingClientConnectionManager(
+            final SchemeRegistry schemeRegistry,
+            final long timeToLive, final TimeUnit tunit) {
+        this(schemeRegistry, timeToLive, tunit, new 
SystemDefaultDnsResolver());
+    }
+    
+    public JMeterPoolingClientConnectionManager(
+            final SchemeRegistry schemeRegistry,
+            final long timeToLive, final TimeUnit tunit,
+            final DnsResolver dnsResolver) {
+        this(schemeRegistry, timeToLive, tunit, dnsResolver, 
VALIDATE_AFTER_INACTIVITY_DEFAULT);
+    }
+
+    public JMeterPoolingClientConnectionManager(final SchemeRegistry 
schemeRegistry,
+                final long timeToLive, final TimeUnit tunit,
+                final DnsResolver dnsResolver,
+                int validateAfterInactivity) {
+        super();
+        Args.notNull(schemeRegistry, "Scheme registry");
+        Args.notNull(dnsResolver, "DNS resolver");
+        this.schemeRegistry = schemeRegistry;
+        this.dnsResolver  = dnsResolver;
+        this.operator = createConnectionOperator(schemeRegistry);
+        this.pool = new HttpConnPool(this.log, this.operator, 2, 20, 
timeToLive, tunit);
+        pool.setValidateAfterInactivity(validateAfterInactivity);
+    }
+    
+    /**
+     * @see #setValidateAfterInactivity(int)
+     *
+     * @since 4.5.2
+     */
+    public int getValidateAfterInactivity() {
+        return pool.getValidateAfterInactivity();
+    }
+
+    /**
+     * Defines period of inactivity in milliseconds after which persistent 
connections must
+     * be re-validated prior to being {@link 
#leaseConnection(java.util.concurrent.Future,
+     *   long, java.util.concurrent.TimeUnit) leased} to the consumer. 
Non-positive value passed
+     * to this method disables connection validation. This check helps detect 
connections
+     * that have become stale (half-closed) while kept inactive in the pool.
+     *
+     * @see #leaseConnection(java.util.concurrent.Future, long, 
java.util.concurrent.TimeUnit)
+     *
+     * @since 4.5.2
+     */
+    public void setValidateAfterInactivity(final int ms) {
+        pool.setValidateAfterInactivity(ms);
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            shutdown();
+        } finally {
+            super.finalize();
+        }
+    }
+
+    /**
+     * Hook for creating the connection operator.
+     * It is called by the constructor.
+     * Derived classes can override this method to change the
+     * instantiation of the operator.
+     * The default implementation here instantiates
+     * {@link DefaultClientConnectionOperator DefaultClientConnectionOperator}.
+     *
+     * @param schreg    the scheme registry.
+     *
+     * @return  the connection operator to use
+     */
+    protected ClientConnectionOperator createConnectionOperator(final 
SchemeRegistry schreg) {
+            return new DefaultClientConnectionOperator(schreg, 
this.dnsResolver);
+    }
+    @Override
+    public SchemeRegistry getSchemeRegistry() {
+        return this.schemeRegistry;
+    }
+
+    private String format(final HttpRoute route, final Object state) {
+        final StringBuilder buf = new StringBuilder();
+        buf.append("[route: ").append(route).append("]");
+        if (state != null) {
+            buf.append("[state: ").append(state).append("]");
+        }
+        return buf.toString();
+    }
+
+    private String formatStats(final HttpRoute route) {
+        final StringBuilder buf = new StringBuilder();
+        final PoolStats totals = this.pool.getTotalStats();
+        final PoolStats stats = this.pool.getStats(route);
+        buf.append("[total kept alive: 
").append(totals.getAvailable()).append("; ");
+        buf.append("route allocated: ").append(stats.getLeased() + 
stats.getAvailable());
+        buf.append(" of ").append(stats.getMax()).append("; ");
+        buf.append("total allocated: ").append(totals.getLeased() + 
totals.getAvailable());
+        buf.append(" of ").append(totals.getMax()).append("]");
+        return buf.toString();
+    }
+
+    private String format(final HttpPoolEntry entry) {
+        final StringBuilder buf = new StringBuilder();
+        buf.append("[id: ").append(entry.getId()).append("]");
+        buf.append("[route: ").append(entry.getRoute()).append("]");
+        final Object state = entry.getState();
+        if (state != null) {
+            buf.append("[state: ").append(state).append("]");
+        }
+        return buf.toString();
+    }
+
+    @Override
+    public ClientConnectionRequest requestConnection(
+            final HttpRoute route,
+            final Object state) {
+        Args.notNull(route, "HTTP route");
+        if (this.log.isDebugEnabled()) {
+            this.log.debug("Connection request: " + format(route, state) + 
formatStats(route));
+        }
+        final Future<HttpPoolEntry> future = this.pool.lease(route, state);
+
+        return new ClientConnectionRequest() {
+            @Override
+            public void abortRequest() {
+                future.cancel(true);
+            }
+            @Override
+            public ManagedClientConnection getConnection(
+                    final long timeout,
+                    final TimeUnit tunit) throws InterruptedException, 
ConnectionPoolTimeoutException {
+                return leaseConnection(future, timeout, tunit);
+            }
+
+        };
+
+    }
+
+    ManagedClientConnection leaseConnection(
+            final Future<HttpPoolEntry> future,
+            final long timeout,
+            final TimeUnit tunit) throws InterruptedException, 
ConnectionPoolTimeoutException {
+        final HttpPoolEntry entry;
+        try {
+            entry = future.get(timeout, tunit);
+            if (entry == null || future.isCancelled()) {
+                throw new InterruptedException();
+            }
+            Asserts.check(entry.getConnection() != null, "Pool entry with no 
connection");
+            if (this.log.isDebugEnabled()) {
+                this.log.debug("Connection leased: " + format(entry) + 
formatStats(entry.getRoute()));
+            }
+            return new ManagedClientConnectionImpl(this, this.operator, entry);
+        } catch (final ExecutionException ex) {
+            Throwable cause = ex.getCause();
+            if (cause == null) {
+                cause = ex;
+            }
+            this.log.error("Unexpected exception leasing connection from 
pool", cause);
+            // Should never happen
+            throw new InterruptedException();
+        } catch (final TimeoutException ex) {
+            throw new ConnectionPoolTimeoutException("Timeout waiting for 
connection from pool");
+        }
+    }
+    @Override
+    public void releaseConnection(
+            final ManagedClientConnection conn, final long keepalive, final 
TimeUnit tunit) {
+
+        Args.check(conn instanceof ManagedClientConnectionImpl, "Connection 
class mismatch, " +
+            "connection not obtained from this manager");
+        final ManagedClientConnectionImpl managedConn = 
(ManagedClientConnectionImpl) conn;
+        Asserts.check(managedConn.getManager() == this, "Connection not 
obtained from this manager");
+        synchronized (managedConn) {
+            final HttpPoolEntry entry = managedConn.detach();
+            if (entry == null) {
+                return;
+            }
+            try {
+                if (managedConn.isOpen() && !managedConn.isMarkedReusable()) {
+                    try {
+                        managedConn.shutdown();
+                    } catch (final IOException iox) {
+                        if (this.log.isDebugEnabled()) {
+                            this.log.debug("I/O exception shutting down 
released connection", iox);
+                        }
+                    }
+                }
+                // Only reusable connections can be kept alive
+                if (managedConn.isMarkedReusable()) {
+                    entry.updateExpiry(keepalive, tunit != null ? tunit : 
TimeUnit.MILLISECONDS);
+                    if (this.log.isDebugEnabled()) {
+                        final String s;
+                        if (keepalive > 0) {
+                            s = "for " + keepalive + " " + tunit;
+                        } else {
+                            s = "indefinitely";
+                        }
+                        this.log.debug("Connection " + format(entry) + " can 
be kept alive " + s);
+                    }
+                }
+            } finally {
+                this.pool.release(entry, managedConn.isMarkedReusable());
+            }
+            if (this.log.isDebugEnabled()) {
+                this.log.debug("Connection released: " + format(entry) + 
formatStats(entry.getRoute()));
+            }
+        }
+    }
+    @Override
+    public void shutdown() {
+        this.log.debug("Connection manager is shutting down");
+        try {
+            this.pool.shutdown();
+        } catch (final IOException ex) {
+            this.log.debug("I/O exception shutting down connection manager", 
ex);
+        }
+        this.log.debug("Connection manager shut down");
+    }
+    @Override
+    public void closeIdleConnections(final long idleTimeout, final TimeUnit 
tunit) {
+        if (this.log.isDebugEnabled()) {
+            this.log.debug("Closing connections idle longer than " + 
idleTimeout + " " + tunit);
+        }
+        this.pool.closeIdle(idleTimeout, tunit);
+    }
+    @Override
+    public void closeExpiredConnections() {
+        this.log.debug("Closing expired connections");
+        this.pool.closeExpired();
+    }
+    @Override
+    public int getMaxTotal() {
+        return this.pool.getMaxTotal();
+    }
+    @Override
+    public void setMaxTotal(final int max) {
+        this.pool.setMaxTotal(max);
+    }
+    @Override
+    public int getDefaultMaxPerRoute() {
+        return this.pool.getDefaultMaxPerRoute();
+    }
+    @Override
+    public void setDefaultMaxPerRoute(final int max) {
+        this.pool.setDefaultMaxPerRoute(max);
+    }
+    @Override
+    public int getMaxPerRoute(final HttpRoute route) {
+        return this.pool.getMaxPerRoute(route);
+    }
+    @Override
+    public void setMaxPerRoute(final HttpRoute route, final int max) {
+        this.pool.setMaxPerRoute(route, max);
+    }
+    @Override
+    public PoolStats getTotalStats() {
+        return this.pool.getTotalStats();
+    }
+    @Override
+    public PoolStats getStats(final HttpRoute route) {
+        return this.pool.getStats(route);
+    }
+
+}
+

Propchange: 
jmeter/trunk/src/protocol/http/org/apache/http/impl/conn/JMeterPoolingClientConnectionManager.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/control/CacheManager.java
URL: 
http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/control/CacheManager.java?rev=1734228&r1=1734227&r2=1734228&view=diff
==============================================================================
--- 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/control/CacheManager.java
 (original)
+++ 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/control/CacheManager.java
 Wed Mar  9 12:23:31 2016
@@ -16,8 +16,6 @@
  *
  */
 
-// For unit tests @see TestCookieManager
-
 package org.apache.jmeter.protocol.http.control;
 
 import java.io.Serializable;
@@ -77,6 +75,11 @@ public class CacheManager extends Config
     private static final int DEFAULT_MAX_SIZE = 5000;
 
     private static final long ONE_YEAR_MS = 365*24*60*60*1000L;
+    
+    /** used to share the cache between 2 cache managers
+     * @see CacheManager#createCacheManagerProxy() 
+     * @since 3.0 */
+    private transient Map<String, CacheEntry> localCache;
 
     public CacheManager() {
         setProperty(new BooleanProperty(CLEAR, false));
@@ -84,6 +87,11 @@ public class CacheManager extends Config
         clearCache();
         useExpires = false;
     }
+    
+    CacheManager(Map<String, CacheEntry> localCache, boolean useExpires) {
+        this.localCache = localCache;
+        this.useExpires = useExpires;
+    }
 
     /*
      * Holder for storing cache details.
@@ -225,7 +233,7 @@ public class CacheManager extends Config
                 }
             }
             // if no-cache is present, ensure that expiresDate remains null, 
which forces revalidation
-            if(cacheControl != null && !cacheControl.contains("no-cache")) {   
 
+            if(cacheControl != null && !cacheControl.contains("no-cache")) {
                 // the max-age directive overrides the Expires header,
                 if(cacheControl.contains(MAX_AGE)) {
                     long maxAgeInSecs = Long.parseLong(
@@ -255,11 +263,11 @@ public class CacheManager extends Config
                                     + date);
                             }
                             // TODO Can't see anything in SPEC
-                            expiresDate = new 
Date(System.currentTimeMillis()+ONE_YEAR_MS);                      
+                            expiresDate = new 
Date(System.currentTimeMillis()+ONE_YEAR_MS);
                         }
                     } else {
                         // TODO Can't see anything in SPEC
-                        expiresDate = new 
Date(System.currentTimeMillis()+ONE_YEAR_MS);                      
+                        expiresDate = new 
Date(System.currentTimeMillis()+ONE_YEAR_MS);
                     }
                 }  
                 // else expiresDate computed in (expires!=null) condition is 
used
@@ -418,8 +426,8 @@ public class CacheManager extends Config
         return false;
     }
 
-    private Map<String, CacheEntry> getCache(){
-        return threadCache.get();
+    private Map<String, CacheEntry> getCache() {
+        return localCache != null?localCache:threadCache.get();
     }
 
     public boolean getClearEachIteration() {
@@ -471,6 +479,17 @@ public class CacheManager extends Config
             }
         };
     }
+    
+    /**
+     * create a cache manager that share the underlying cache of the current 
one
+     * it allows to use the same cache in different threads which does not 
inherit from each other
+     * @return a cache manager that share the underlying cache of the current 
one
+     * @since 3.0
+     */
+    public CacheManager createCacheManagerProxy() {
+        CacheManager cm = new CacheManager(getCache(), this.useExpires);
+        return cm;
+    }
 
     @Override
     public void testStarted() {
@@ -493,7 +512,7 @@ public class CacheManager extends Config
         if (getClearEachIteration()) {
             clearCache();
         }
-        useExpires=getUseExpires(); // cache the value
+        useExpires = getUseExpires(); // cache the value
     }
 
 }

Modified: 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/HTTPHC4Impl.java
URL: 
http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/HTTPHC4Impl.java?rev=1734228&r1=1734227&r2=1734228&view=diff
==============================================================================
--- 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/HTTPHC4Impl.java
 (original)
+++ 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/HTTPHC4Impl.java
 Wed Mar  9 12:23:31 2016
@@ -140,6 +140,10 @@ public class HTTPHC4Impl extends HTTPHCA
 
     /** Idle timeout to be applied to connections if no Keep-Alive header is 
sent by the server (default 0 = disable) */
     private static final int IDLE_TIMEOUT = 
JMeterUtils.getPropDefault("httpclient4.idletimeout", 0);
+    
+    private static final int VALIDITY_AFTER_INACTIVITY_TIMEOUT = 
JMeterUtils.getPropDefault("httpclient4.validate_after_inactivity", 2000);
+    
+    private static final int TIME_TO_LIVE = 
JMeterUtils.getPropDefault("httpclient4.time_to_live", 2000);
 
     private static final String CONTEXT_METRICS = "jmeter_metrics"; // TODO 
hack for metrics related to HTTPCLIENT-1081, to be removed later
 
@@ -204,6 +208,8 @@ public class HTTPHC4Impl extends HTTPHCA
     private static final String USER_TOKEN = "__jmeter.USER_TOKEN__"; 
//$NON-NLS-1$
     
     static final String SAMPLER_RESULT_TOKEN = "__jmeter.SAMPLER_RESULT__"; 
//$NON-NLS-1$
+    
+    private static final String HTTPCLIENT_TOKEN = 
"__jmeter.HTTPCLIENT_TOKEN__";
 
     static {
         log.info("HTTP request retry count = "+RETRY_COUNT);
@@ -420,6 +426,7 @@ public class HTTPHC4Impl extends HTTPHCA
             return res;
         } finally {
             currentRequest = null;
+            
JMeterContextService.getContext().getSamplerContext().remove(HTTPCLIENT_TOKEN);
         }
         return res;
     }
@@ -686,7 +693,14 @@ public class HTTPHC4Impl extends HTTPHCA
         // Lookup key - must agree with all the values used to create the 
HttpClient.
         HttpClientKey key = new HttpClientKey(url, useProxy, proxyHost, 
proxyPort, proxyUser, proxyPass);
         
-        HttpClient httpClient = mapHttpClientPerHttpClientKey.get(key);
+        HttpClient httpClient = null;
+        if(this.testElement.isConcurrentDwn()) {
+            httpClient = (HttpClient) 
JMeterContextService.getContext().getSamplerContext().get(HTTPCLIENT_TOKEN);
+        }
+        
+        if (httpClient == null) {
+            httpClient = mapHttpClientPerHttpClientKey.get(key);
+        }
 
         if (httpClient != null && resetSSLContext && 
HTTPConstants.PROTOCOL_HTTPS.equalsIgnoreCase(url.getProtocol())) {
             ((AbstractHttpClient) httpClient).clearRequestInterceptors(); 
@@ -706,7 +720,11 @@ public class HTTPHC4Impl extends HTTPHCA
             if (resolver == null) {
                 resolver = SystemDefaultDnsResolver.INSTANCE;
             }
-            MeasuringConnectionManager connManager = new 
MeasuringConnectionManager(createSchemeRegistry(), resolver);
+            MeasuringConnectionManager connManager = new 
MeasuringConnectionManager(
+                    createSchemeRegistry(), 
+                    resolver, 
+                    TIME_TO_LIVE,
+                    VALIDITY_AFTER_INACTIVITY_TIMEOUT);
             
             // Modern browsers use more connections per host than the current 
httpclient default (2)
             // when using parallel download the httpclient and connection 
manager are shared by the downloads threads
@@ -770,6 +788,10 @@ public class HTTPHC4Impl extends HTTPHCA
             }
         }
 
+        if(this.testElement.isConcurrentDwn()) {
+            
JMeterContextService.getContext().getSamplerContext().put(HTTPCLIENT_TOKEN, 
httpClient);
+        }
+
         // TODO - should this be done when the client is created?
         // If so, then the details need to be added as part of HttpClientKey
         setConnectionAuthorization(httpClient, url, getAuthManager(), key);

Modified: 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/HTTPSamplerBase.java
URL: 
http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/HTTPSamplerBase.java?rev=1734228&r1=1734227&r2=1734228&view=diff
==============================================================================
--- 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/HTTPSamplerBase.java
 (original)
+++ 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/HTTPSamplerBase.java
 Wed Mar  9 12:23:31 2016
@@ -38,11 +38,6 @@ import java.util.Set;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.ThreadFactory;
-import java.util.concurrent.ThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
 
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -59,6 +54,7 @@ import org.apache.jmeter.protocol.http.c
 import org.apache.jmeter.protocol.http.parser.BaseParser;
 import org.apache.jmeter.protocol.http.parser.LinkExtractorParseException;
 import org.apache.jmeter.protocol.http.parser.LinkExtractorParser;
+import 
org.apache.jmeter.protocol.http.sampler.ResourcesDownloader.AsynSamplerResultHolder;
 import org.apache.jmeter.protocol.http.util.ConversionUtils;
 import org.apache.jmeter.protocol.http.util.EncoderCache;
 import org.apache.jmeter.protocol.http.util.HTTPArgument;
@@ -191,11 +187,6 @@ public abstract class HTTPSamplerBase ex
 
     public static final boolean BROWSER_COMPATIBLE_MULTIPART_MODE_DEFAULT = 
false; // The default setting to be used (i.e. historic)
     
-    private static final long KEEPALIVETIME = 0; // for Thread Pool for 
resources but no need to use a special value?
-    
-    private static final long AWAIT_TERMINATION_TIMEOUT = 
-        JMeterUtils.getPropDefault("httpsampler.await_termination_timeout", 
60); // $NON-NLS-1$ // default value: 60 secs 
-    
     private static final boolean IGNORE_FAILED_EMBEDDED_RESOURCES = 
             
JMeterUtils.getPropDefault("httpsampler.ignore_failed_embedded_resources", 
false); // $NON-NLS-1$ // default value: false
 
@@ -1237,7 +1228,6 @@ public abstract class HTTPSamplerBase ex
             int maxConcurrentDownloads = CONCURRENT_POOL_SIZE; // init with 
default value
             boolean isConcurrentDwn = isConcurrentDwn();
             if(isConcurrentDwn) {
-                
                 try {
                     maxConcurrentDownloads = 
Integer.parseInt(getConcurrentPool());
                 } catch (NumberFormatException nfe) {
@@ -1261,7 +1251,7 @@ public abstract class HTTPSamplerBase ex
                         log.warn("Null URL detected (should not happen)");
                     } else {
                         String urlstr = url.toString();
-                        String 
urlStrEnc=escapeIllegalURLCharacters(encodeSpaces(urlstr));
+                        String urlStrEnc = 
escapeIllegalURLCharacters(encodeSpaces(urlstr));
                         if (!urlstr.equals(urlStrEnc)){// There were some 
spaces in the URL
                             try {
                                 url = new URL(urlStrEnc);
@@ -1282,6 +1272,7 @@ public abstract class HTTPSamplerBase ex
                             setParentSampleSuccess(res, false);
                             continue;
                         }
+
                         if (isConcurrentDwn) {
                             // if concurrent download emb. resources, add to a 
list for async gets later
                             list.add(new ASyncSample(url, HTTPConstants.GET, 
false, frameDepth + 1, getCookieManager(), this));
@@ -1302,67 +1293,31 @@ public abstract class HTTPSamplerBase ex
             // IF for download concurrent embedded resources
             if (isConcurrentDwn && !list.isEmpty()) {
 
-                final String parentThreadName = 
Thread.currentThread().getName();
-                // Thread pool Executor to get resources 
-                // use a LinkedBlockingQueue, note: max pool size doesn't 
effect
-                final ThreadPoolExecutor exec = new ThreadPoolExecutor(
-                        maxConcurrentDownloads, maxConcurrentDownloads, 
KEEPALIVETIME, TimeUnit.SECONDS,
-                        new LinkedBlockingQueue<Runnable>(),
-                        new ThreadFactory() {
-                            @Override
-                            public Thread newThread(final Runnable r) {
-                                Thread t = new CleanerThread(new Runnable() {
-                                    @Override
-                                    public void run() {
-                                        try {
-                                            r.run();
-                                        } finally {
-                                            
((CleanerThread)Thread.currentThread()).notifyThreadEnd();
-                                        }
-                                    }
-                                });
-                                t.setName(parentThreadName+"-ResDownload-" + 
t.getName()); //$NON-NLS-1$
-                                t.setDaemon(true);
-                                return t;
-                            }
-                        });
+                ResourcesDownloader resourcesDownloader = 
ResourcesDownloader.getInstance();
 
-                boolean tasksCompleted = false;
                 try {
-                    // sample all resources with threadpool
-                    final List<Future<AsynSamplerResultHolder>> retExec = 
exec.invokeAll(list);
-                    // call normal shutdown (wait ending all tasks)
-                    exec.shutdown();
-                    // put a timeout if tasks couldn't terminate
-                    exec.awaitTermination(AWAIT_TERMINATION_TIMEOUT, 
TimeUnit.SECONDS);
+                    // sample all resources
+                    final List<Future<AsynSamplerResultHolder>> retExec = 
resourcesDownloader.invokeAllAndAwaitTermination(maxConcurrentDownloads, list);
                     CookieManager cookieManager = getCookieManager();
                     // add result to main sampleResult
                     for (Future<AsynSamplerResultHolder> future : retExec) {
-                        AsynSamplerResultHolder binRes;
-                        try {
-                            binRes = future.get(1, TimeUnit.MILLISECONDS);
-                            if(cookieManager != null) {
-                                CollectionProperty cookies = 
binRes.getCookies();
-                                for (JMeterProperty jMeterProperty : cookies) {
-                                    Cookie cookie = (Cookie) 
jMeterProperty.getObjectValue();
-                                    cookieManager.add(cookie);
-                                }
+                        // this call will not block as the futures return by 
invokeAllAndAwaitTermination 
+                        //   are either done or cancelled
+                        AsynSamplerResultHolder binRes = future.get();
+                        if(cookieManager != null) {
+                            CollectionProperty cookies = binRes.getCookies();
+                            for (JMeterProperty jMeterProperty : cookies) {
+                                Cookie cookie = (Cookie) 
jMeterProperty.getObjectValue();
+                                cookieManager.add(cookie);
                             }
-                            res.addSubResult(binRes.getResult());
-                            setParentSampleSuccess(res, res.isSuccessful() && 
(binRes.getResult() != null ? binRes.getResult().isSuccessful():true));
-                        } catch (TimeoutException e) {
-                            errorResult(e, res);
                         }
+                        res.addSubResult(binRes.getResult());
+                        setParentSampleSuccess(res, res.isSuccessful() && 
(binRes.getResult() != null ? binRes.getResult().isSuccessful():true));
                     }
-                    tasksCompleted = exec.awaitTermination(1, 
TimeUnit.MILLISECONDS); // did all the tasks finish?
                 } catch (InterruptedException ie) {
                     log.warn("Interrupted fetching embedded resources", ie); 
// $NON-NLS-1$
                 } catch (ExecutionException ee) {
                     log.warn("Execution issue when fetching embedded 
resources", ee); // $NON-NLS-1$
-                } finally {
-                    if (!tasksCompleted) {
-                        exec.shutdownNow(); // kill any remaining tasks
-                    }
                 }
             }
         }
@@ -1972,7 +1927,7 @@ public abstract class HTTPSamplerBase ex
             // We don't want to use CacheManager clone but the parent one, and 
CacheManager is Thread Safe
             CacheManager cacheManager = base.getCacheManager();
             if (cacheManager != null) {
-                this.sampler.setCacheManagerProperty(cacheManager);
+                
this.sampler.setCacheManagerProperty(cacheManager.createCacheManagerProxy());
             }
             
             if(cookieManager != null) {
@@ -1996,67 +1951,6 @@ public abstract class HTTPSamplerBase ex
         }
     }
     
-    /**
-     * Custom thread implementation that allows notification of threadEnd
-     *
-     */
-    private static class CleanerThread extends Thread {
-        private final List<HTTPSamplerBase> samplersToNotify = new 
ArrayList<>();
-        /**
-         * @param runnable Runnable
-         */
-        public CleanerThread(Runnable runnable) {
-           super(runnable);
-        }
-        
-        /**
-         * Notify of thread end
-         */
-        public void notifyThreadEnd() {
-            for (HTTPSamplerBase samplerBase : samplersToNotify) {
-                samplerBase.threadFinished();
-            }
-            samplersToNotify.clear();
-        }
-
-        /**
-         * Register sampler to be notify at end of thread
-         * @param sampler {@link HTTPSamplerBase}
-         */
-        public void registerSamplerForEndNotification(HTTPSamplerBase sampler) 
{
-            this.samplersToNotify.add(sampler);
-        }
-    }
-    
-    /**
-     * Holder of AsynSampler result
-     */
-    private static class AsynSamplerResultHolder {
-        private final HTTPSampleResult result;
-        private final CollectionProperty cookies;
-        /**
-         * @param result {@link HTTPSampleResult} to hold
-         * @param cookies cookies to hold
-         */
-        public AsynSamplerResultHolder(HTTPSampleResult result, 
CollectionProperty cookies) {
-            super();
-            this.result = result;
-            this.cookies = cookies;
-        }
-        /**
-         * @return the result
-         */
-        public HTTPSampleResult getResult() {
-            return result;
-        }
-        /**
-         * @return the cookies
-         */
-        public CollectionProperty getCookies() {
-            return cookies;
-        }
-    }
-    
     /**
      * @see 
org.apache.jmeter.samplers.AbstractSampler#applies(org.apache.jmeter.config.ConfigTestElement)
      */

Modified: 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/MeasuringConnectionManager.java
URL: 
http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/MeasuringConnectionManager.java?rev=1734228&r1=1734227&r2=1734228&view=diff
==============================================================================
--- 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/MeasuringConnectionManager.java
 (original)
+++ 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/MeasuringConnectionManager.java
 Wed Mar  9 12:23:31 2016
@@ -38,6 +38,7 @@ import org.apache.http.conn.DnsResolver;
 import org.apache.http.conn.ManagedClientConnection;
 import org.apache.http.conn.routing.HttpRoute;
 import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.impl.conn.JMeterPoolingClientConnectionManager;
 import org.apache.http.impl.conn.PoolingClientConnectionManager;
 import org.apache.http.params.HttpParams;
 import org.apache.http.protocol.HttpContext;
@@ -48,10 +49,14 @@ import org.apache.jmeter.samplers.Sample
  * that wraps all connection requests into time-measured implementation a 
private
  * MeasuringConnectionRequest
  */
-public class MeasuringConnectionManager extends PoolingClientConnectionManager 
{
+public class MeasuringConnectionManager extends 
JMeterPoolingClientConnectionManager {
 
-    public MeasuringConnectionManager(SchemeRegistry schemeRegistry, 
DnsResolver resolver) {
-        super(schemeRegistry, resolver);
+    
+    public MeasuringConnectionManager(SchemeRegistry schemeRegistry, 
+            DnsResolver resolver, 
+            int timeToLiveMs,
+            int validateAfterInactivityMs) {
+        super(schemeRegistry, timeToLiveMs, TimeUnit.MILLISECONDS, resolver, 
validateAfterInactivityMs);
     }
 
     @Override

Added: 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/ResourcesDownloader.java
URL: 
http://svn.apache.org/viewvc/jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/ResourcesDownloader.java?rev=1734228&view=auto
==============================================================================
--- 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/ResourcesDownloader.java
 (added)
+++ 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/ResourcesDownloader.java
 Wed Mar  9 12:23:31 2016
@@ -0,0 +1,245 @@
+/*
+ * 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.jmeter.protocol.http.sampler;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionService;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.Future;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.jmeter.testelement.property.CollectionProperty;
+import org.apache.jmeter.util.JMeterUtils;
+import org.apache.jorphan.logging.LoggingManager;
+import org.apache.log.Logger;
+
+/**
+ * Manages the parallel http resources download.<br>
+ * A shared thread pool is used by all the sample.<br>
+ * A sampler will usually do the following
+ * <pre> {@code 
+ *   // list of AsynSamplerResultHolder to download
+ *   List<Callable<AsynSamplerResultHolder>> list = ...
+ *   
+ *   // max parallel downloads
+ *   int maxConcurrentDownloads = ...
+ *   
+ *   // get the singleton instance
+ *   ResourcesDownloader resourcesDownloader = 
ResourcesDownloader.getInstance();
+ *   
+ *   // schedule the downloads and wait for the completion
+ *   List<Future<AsynSamplerResultHolder>> retExec = 
resourcesDownloader.invokeAllAndAwaitTermination(maxConcurrentDownloads, list);
+ *   
+ * }</pre>
+ * 
+ * the call to invokeAllAndAwaitTermination will block until the downloads 
complete or get interrupted<br>
+ * the Future list only contains task that have been scheduled in the 
threadpool.<br>
+ * The status of those futures are either done or cancelled<br>
+ * <br>
+ *  
+ *  Future enhancements :
+ *  <ul>
+ *  <li>this implementation should be replaced with a NIO async download
+ *   in order to reduce the number of threads needed</li>
+ *  </ul>
+ * @since 3.0
+ */
+public class ResourcesDownloader {
+
+    private static final Logger LOG = LoggingManager.getLoggerForClass();
+    
+    /** this is the maximum time that excess idle threads will wait for new 
tasks before terminating */
+    private static final long THREAD_KEEP_ALIVE_TIME = 
JMeterUtils.getPropDefault("httpsampler.parallel_download_thread_keepalive_inseconds",
 60L);
+    
+    private static final int MIN_POOL_SIZE = 1;
+    private static final int MAX_POOL_SIZE = Integer.MAX_VALUE;
+    
+    private static final ResourcesDownloader INSTANCE = new 
ResourcesDownloader();
+    
+    public static ResourcesDownloader getInstance() {
+        return INSTANCE;
+    }
+    
+    
+    private ThreadPoolExecutor concurrentExecutor = null;
+
+    private ResourcesDownloader() {
+        init();
+    }
+    
+    
+    private void init() {
+        LOG.info("Creating ResourcesDownloader with 
keepalive_inseconds:"+THREAD_KEEP_ALIVE_TIME);
+        ThreadPoolExecutor exec = new ThreadPoolExecutor(
+                MIN_POOL_SIZE, MAX_POOL_SIZE, THREAD_KEEP_ALIVE_TIME, 
TimeUnit.SECONDS,
+                new SynchronousQueue<Runnable>(),
+                new ThreadFactory() {
+                    @Override
+                    public Thread newThread(final Runnable r) {
+                        Thread t = new Thread(r);
+                        t.setName("ResDownload-" + t.getName()); //$NON-NLS-1$
+                        t.setDaemon(true);
+                        return t;
+                    }
+                }) {
+
+        };
+        concurrentExecutor = exec;
+    }
+    
+    /**
+     * this method will try to shrink the thread pool size as much as possible
+     * it should be called at the end of a test
+     */
+    public void shrink() {
+        if(concurrentExecutor.getPoolSize() > MIN_POOL_SIZE) {
+            // drain the queue
+            concurrentExecutor.purge();
+            List<Runnable> drainList = new ArrayList<>();
+            concurrentExecutor.getQueue().drainTo(drainList);
+            if(!drainList.isEmpty()) {
+                LOG.warn("the pool executor workqueue is not empty size=" + 
drainList.size());
+                for (Runnable runnable : drainList) {
+                    if(runnable instanceof Future<?>) {
+                        Future<?> f = (Future<?>) runnable;
+                        f.cancel(true);
+                    }
+                    else {
+                        LOG.warn("Content of workqueue is not an instance of 
Future");
+                    }
+                }
+            }
+            
+            // this will force the release of the extra threads that are idle
+            // the remaining extra threads will be released with the 
keepAliveTime of the thread
+            concurrentExecutor.setMaximumPoolSize(MIN_POOL_SIZE);
+            
+            // do not immediately restore the MaximumPoolSize as it will block 
the release of the threads
+        }
+    }
+    
+    // probablyTheBestMethodNameInTheUniverseYeah!
+    /**
+     * This method will block until the downloads complete or it get 
interrupted
+     * the Future list returned by this method only contains tasks that have 
been scheduled in the threadpool.<br>
+     * The status of those futures are either done or cancelled
+     * 
+     * @param maxConcurrentDownloads max concurrent downloads
+     * @param list list of resources to download
+     * @return list tasks that have been scheduled
+     * @throws InterruptedException
+     */
+    public List<Future<AsynSamplerResultHolder>> 
invokeAllAndAwaitTermination(int maxConcurrentDownloads, 
List<Callable<AsynSamplerResultHolder>> list) throws InterruptedException {
+        List<Future<AsynSamplerResultHolder>> submittedTasks = new 
ArrayList<>();
+        
+        // paranoid fast path
+        if(list.isEmpty()) {
+            return submittedTasks;
+        }
+        
+        // restore MaximumPoolSize original value
+        concurrentExecutor.setMaximumPoolSize(MAX_POOL_SIZE);
+        
+        if(LOG.isDebugEnabled()) {
+            LOG.debug("PoolSize=" + concurrentExecutor.getPoolSize()+" 
LargestPoolSize=" + concurrentExecutor.getLargestPoolSize());
+        }
+        
+        CompletionService<AsynSamplerResultHolder> completionService = new 
ExecutorCompletionService<>(concurrentExecutor);
+        int remainingTasksToTake = list.size();
+        
+        try {
+            // push the task in the threadpool until <maxConcurrentDownloads> 
is reached
+            int i = 0;
+            for (i = 0; i < Math.min(maxConcurrentDownloads, list.size()); 
i++) {
+                Callable<AsynSamplerResultHolder> task = list.get(i);
+                submittedTasks.add(completionService.submit(task));
+            }
+            
+            // push the remaining tasks but ensure we use at most 
<maxConcurrentDownloads> threads
+            // wait for a previous download to finish before submitting a new 
one
+            for (; i < list.size(); i++) {
+                Callable<AsynSamplerResultHolder> task = list.get(i);
+                completionService.take();
+                remainingTasksToTake--;
+                submittedTasks.add(completionService.submit(task));
+            }
+            
+            // all the resources downloads are in the thread pool queue
+            // wait for the completion of all downloads
+            while (remainingTasksToTake > 0) {
+                completionService.take();
+                remainingTasksToTake--;
+            }
+        }
+        finally {
+            //bug 51925 : Calling Stop on Test leaks executor threads when 
concurrent download of resources is on
+            if(remainingTasksToTake > 0) {
+                if(LOG.isDebugEnabled()) {
+                    LOG.debug("Interrupted while waiting for resource 
downloads : cancelling remaining tasks");
+                }
+                for (Future<AsynSamplerResultHolder> future : submittedTasks) {
+                    if(!future.isDone()) {
+                        future.cancel(true);
+                    }
+                }
+            }
+        }
+        
+        return submittedTasks;
+    }
+    
+    
+    /**
+     * Holder of AsynSampler result
+     */
+    public static class AsynSamplerResultHolder {
+        private final HTTPSampleResult result;
+        private final CollectionProperty cookies;
+        
+        /**
+         * @param result {@link HTTPSampleResult} to hold
+         * @param cookies cookies to hold
+         */
+        public AsynSamplerResultHolder(HTTPSampleResult result, 
CollectionProperty cookies) {
+            super();
+            this.result = result;
+            this.cookies = cookies;
+        }
+        
+        /**
+         * @return the result
+         */
+        public HTTPSampleResult getResult() {
+            return result;
+        }
+        
+        /**
+         * @return the cookies
+         */
+        public CollectionProperty getCookies() {
+            return cookies;
+        }
+    }
+    
+}

Propchange: 
jmeter/trunk/src/protocol/http/org/apache/jmeter/protocol/http/sampler/ResourcesDownloader.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: jmeter/trunk/xdocs/changes.xml
URL: 
http://svn.apache.org/viewvc/jmeter/trunk/xdocs/changes.xml?rev=1734228&r1=1734227&r2=1734228&view=diff
==============================================================================
--- jmeter/trunk/xdocs/changes.xml (original)
+++ jmeter/trunk/xdocs/changes.xml Wed Mar  9 12:23:31 2016
@@ -146,6 +146,7 @@ Summary
     <li><bug>59129</bug>HTTP Request : Simplify GUI with simple/advanced 
Tabs</li>
     <li><bug>59033</bug>Parallel Download : Rework Parser classes hierarchy to 
allow pluging parsers for different mime types</li>
     <li><bug>59146</bug>MeasuringConnectionManager is not Thread Safe (nightly 
before 3.0)</li>
+    <li><bug>52073</bug>Embedded Resources Parallel download : Improve 
performances by avoiding shutdown of ThreadPoolExecutor at each sample </li>
 </ul>
 
 <h3>Other samplers</h3>


Reply via email to