io.atlassian.util.concurrent.AsyncCompleter Maven / Gradle / Ivy
Show all versions of atlassian-util-concurrent Show documentation
/**
* Copyright 2011 Atlassian Pty Ltd
*
* 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 io.atlassian.util.concurrent;
import io.atlassian.util.concurrent.ExceptionPolicy.Policies;
import net.jcip.annotations.ThreadSafe;
import javax.annotation.Nonnull;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static io.atlassian.util.concurrent.Executors.limited;
import static io.atlassian.util.concurrent.Lazy.supplier;
import static io.atlassian.util.concurrent.Timeout.getNanosTimeout;
import static java.util.Objects.requireNonNull;
/**
* Convenient encapsulation of {@link java.util.concurrent.CompletionService}
* usage that allows a collection of jobs to be issued to an
* {@link java.util.concurrent.Executor} and return an
* {@link java.lang.Iterable} of the results that is in the order that the
* results return.
*
* Unlike
* {@link java.util.concurrent.ExecutorService#invokeAll(java.util.Collection)}
* {@link #invokeAll(Iterable)} here does not itself block, rather the
* {@link java.util.Iterator#next()} calls to the returned
* {@link java.lang.Iterable} will block the first time it is iterated. This
* allows the client to defer the reification of the result until it is ready to
* use it.
*
* To create an instance of this class, please use the supplied
* {@link io.atlassian.util.concurrent.AsyncCompleter.Builder}.
*
* @since 1.0
*/
@ThreadSafe public final class AsyncCompleter {
private final Executor executor;
private final ExceptionPolicy policy;
private final ExecutorCompletionServiceFactory completionServiceFactory;
private final CompletionServiceDecorator completionServiceDecorator;
AsyncCompleter(final Executor executor, final ExceptionPolicy policy, final ExecutorCompletionServiceFactory completionServiceFactory,
CompletionServiceDecorator completionServiceDecorator) {
this.executor = requireNonNull(executor, "executor");
this.policy = requireNonNull(policy, "policy");
this.completionServiceFactory = requireNonNull(completionServiceFactory, "completionServiceFactory");
this.completionServiceDecorator = completionServiceDecorator;
}
/**
* Queue the {@link Callable jobs} on the contained
* {@link java.util.concurrent.Executor} and return a lazily evaluated
* {@link java.lang.Iterable} of the results in the order they return in
* (fastest first).
*
* Note that if any of the jobs return null then nulls WILL BE included in the
* results. Similarly if an exception is thrown and exceptions are being
* ignored then there will be a NULL result returned. If you want to filter
* nulls this is trivial, but be aware that filtering of the results forces
* {@link java.util.Iterator#next()} to be called while calling
* {@link java.util.Iterator#hasNext()} (which may block).
*
* @param the result type
* @param callables the jobs to run
* @return an Iterable that returns the results in the order in which they
* return, excluding any null values.
*/
public Iterable invokeAll(final Iterable extends Callable> callables) {
return invokeAllTasks(callables, new BlockingAccessor<>());
}
/**
* Version of {@link #invokeAll(Iterable)} that supports a timeout. Any jobs
* that are not complete by the timeout are interrupted and discarded.
*
* @param the result type
* @param callables the jobs to run
* @param time the max time spent per job specified by:
* @param unit the TimeUnit time is specified in
* @return an Iterable that returns the results in the order in which they
* return, excluding any null values.
* @see #invokeAll(Iterable)
* @since 2.1
*/
public Iterable invokeAll(final Iterable extends Callable> callables, final long time, final TimeUnit unit) {
return invokeAllTasks(callables, new TimeoutAccessor<>(getNanosTimeout(time, unit)));
}
/**
* Implementation for the invokeAll methods, needs to be passed an accessor
* function that is responsible for getting things from the CompletionService.
*/
Iterable invokeAllTasks(final Iterable extends Callable> callables, final Accessor accessor) {
final CompletionService apply = completionServiceDecorator.apply(completionServiceFactory. create().apply(executor));
// we must copy the resulting Iterable so
// each iteration doesn't resubmit the jobs
final List> lazyAsyncSuppliers = StreamSupport.stream(callables.spliterator(), false)
.map(new AsyncCompletionFunction<>(apply, accessor)).collect(Collectors.toList());
return new Iterable() {
private Stream newStream() {
return lazyAsyncSuppliers.stream().map(policy. handler()).map(Functions. fromSupplier()).filter(x -> x != null);
}
@Override public Iterator iterator() {
return newStream().iterator();
}
@Override public Spliterator spliterator() {
return newStream().spliterator();
}
};
}
/**
* For creating instances of {@link AsyncCompleter}.
*/
public static class Builder {
Executor executor;
ExceptionPolicy policy = Policies.THROW;
ExecutorCompletionServiceFactory completionServiceFactory = new DefaultExecutorCompletionServiceFactory();
CompletionServiceDecorator completionServiceDecorator = CompletionServiceDecorator.Identity.INSTANCE;
/**
* Create a Builder with the supplied Executor
*
* @param executor executor to use with the constructed completer
*/
public Builder(@NotNull final Executor executor) {
this.executor = requireNonNull(executor, "executor");
}
/**
* Ignore exceptions thrown, note will cause nulls in the resulting
* iterable!
*/
public Builder ignoreExceptions() {
return handleExceptions(Policies.IGNORE_EXCEPTIONS);
}
public Builder handleExceptions(final ExceptionPolicy policy) {
this.policy = policy;
return this;
}
public Builder completionServiceFactory(final ExecutorCompletionServiceFactory completionServiceFactory) {
this.completionServiceFactory = requireNonNull(completionServiceFactory, "completionServiceFactory");
return this;
}
public Builder checkCompletionServiceFutureIdentity() {
this.completionServiceDecorator = new CompletionServiceDecorator.IdentityChecker();
return this;
}
/**
* Create a {@link AsyncCompleter} that limits the number of jobs submitted
* to the underlying executor to a hard limit.
*
* Note: this only makes sense if the underlying executor does not have a
* limit on the number of threads it will create, on the size of the queue
* submitted jobs will live in, or where the limit is much higher than this
* limit.
*
* @param limit the number of parallel jobs to execute at any one time
* @see LimitedExecutor for more discussion of how this limit is relevant
*/
public AsyncCompleter limitParallelExecutionTo(final int limit) {
return new AsyncCompleter(limited(executor, limit), policy, completionServiceFactory, completionServiceDecorator);
}
public AsyncCompleter build() {
return new AsyncCompleter(executor, policy, completionServiceFactory, completionServiceDecorator);
}
}
/**
* Extension point if a custom CompletionService is required, for instance to
* implement a custom concellation policy.
*/
public interface ExecutorCompletionServiceFactory {
Function> create();
}
/**
* Function for submitting {@link Callable} instances to an executor and
* asynchronously waiting for the first result. Instances of this should not
* be shared as each has its own {@link CompletionService} instance (and
* therefore its own queue) so anything subsequently submitted to this
* Function may end up as the result of the supplier.
*
* @param the result type.
*/
private static class AsyncCompletionFunction implements Function, Supplier> {
private final CompletionService completionService;
private final Accessor accessor;
// the result gets memoized, so we only need one
private final Supplier nextCompleteItem = new Supplier() {
public T get() {
return accessor.apply(completionService);
}
};
AsyncCompletionFunction(final CompletionService completionService, final Accessor accessor) {
this.completionService = completionService;
this.accessor = accessor;
}
public Supplier apply(final Callable task) {
accessor.register(completionService.submit(task));
// never call get twice as it gets a new element from the queue
return supplier(nextCompleteItem::get);
}
}
/**
* Responsible for accessing the next complete item and maintaining the
* registered futures in case of timeouts.
*/
interface Accessor extends Function, T> {
/** Register a Future, may be interesting later */
void register(Future f);
}
static final class TimeoutAccessor implements Accessor {
private final Timeout timeout;
private final Collection> futures = new ConcurrentLinkedQueue<>();
TimeoutAccessor(final Timeout timeout) {
this.timeout = timeout;
}
@Override public T apply(final CompletionService completionService) {
try {
final Future future = completionService.poll(timeout.getTime(), timeout.getUnit());
if (future == null) {
cancelRemaining();
throw timeout.getTimeoutException();
}
futures.remove(future);
return future.get();
} catch (final InterruptedException e) {
throw new RuntimeInterruptedException(e);
} catch (final ExecutionException e) {
throw new RuntimeExecutionException(e);
}
}
@Override public void register(final Future f) {
futures.add(f);
}
private void cancelRemaining() {
for (final Future f : futures) {
f.cancel(true);
}
futures.clear();
}
}
static final class BlockingAccessor implements Accessor {
@Override public T apply(final CompletionService completionService) {
try {
return completionService.take().get();
} catch (final InterruptedException e) {
throw new RuntimeInterruptedException(e);
} catch (final ExecutionException e) {
throw new RuntimeExecutionException(e);
}
}
@Override public void register(final Future f) {}
}
static final class DefaultExecutorCompletionServiceFactory implements ExecutorCompletionServiceFactory {
public Function> create() {
return new ExecutorCompletionServiceFunction<>();
}
}
static final class ExecutorCompletionServiceFunction implements Function> {
public CompletionService apply(final Executor executor) {
return new ExecutorCompletionService<>(executor);
}
}
interface CompletionServiceDecorator {
CompletionService apply(CompletionService acc);
enum Identity implements CompletionServiceDecorator {
INSTANCE;
@Override public CompletionService apply(CompletionService acc) {
return acc;
}
}
/**
* Used to assert that the CompletionService returns identical Futures from
* submit and take/poll
*/
class IdentityChecker implements CompletionServiceDecorator {
@Override public CompletionService apply(CompletionService delegate) {
return new IdentityCheckedCompletionService(delegate);
}
}
}
/**
* Checks that any Future returned equals one that was submitted through here.
*
* Note that this guarantee is not strictly supported by the interface, but it
* may be desirable to check that the delegate preserves identity.
*/
static class IdentityCheckedCompletionService implements CompletionService {
private final CompletionService delegate;
private final Collection> futures = new ConcurrentLinkedQueue>();
IdentityCheckedCompletionService(CompletionService delegate) {
this.delegate = delegate;
}
Future add(Future f) {
futures.add(f);
return f;
}
Future check(Future f) {
if (!futures.remove(f)) {
throw new IllegalArgumentException("Expected the future to be in the list of registered futures");
}
return f;
}
public @Nonnull Future submit(@Nonnull final Callable task) {
return add(delegate.submit(task));
}
public @Nonnull Future submit(@Nonnull final Runnable task, final T result) {
return add(delegate.submit(task, result));
}
public Future take() throws InterruptedException {
return check(delegate.take());
}
public Future poll() {
return check(delegate.poll());
}
public Future poll(final long timeout, @Nonnull final TimeUnit unit) throws InterruptedException {
return check(delegate.poll(timeout, unit));
}
}
}