/* * Copyright 2000-2016 JetBrains s.r.o. * * Licensed 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 com.intellij.util.concurrency; import com.intellij.Patches; import com.intellij.openapi.Disposable; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Disposer; import com.intellij.util.ExceptionUtil; import com.intellij.util.Function; import com.intellij.util.ObjectUtils; import com.intellij.util.ReflectionUtil; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; /** * ExecutorService which limits the number of tasks running simultaneously. * The number of submitted tasks is unrestricted. */ public class BoundedTaskExecutor extends AbstractExecutorService { private static final Logger LOG = Logger.getInstance(BoundedTaskExecutor.class); private volatile boolean myShutdown; @NotNull private final String myName; private final Executor myBackendExecutor; private final int myMaxTasks; // low 32 bits: number of tasks running (or trying to run) // high 32 bits: myTaskQueue modification stamp private final AtomicLong myStatus = new AtomicLong(); private final BlockingQueue<Runnable> myTaskQueue = new LinkedBlockingQueue<Runnable>(); public BoundedTaskExecutor(@NotNull String name, @NotNull Executor backendExecutor, int maxSimultaneousTasks) { myName = name; myBackendExecutor = backendExecutor; if (maxSimultaneousTasks < 1) { throw new IllegalArgumentException("maxSimultaneousTasks must be >=1 but got: "+maxSimultaneousTasks); } if (backendExecutor instanceof BoundedTaskExecutor) { throw new IllegalArgumentException("backendExecutor is already BoundedTaskExecutor: "+backendExecutor); } myMaxTasks = maxSimultaneousTasks; } /** * @deprecated use {@link #BoundedTaskExecutor(String, Executor, int)} instead */ public BoundedTaskExecutor(@NotNull Executor backendExecutor, int maxSimultaneousTasks) { this(ExceptionUtil.getThrowableText(new Throwable("Creation point:")), backendExecutor, maxSimultaneousTasks); } /** * Constructor which automatically shuts down this executor when {@code parent} is disposed. */ public BoundedTaskExecutor(@NotNull String name, @NotNull Executor backendExecutor, int maxSimultaneousTasks, @NotNull Disposable parent) { this(name, backendExecutor, maxSimultaneousTasks); Disposer.register(parent, new Disposable() { @Override public void dispose() { shutdownNow(); } }); } // for diagnostics static Object info(Runnable info) { Object task = info; String extra = null; if (task instanceof FutureTask) { extra = ((FutureTask)task).isCancelled() ? " (future cancelled)" : ((FutureTask)task).isDone() ? " (future done)" : null; task = ObjectUtils.chooseNotNull(ReflectionUtil.getField(task.getClass(), task, Callable.class, "callable"), task); } if (task instanceof Callable && task.getClass().getName().equals("java.util.concurrent.Executors$RunnableAdapter")) { task = ObjectUtils.chooseNotNull(ReflectionUtil.getField(task.getClass(), task, Runnable.class, "task"), task); } return extra == null ? task : task == null ? extra : task.getClass() + extra; } @Override public void shutdown() { if (myShutdown) throw new IllegalStateException("Already shutdown: "+this); myShutdown = true; } @NotNull @Override public List<Runnable> shutdownNow() { shutdown(); return clearAndCancelAll(); } @Override public boolean isShutdown() { return myShutdown; } @Override public boolean isTerminated() { return myShutdown; } // can be executed even after shutdown private static class LastTask extends FutureTask<Void> { LastTask(@NotNull Runnable runnable) { super(runnable, null); } } @Override public boolean awaitTermination(long timeout, @NotNull TimeUnit unit) throws InterruptedException { if (!isShutdown()) throw new IllegalStateException("you must call shutdown() or shutdownNow() first"); try { waitAllTasksExecuted(timeout, unit); } catch (ExecutionException e) { throw new RuntimeException(e.getCause()); } catch (TimeoutException e) { return false; } return true; } @Override public void execute(@NotNull Runnable task) { if (isShutdown() && !(task instanceof LastTask)) { throw new RejectedExecutionException("Already shutdown"); } long status = incrementCounterAndTimestamp(); // increment inProgress and queue stamp atomically int inProgress = (int)status; assert inProgress > 0 : inProgress; if (inProgress <= myMaxTasks) { // optimisation: can execute without queue/dequeue wrapAndExecute(task, status); return; } if (!myTaskQueue.offer(task)) { throw new RejectedExecutionException(); } Runnable next = pollOrGiveUp(status); if (next != null) { wrapAndExecute(next, status); } } static { assert Patches.USE_REFLECTION_TO_ACCESS_JDK8; } // todo replace with myStatus.getAndUpdate() private long incrementCounterAndTimestamp() { long status; long newStatus; do { status = myStatus.get(); // avoid "tasks number" bits to be garbled on overflow newStatus = status + 1 + (1L << 32) & 0x7fffffffffffffffL; } while (!myStatus.compareAndSet(status, newStatus)); return newStatus; } // return next task taken from the queue if it can be executed now // or decrement my counter (atomically) and return null private Runnable pollOrGiveUp(long status) { while (true) { int inProgress = (int)status; assert inProgress > 0 : inProgress; Runnable next; if (inProgress <= myMaxTasks && (next = myTaskQueue.poll()) != null) { return next; } if (myStatus.compareAndSet(status, status - 1)) { break; } status = myStatus.get(); } return null; } private void wrapAndExecute(@NotNull final Runnable firstTask, final long status) { try { final AtomicReference<Runnable> currentTask = new AtomicReference<Runnable>(firstTask); myBackendExecutor.execute(new Runnable() { @Override public void run() { // we are back inside backend executor, no need to call .execute() - just run synchronously Runnable task = currentTask.get(); do { currentTask.set(task); try { task.run(); } catch (Throwable e) { // do not lose queued tasks because of this exception try { LOG.error(e); } catch (Throwable ignored) { } } task = pollOrGiveUp(status); } while (task != null); } @Override public String toString() { return String.valueOf(info(currentTask.get())); } }); } catch (Error e) { myStatus.decrementAndGet(); throw e; } catch (RuntimeException e) { myStatus.decrementAndGet(); throw e; } } public void waitAllTasksExecuted(long timeout, @NotNull TimeUnit unit) throws ExecutionException, InterruptedException, TimeoutException { final CountDownLatch started = new CountDownLatch(myMaxTasks); final CountDownLatch readyToFinish = new CountDownLatch(1); // start myMaxTasks runnables which will spread to all available executor threads // and wait for them all to finish List<Future> futures = ContainerUtil.map(Collections.nCopies(myMaxTasks, null), new Function<Object, Future>() { @Override public Future fun(Object o) { final LastTask wait = new LastTask(new Runnable() { @Override public void run() { try { started.countDown(); readyToFinish.await(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); execute(wait); return wait; } }); try { if (!started.await(timeout, unit)) { throw new TimeoutException("Interrupted by timeout. " + this); } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { readyToFinish.countDown(); } for (Future future : futures) { future.get(timeout, unit); } } @NotNull public List<Runnable> clearAndCancelAll() { List<Runnable> queued = new ArrayList<Runnable>(); myTaskQueue.drainTo(queued); for (Runnable task : queued) { if (task instanceof FutureTask) { ((FutureTask) task).cancel(false); } } return queued; } @Override public String toString() { return "BoundedExecutor(" + myMaxTasks + ") " + (isShutdown() ? "SHUTDOWN " : "") + "inProgress: " + (int)myStatus.get() + "; " + (myTaskQueue.isEmpty() ? "" : "Queue size: "+myTaskQueue.size() +"; tasks in queue: [" + ContainerUtil.map(myTaskQueue, new Function<Runnable, Object>() { @Override public Object fun(Runnable runnable) { return info(runnable); } }) + "]") + "name: "+myName; } }