// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.cache; import java.io.IOException; import java.net.URL; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.openstreetmap.josm.Main; /** * Queue for ThreadPoolExecutor that implements per-host limit. It will acquire a semaphore for each task * and it will set a runnable task with semaphore release, when job has finished. * <p> * This implementation doesn't guarantee to have at most hostLimit connections per host[1], and it doesn't * guarantee that all threads will be busy, when there is work for them[2]. <br> * [1] More connection per host may happen, when ThreadPoolExecutor is growing its pool, and thus * tasks do not go through the Queue <br> * [2] If we have a queue, and for all hosts in queue we will fail to acquire semaphore, the thread * take the first available job and wait for semaphore. It might be the case, that semaphore was released * for some task further in queue, but this implementation doesn't try to detect such situation * * @author Wiktor Niesiobędzki */ public class HostLimitQueue extends LinkedBlockingDeque<Runnable> { private static final long serialVersionUID = 1L; private final Map<String, Semaphore> hostSemaphores = new ConcurrentHashMap<>(); private final int hostLimit; private ThreadPoolExecutor executor; private int corePoolSize; private int maximumPoolSize; /** * Creates an unbounded queue * @param hostLimit how many parallel calls to host to allow */ public HostLimitQueue(int hostLimit) { super(); // create unbounded queue this.hostLimit = hostLimit; } private JCSCachedTileLoaderJob<?, ?> findJob() { for (Iterator<Runnable> it = iterator(); it.hasNext();) { Runnable r = it.next(); if (r instanceof JCSCachedTileLoaderJob) { JCSCachedTileLoaderJob<?, ?> job = (JCSCachedTileLoaderJob<?, ?>) r; if (tryAcquireSemaphore(job)) { if (remove(job)) { return job; } else { // we have acquired the semaphore, but we didn't manage to remove job, as someone else did // release the semaphore and look for another candidate releaseSemaphore(job); } } else { URL url = null; try { url = job.getUrl(); } catch (IOException e) { Main.debug(e); } Main.debug("TMS - Skipping job {0} because host limit reached", url); } } } return null; } @Override public Runnable poll(long timeout, TimeUnit unit) throws InterruptedException { Runnable job = findJob(); if (job != null) { return job; } job = pollFirst(timeout, unit); if (job != null) { try { boolean gotLock = tryAcquireSemaphore(job, timeout, unit); return gotLock ? job : null; } catch (InterruptedException e) { // acquire my got interrupted, first offer back what was taken if (!offer(job)) { Main.warn("Unable to offer back " + job); } throw e; } } return job; } @Override public Runnable take() throws InterruptedException { Runnable job = findJob(); if (job != null) { return job; } job = takeFirst(); try { acquireSemaphore(job); } catch (InterruptedException e) { // acquire my got interrupted, first offer back what was taken if (!offer(job)) { Main.warn("Unable to offer back " + job); } throw e; } return job; } /** * Set the executor for which this queue works. It's needed to spawn new threads. * See: http://stackoverflow.com/questions/9622599/java-threadpoolexecutor-strategy-direct-handoff-with-queue# * * @param executor executor for which this queue works */ public void setExecutor(ThreadPoolExecutor executor) { this.executor = executor; this.maximumPoolSize = executor.getMaximumPoolSize(); this.corePoolSize = executor.getCorePoolSize(); } @Override public boolean offer(Runnable e) { if (!super.offer(e)) { return false; } if (executor != null) { // See: http://stackoverflow.com/questions/9622599/java-threadpoolexecutor-strategy-direct-handoff-with-queue# // force spawn of a thread if not reached maximum int currentPoolSize = executor.getPoolSize(); if (currentPoolSize < maximumPoolSize && currentPoolSize >= corePoolSize) { executor.setCorePoolSize(currentPoolSize + 1); executor.setCorePoolSize(corePoolSize); } } return true; } private Semaphore getSemaphore(JCSCachedTileLoaderJob<?, ?> job) { String host; try { host = job.getUrl().getHost(); } catch (IOException e) { // do not pass me illegal URL's throw new IllegalArgumentException(e); } Semaphore limit = hostSemaphores.get(host); if (limit == null) { synchronized (hostSemaphores) { limit = hostSemaphores.get(host); if (limit == null) { limit = new Semaphore(hostLimit); hostSemaphores.put(host, limit); } } } return limit; } private void acquireSemaphore(Runnable job) throws InterruptedException { if (job instanceof JCSCachedTileLoaderJob) { final JCSCachedTileLoaderJob<?, ?> jcsJob = (JCSCachedTileLoaderJob<?, ?>) job; getSemaphore(jcsJob).acquire(); jcsJob.setFinishedTask(() -> releaseSemaphore(jcsJob)); } } private boolean tryAcquireSemaphore(final JCSCachedTileLoaderJob<?, ?> job) { boolean ret = true; Semaphore limit = getSemaphore(job); if (limit != null) { ret = limit.tryAcquire(); if (ret) { job.setFinishedTask(() -> releaseSemaphore(job)); } } return ret; } private boolean tryAcquireSemaphore(Runnable job, long timeout, TimeUnit unit) throws InterruptedException { boolean ret = true; if (job instanceof JCSCachedTileLoaderJob) { final JCSCachedTileLoaderJob<?, ?> jcsJob = (JCSCachedTileLoaderJob<?, ?>) job; Semaphore limit = getSemaphore(jcsJob); if (limit != null) { ret = limit.tryAcquire(timeout, unit); if (ret) { jcsJob.setFinishedTask(() -> releaseSemaphore(jcsJob)); } } } return ret; } private void releaseSemaphore(JCSCachedTileLoaderJob<?, ?> job) { Semaphore limit = getSemaphore(job); if (limit != null) { limit.release(); if (limit.availablePermits() > hostLimit) { Main.warn("More permits than it should be"); } } } }