package org.threadly.concurrent;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.function.Supplier;
import org.threadly.concurrent.future.FutureUtils;
import org.threadly.concurrent.future.ImmediateResultListenableFuture;
import org.threadly.concurrent.future.ListenableFuture;
import org.threadly.concurrent.future.ListenableFutureAdapterTask;
import org.threadly.concurrent.future.ListenableFutureTask;
import org.threadly.concurrent.future.ListenableRunnableFuture;
import org.threadly.concurrent.future.SettableListenableFuture;
import org.threadly.concurrent.future.Watchdog;
import org.threadly.util.Pair;
/**
* Simple class for watching a condition and getting notified when a state has changed. One handy
* tool is the ability to transition java's {@link Future} into Threadly's much nicer
* {@link ListenableFuture}.
* <p>
* The frequency at which this polls should be determined based off how cheap polling is, and how
* many items will be polled on average.
* <p>
* If being allowed to garbage collect, this poller will continue to schedule itself as long as
* there are outstanding futures. Once all have completed (from timeout or naturally), then this
* will stop scheduling itself to poll for updates. Thus no explicit cleanup is needed. As long
* as the {@link Supplier}'s are quick/fast ({@link Future} conversions are always quick/fast),
* it's most efficient to reuse the {@link Poller} instance. But if you need dynamic
* timeout's/max wait time you could construct a {@link Poller} and toss it away once you get the
* returned {@link ListenableFuture} from it.
*/
public class Poller {
private final Watchdog futureWatchdog;
private final PollRunner runner;
/**
* Construct a new poller which will run on the provided scheduler, and run at the specified
* frequency.
*
* @param scheduler Scheduler to run polling task on
* @param pollFrequency Time in milliseconds to wait between polling events
*/
public Poller(SchedulerService scheduler, long pollFrequency) {
this(scheduler, pollFrequency, -1);
}
/**
* Construct a new poller which will run on the provided scheduler, and run at the specified
* frequency.
* <p>
* This constructor additionally allows you to specify the maximum time in millseconds we should
* wait for the condition to become true. At this point if we are still not seeing our expected
* polling state, then the return future will be canceled.
*
* @param scheduler Scheduler to run polling task on
* @param pollFrequency Time in milliseconds to wait between polling events
* @param maxWaitTime Maximum time in milliseconds till returned futures should be canceled
*/
public Poller(SchedulerService scheduler, long pollFrequency, long maxWaitTime) {
if (maxWaitTime > 0 && maxWaitTime != Long.MAX_VALUE) {
futureWatchdog = new Watchdog(scheduler, maxWaitTime, false);
} else {
futureWatchdog = null;
}
this.runner = new PollRunner(scheduler, pollFrequency);
}
/**
* Watch suppliers returned condition. Once Supplier is witnessed in the {@code true} state, the
* returned future is completed. Listeners and FutureCallback's executed on the returned future
* without a specified pool will run on the polling thread, and so should be kept to a minimum.
*
* @param p Supplier to provide state for when poll has completed as expected
* @return Future to complete once boolean state is witnessed as {@code true}
*/
public ListenableFuture<?> watch(Supplier<Boolean> p) {
ListenableFuture<?> result = runner.watch(p);
if (futureWatchdog != null) {
futureWatchdog.watch(result);
}
return result;
}
/**
* Convert a conventional {@link Future} into a {@link ListenableFuture}. As poller runs it
* will check if the provided future has completed. Once it does complete the returned future
* will also complete in the exact same way. Canceling the returned future will have NO impact
* on the provided future (and thus the use with a timeout is not a concern to interrupting the
* provided future). Because this is only checked at poll intervals the returned future's
* completion will be delayed by that polling delay.
*
* @param <T> Type of object returned from future
* @param f Future to monitor for completetion
* @return ListenableFuture that will provide the result from the source future
*/
@SuppressWarnings("unchecked")
public <T> ListenableFuture<T> watch(Future<? extends T> f) {
if ((futureWatchdog == null || f.isDone()) && f instanceof ListenableFuture) {
return (ListenableFuture<T>)f;
}
ListenableFuture<T> result = runner.watch(f);
if (futureWatchdog != null) {
futureWatchdog.watch(result);
}
return result;
}
/**
* Class which when run checks across a collection of polls, looking for ones which have
* completed. The runner reschedules itself as long as there is polls to be checked.
*/
protected static class PollRunner extends ReschedulingOperation {
private final Collection<Pair<ListenableRunnableFuture<?>, Supplier<Boolean>>> polls =
new ConcurrentLinkedQueue<>();
public PollRunner(SubmitterScheduler scheduler, long scheduleDelay) {
super(scheduler, scheduleDelay);
}
public ListenableFuture<?> watch(Supplier<Boolean> p) {
if (p.get()) {
return ImmediateResultListenableFuture.NULL_RESULT;
} else {
ListenableRunnableFuture<?> result =
new ListenableFutureTask<>(false, DoNothingRunnable.instance());
polls.add(new Pair<>(result, p));
signalToRun();
return result;
}
}
public <T> ListenableFuture<T> watch(Future<? extends T> f) {
if (f.isDone()) { // optimized path for already complete futures if possible
if (f.isCancelled()) {
SettableListenableFuture<T> slf = new SettableListenableFuture<>();
slf.cancel(false);
return slf;
}
try {
return FutureUtils.immediateResultFuture(f.get());
} catch (InterruptedException e) {
// should not be possible
throw new RuntimeException(e);
} catch (ExecutionException e) {
// failure in getting result from future, transfer failure
return FutureUtils.immediateFailureFuture(e.getCause());
}
} else {
ListenableRunnableFuture<T> result = new ListenableFutureAdapterTask<T>(f);
polls.add(new Pair<>(result, () -> f.isDone()));
signalToRun();
return result;
}
}
@Override
public void run() {
Iterator<Pair<ListenableRunnableFuture<?>, Supplier<Boolean>>> it = polls.iterator();
boolean hasMore = false;
while (it.hasNext()) {
Pair<ListenableRunnableFuture<?>, Supplier<Boolean>> p = it.next();
if (p.getLeft().isDone()) {
it.remove(); // likely completed/canceled by Watchdog
} else if (p.getRight().get()) {
it.remove();
p.getLeft().run(); // mark as done
} else {
hasMore = true;
}
}
if (hasMore) {
signalToRun();
}
}
}
}