/* * 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.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.apache.jmeter.testelement.property.CollectionProperty; import org.apache.jmeter.util.JMeterUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 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 = LoggerFactory.getLogger(ResourcesDownloader.class); /** 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); concurrentExecutor = new ThreadPoolExecutor( MIN_POOL_SIZE, MAX_POOL_SIZE, THREAD_KEEP_ALIVE_TIME, TimeUnit.SECONDS, new SynchronousQueue<>(), r -> { Thread t = new Thread(r); t.setName("ResDownload-" + t.getName()); //$NON-NLS-1$ t.setDaemon(true); return t; }) { }; } /** * 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 when interrupted while waiting */ 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={} LargestPoolSize={}", concurrentExecutor.getPoolSize(), 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) { 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; } } }