package org.threadly.concurrent.future;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import org.threadly.concurrent.SubmitterScheduler;
/**
* A class which handles a collection of {@link Watchdog} instances. Because the timeout for
* {@link Watchdog} is set in the constructor {@link Watchdog#Watchdog(long, boolean)}, you can
* use this class to be more flexible and set the timeout at the time of watching the future.
*
* @since 4.0.0
*/
public class WatchdogCache {
protected static final int INSPECTION_INTERVAL_MILLIS = 1000 * 10;
protected final SubmitterScheduler scheduler;
protected final boolean sendInterruptOnFutureCancel;
protected final ConcurrentMap<Long, Watchdog> cachedDogs;
protected final Function<Long, Watchdog> watchdogProducer;
protected final Runnable cacheCleaner;
private final AtomicBoolean cleanerScheduled;
/**
* Constructs a new {@link WatchdogCache}. 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 Watchdog#Watchdog(long, boolean)}}.
*
* @param sendInterruptOnFutureCancel If {@code true}, and a thread is provided with the future,
* an interrupt will be sent on timeout
*/
public WatchdogCache(boolean sendInterruptOnFutureCancel) {
this(Watchdog.getStaticScheduler(), sendInterruptOnFutureCancel);
}
/**
* Constructs a new {@link WatchdogCache} 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 sendInterruptOnFutureCancel If {@code true}, and a thread is provided with the future,
* an interrupt will be sent on timeout
*/
public WatchdogCache(SubmitterScheduler scheduler, boolean sendInterruptOnFutureCancel) {
this.scheduler = scheduler;
this.sendInterruptOnFutureCancel = sendInterruptOnFutureCancel;
cachedDogs = new ConcurrentHashMap<>();
watchdogProducer = (timeout) -> new Watchdog(scheduler, timeout, sendInterruptOnFutureCancel);
cacheCleaner = new CleanRunner();
cleanerScheduled = new AtomicBoolean(false);
}
/**
* Watch a given {@link ListenableFuture} to ensure that it completes within the provided
* 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 WatchdogCache} was constructed.
*
* @param future Future to inspect to ensure completion
* @param timeoutInMillis Time in milliseconds that future should be completed within
*/
public void watch(ListenableFuture<?> future, long timeoutInMillis) {
// attempt around a cheap shortcut
if (future == null || future.isDone()) {
return;
}
cachedDogs.computeIfAbsent(timeoutInMillis, watchdogProducer)
.watch(future);
maybeScheduleCleaner();
}
private void maybeScheduleCleaner() {
if (! cleanerScheduled.get() && cleanerScheduled.compareAndSet(false, true)) {
scheduler.schedule(cacheCleaner, INSPECTION_INTERVAL_MILLIS);
}
}
/**
* Runnable which looks over all cached {@link Watchdog} instances to see if any are no longer
* active. Removing the inactive ones as they are found. This also handles rescheduling itself
* if future inspection may be needed.
*
* @since 4.0.0
*/
private class CleanRunner implements Runnable {
@Override
public void run() {
Iterator<Watchdog> it = cachedDogs.values().iterator();
while (it.hasNext()) {
if (! it.next().isActive()) {
it.remove();
}
}
// must set unscheduled before checking if we need to reschedule
cleanerScheduled.set(false);
if (! cachedDogs.isEmpty()) {
maybeScheduleCleaner();
}
}
}
}