/* * 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.openapi.diagnostic.Logger; import com.intellij.openapi.util.Condition; import com.intellij.util.IncorrectOperationException; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; /** * Makes a {@link ScheduledExecutorService} from the supplied plain, non-scheduling {@link ExecutorService} by awaiting scheduled tasks in a separate thread * and then passing them for execution to the {@code backendExecutorService}. * Unlike the existing {@link ScheduledThreadPoolExecutor}, this pool can be unbounded if the {@code backendExecutorService} is. */ class SchedulingWrapper implements ScheduledExecutorService { private static final Logger LOG = Logger.getInstance("#com.intellij.util.concurrency.SchedulingWrapper"); private final AtomicBoolean shutdown = new AtomicBoolean(); @NotNull final ExecutorService backendExecutorService; final AppDelayQueue delayQueue; SchedulingWrapper(@NotNull final ExecutorService backendExecutorService, @NotNull AppDelayQueue delayQueue) { this.delayQueue = delayQueue; if (backendExecutorService instanceof ScheduledExecutorService) { throw new IllegalArgumentException("backendExecutorService: "+backendExecutorService+" is already ScheduledExecutorService"); } this.backendExecutorService = backendExecutorService; } @NotNull @Override public List<Runnable> shutdownNow() { return doShutdownNow(); } @Override public void shutdown() { doShutdown(); } void doShutdown() { if (!shutdown.compareAndSet(false, true)) { throw new IllegalStateException("Already shutdown"); } } @NotNull List<Runnable> doShutdownNow() { doShutdown(); // shutdown me first to avoid further delayQueue offers List<MyScheduledFutureTask> result = ContainerUtil.filter(delayQueue, new Condition<MyScheduledFutureTask>() { @Override public boolean value(MyScheduledFutureTask task) { if (task.getBackendExecutorService() == backendExecutorService) { task.cancel(false); return true; } return false; } }); delayQueue.removeAll(new HashSet<MyScheduledFutureTask>(result)); if (LOG.isTraceEnabled()) { LOG.trace("Shutdown. Drained tasks: "+result); } //noinspection unchecked return (List)result; } @Override public boolean isShutdown() { return shutdown.get(); } @Override public boolean isTerminated() { return isShutdown(); } @Override public boolean awaitTermination(long timeout, @NotNull TimeUnit unit) throws InterruptedException { if (!isShutdown()) throw new IllegalStateException("must await termination after shutdown() or shutdownNow() only"); List<MyScheduledFutureTask> tasks = new ArrayList<MyScheduledFutureTask>(delayQueue); for (MyScheduledFutureTask task : tasks) { if (task.getBackendExecutorService() != backendExecutorService) { continue; } try { task.get(timeout, unit); } catch (ExecutionException ignored) { } catch (TimeoutException e) { return false; } } return backendExecutorService.awaitTermination(timeout, unit); } class MyScheduledFutureTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V> { /** * Sequence number to break ties FIFO */ private final long sequenceNumber; /** * The time the task is enabled to execute in nanoTime units */ private long time; /** * Period in nanoseconds for repeating tasks. A positive * value indicates fixed-rate execution. A negative value * indicates fixed-delay execution. A value of 0 indicates a * non-repeating task. */ private final long period; /** * Creates a one-shot action with given nanoTime-based trigger time. */ private MyScheduledFutureTask(@NotNull Runnable r, V result, long ns) { super(r, result); time = ns; period = 0; sequenceNumber = sequencer.getAndIncrement(); } /** * Creates a periodic action with given nano time and period. */ private MyScheduledFutureTask(@NotNull Runnable r, V result, long ns, long period) { super(r, result); time = ns; this.period = period; sequenceNumber = sequencer.getAndIncrement(); } /** * Creates a one-shot action with given nanoTime-based trigger time. */ private MyScheduledFutureTask(@NotNull Callable<V> callable, long ns) { super(callable); time = ns; period = 0; sequenceNumber = sequencer.getAndIncrement(); } @Override public boolean cancel(boolean mayInterruptIfRunning) { boolean canceled = super.cancel(mayInterruptIfRunning); delayQueue.remove(this); return canceled; } @Override public long getDelay(@NotNull TimeUnit unit) { return unit.convert(time - now(), TimeUnit.NANOSECONDS); } @Override public int compareTo(@NotNull Delayed other) { if (other == this) { return 0; } if (other instanceof MyScheduledFutureTask) { MyScheduledFutureTask<?> x = (MyScheduledFutureTask<?>)other; long diff = time - x.time; if (diff < 0) { return -1; } if (diff > 0) { return 1; } if (sequenceNumber < x.sequenceNumber) { return -1; } return 1; } long diff = getDelay(TimeUnit.NANOSECONDS) - other.getDelay(TimeUnit.NANOSECONDS); return diff < 0 ? -1 : diff > 0 ? 1 : 0; } /** * Returns {@code true} if this is a periodic (not a one-shot) action. * * @return {@code true} if periodic */ @Override public boolean isPeriodic() { return period != 0; } /** * Sets the next time to run for a periodic task. */ private void setNextRunTime() { long p = period; if (p > 0) { time += p; } else { time = triggerTime(delayQueue, -p); } } /** * Overrides FutureTask version so as to reset/requeue if periodic. */ @Override public void run() { if (LOG.isTraceEnabled()) { LOG.trace("Executing " + BoundedTaskExecutor.info(this)); } boolean periodic = isPeriodic(); if (!periodic) { super.run(); } else if (runAndReset()) { setNextRunTime(); delayQueue.offer(this); } } @Override public String toString() { Object info = BoundedTaskExecutor.info(this); return "Delay: " + getDelay(TimeUnit.MILLISECONDS) + "ms; " + (info == this ? super.toString() : info); } @NotNull ExecutorService getBackendExecutorService() { return backendExecutorService; } } /** * Sequence number to break scheduling ties, and in turn to * guarantee FIFO order among tied entries. */ private static final AtomicLong sequencer = new AtomicLong(); /** * Returns the trigger time of a delayed action. */ private static long triggerTime(@NotNull AppDelayQueue queue, long delay, TimeUnit unit) { return triggerTime(queue, unit.toNanos(delay < 0 ? 0 : delay)); } private static long now() { return System.nanoTime(); } /** * Returns the trigger time of a delayed action. */ private static long triggerTime(@NotNull AppDelayQueue queue, long delay) { return now() + (delay < Long.MAX_VALUE >> 1 ? delay : overflowFree(queue, delay)); } /** * Constrains the values of all delays in the queue to be within * Long.MAX_VALUE of each other, to avoid overflow in compareTo. * This may occur if a task is eligible to be dequeued, but has * not yet been, while some other task is added with a delay of * Long.MAX_VALUE. */ private static long overflowFree(@NotNull AppDelayQueue queue, long delay) { Delayed head = queue.peek(); if (head != null) { long headDelay = head.getDelay(TimeUnit.NANOSECONDS); if (headDelay < 0 && delay - headDelay < 0) { delay = Long.MAX_VALUE + headDelay; } } return delay; } @NotNull @Override public ScheduledFuture<?> schedule(@NotNull Runnable command, long delay, @NotNull TimeUnit unit) { MyScheduledFutureTask<?> t = new MyScheduledFutureTask<Void>(command, null, triggerTime(delayQueue, delay, unit)); return delayedExecute(t); } @NotNull private <T> MyScheduledFutureTask<T> delayedExecute(@NotNull MyScheduledFutureTask<T> t) { if (LOG.isTraceEnabled()) { LOG.trace("Submit at delay " + t.getDelay(TimeUnit.MILLISECONDS) + "ms " + BoundedTaskExecutor.info(t)); } if (isShutdown()) { throw new RejectedExecutionException("Already shutdown"); } delayQueue.add(t); if (t.getDelay(TimeUnit.DAYS) > 31 && !t.isPeriodic()) { // guard against inadvertent queue overflow throw new IllegalArgumentException("Unsupported crazy delay " + t.getDelay(TimeUnit.DAYS) + " days: " + BoundedTaskExecutor.info(t)); } return t; } @NotNull @Override public <V> ScheduledFuture<V> schedule(@NotNull Callable<V> callable, long delay, @NotNull TimeUnit unit) { MyScheduledFutureTask<V> t = new MyScheduledFutureTask<V>(callable, triggerTime(delayQueue, delay, unit)); return delayedExecute(t); } @NotNull @Override public ScheduledFuture<?> scheduleAtFixedRate(@NotNull Runnable command, long initialDelay, long period, @NotNull TimeUnit unit) { throw new IncorrectOperationException("Not supported because it's bad for hibernation; use scheduleWithFixedDelay() with the same parameters instead."); } @NotNull @Override public ScheduledFuture<?> scheduleWithFixedDelay(@NotNull Runnable command, long initialDelay, long delay, @NotNull TimeUnit unit) { if (delay <= 0) { throw new IllegalArgumentException("delay must be positive but got: "+delay); } MyScheduledFutureTask<Void> sft = new MyScheduledFutureTask<Void>(command, null, triggerTime(delayQueue, initialDelay, unit), unit.toNanos(-delay)); return delayedExecute(sft); } /////////////////////// delegates for ExecutorService /////////////////////////// @NotNull @Override public <T> Future<T> submit(@NotNull Callable<T> task) { return backendExecutorService.submit(task); } @NotNull @Override public <T> Future<T> submit(@NotNull Runnable task, T result) { return backendExecutorService.submit(task, result); } @NotNull @Override public Future<?> submit(@NotNull Runnable task) { return backendExecutorService.submit(task); } @NotNull @Override public <T> List<Future<T>> invokeAll(@NotNull Collection<? extends Callable<T>> tasks) throws InterruptedException { return backendExecutorService.invokeAll(tasks); } @NotNull @Override public <T> List<Future<T>> invokeAll(@NotNull Collection<? extends Callable<T>> tasks, long timeout, @NotNull TimeUnit unit) throws InterruptedException { return backendExecutorService.invokeAll(tasks, timeout, unit); } @NotNull @Override public <T> T invokeAny(@NotNull Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException { return backendExecutorService.invokeAny(tasks); } @Override public <T> T invokeAny(@NotNull Collection<? extends Callable<T>> tasks, long timeout, @NotNull TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return backendExecutorService.invokeAny(tasks, timeout, unit); } @Override public void execute(@NotNull Runnable command) { backendExecutorService.execute(command); } }