package org.threadly.concurrent.future; import java.util.Iterator; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicReference; import org.threadly.concurrent.ConfigurableThreadFactory; import org.threadly.concurrent.ReschedulingOperation; import org.threadly.concurrent.SingleThreadScheduler; import org.threadly.concurrent.SubmitterScheduler; import org.threadly.util.Clock; /** * This class is to guarantee that a given {@link ListenableFuture} is completed within a * timeout. Once the timeout is reached, if the future has not already completed this will * attempt to invoke {@link ListenableFuture#cancel(boolean)}. The future should then throw a * {@link java.util.concurrent.CancellationException} on a {@link ListenableFuture#get()} call. * * @since 4.0.0 */ public class Watchdog { private static final AtomicReference<SingleThreadScheduler> STATIC_SCHEDULER = new AtomicReference<>(); protected static final SubmitterScheduler getStaticScheduler() { SingleThreadScheduler sts = STATIC_SCHEDULER.get(); if (sts == null) { sts = new SingleThreadScheduler(new ConfigurableThreadFactory("WatchdogDefaultScheduler-", false)); if (! STATIC_SCHEDULER.compareAndSet(null, sts)) { sts.shutdownNow(); sts = STATIC_SCHEDULER.get(); } } return sts; } protected final SubmitterScheduler scheduler; protected final long timeoutInMillis; protected final boolean sendInterruptToTrackedThreads; protected final CheckRunner checkRunner; protected final Queue<FutureWrapper> futures; /** * Constructs a new {@link Watchdog}. This constructor will use a default static scheduler * (which is lazily constructed). This should be fine in most cases, but you can provide your * own scheduler if you want to avoid the thread creation (which is shared among all instances * that were constructed with this constructor or {@link WatchdogCache#WatchdogCache(boolean)}). * * @param timeoutInMillis Time in milliseconds that futures will be set to error if they are not done * @param sendInterruptOnFutureCancel If {@code true}, and a thread is provided with the future, * an interrupt will be sent on timeout */ public Watchdog(long timeoutInMillis, boolean sendInterruptOnFutureCancel) { this(getStaticScheduler(), timeoutInMillis, sendInterruptOnFutureCancel); } /** * Constructs a new {@link Watchdog} with a scheduler of your choosing. It is critical that * this scheduler has a free thread available to inspect futures which may not have completed in * the given timeout. You may want to use a org.threadly.concurrent.limiter to ensure that * there are threads available. * * @param scheduler Scheduler to schedule task to look for expired futures * @param timeoutInMillis Time in milliseconds that futures will be set to error if they are not done * @param sendInterruptOnFutureCancel If {@code true}, and a thread is provided with the future, * an interrupt will be sent on timeout */ public Watchdog(SubmitterScheduler scheduler, long timeoutInMillis, boolean sendInterruptOnFutureCancel) { this.scheduler = scheduler; this.timeoutInMillis = timeoutInMillis; this.sendInterruptToTrackedThreads = sendInterruptOnFutureCancel; this.checkRunner = new CheckRunner(scheduler, timeoutInMillis); this.futures = new ConcurrentLinkedQueue<>(); } /** * Request the timeout in milliseconds until futures that have not completed are canceled. This * is the timeout that the class was constructed with (since it can not be changed after * construction). * * @return Time in milliseconds till incomplete futures have {@link ListenableFuture#cancel(boolean)} invoked */ public long getTimeoutInMillis() { return timeoutInMillis; } /** * Checks to see if this watchdog is currently active. Meaning there are futures on it which * either have not been completed yet, or have not been inspected for completion. If this * returns false, it means that there are no futures waiting to complete, and no scheduled tasks * currently scheduled to inspect them. * * @return {@code true} if this watchdog is currently in use */ public boolean isActive() { return ! futures.isEmpty() || checkRunner.isActive(); } /** * Watch a given {@link ListenableFuture} to ensure that it completes within the constructed * time limit. If the future is not marked as done by the time limit then it will be completed * by invoking {@link ListenableFuture#cancel(boolean)}. Weather a {@code true} or * {@code false} will be provided to interrupt the running thread is dependent on how this * {@link Watchdog} was constructed. * * @param future Future to inspect to ensure completion */ public void watch(ListenableFuture<?> future) { if (future == null || future.isDone()) { return; } final FutureWrapper fw = new FutureWrapper(future); futures.add(fw); // we attempt to remove the future on completion to reduce inspection needed future.addListener(new Runnable() { @Override public void run() { futures.remove(fw); } }); checkRunner.signalToRun(); } /** * Just a simple wrapper class so we can hold not just the future, but what time the future will * expire at. * * @since 4.0.0 */ private class FutureWrapper { public final long expireTime; private final ListenableFuture<?> future; public FutureWrapper(ListenableFuture<?> future) { this.expireTime = Clock.accurateForwardProgressingMillis() + timeoutInMillis; this.future = future; } } /** * This runnable inspects over the queue looking for futures which have expired and need to be * canceled. It may reschedule itself if it is not able to fully examine the queue (because not * all items are currently ready for inspection). * * @since 4.0.0 */ private class CheckRunner extends ReschedulingOperation { public CheckRunner(SubmitterScheduler scheduler, long scheduleDelay) { super(scheduler, scheduleDelay); } @Override protected void run() { long now = Clock.accurateForwardProgressingMillis(); Iterator<FutureWrapper> it = futures.iterator(); FutureWrapper fw = null; while (it.hasNext()) { fw = it.next(); if (now >= fw.expireTime) { it.remove(); try { fw.future.cancel(sendInterruptToTrackedThreads); } finally { fw = null; } } else { /* since futures are added in order of expiration, we know at this point we don't need to inspect any more items*/ break; } } if (fw != null) { signalToRun(); // notify we still have work to do // update our execution time to when the next expiration will occur setScheduleDelay(fw.expireTime - now); } else { // ensure schedule delay is set correctly setScheduleDelay(timeoutInMillis); } } } }