org.cometd.common.AsyncFoldLeft Maven / Gradle / Ivy
/*
* Copyright (c) 2008-2021 the original author or authors.
*
* 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 org.cometd.common;
import java.util.Collection;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.IntFunction;
import org.cometd.bayeux.Promise;
/**
* Processes asynchronously a sequence of elements, producing a result that
* is the reduction of the results produced by the processing of each element.
* This class implements the asynchronous version of the following synchronous
* {@code for} loop, but where the processing of each element may be asynchronous.
*
* R result;
* for (T element : list) {
* (result, proceed) = operation.apply(result, element);
* if (!proceed) {
* break;
* }
* }
*
* Using this class, the loop above becomes:
*
* R zero;
* AsyncFoldLeft.run(list, zero, (result, element, loop) -> {
* CompletableFuture<R> future = processAsync(element);
* future.whenComplete((r, x) -> {
* if (x == null) {
* R reduced = reduce(result, r);
* if (shouldIterate(r)) {
* loop.proceed(reduced);
* } else {
* loop.leave(reduced);
* }
* } else {
* loop.fail(x);
* }
* })
* }, Promise.complete((r, x) -> {
* // Process final result or failure.
* });
*
*/
public class AsyncFoldLeft {
public static void run(T[] array, R zero, Operation operation, Promise promise) {
int size = array.length;
if (size == 0) {
promise.succeed(zero);
} else {
IndexedLoop loop = new IndexedLoop<>(i -> array[i], size, zero, operation, promise);
loop.run();
}
}
/**
* Processes the given {@code collection} of elements.
* The initial result {@code zero} is returned if the collection is empty.
* For each element the {@link Operation#apply(Object, Object, Loop) operation}
* function is invoked.
* The {@code collection} should have a "stable" iterator, i.e. it should not be
* affected if the collection is modified concurrently by another thread, or by the
* same thread in th {@code operation} during the iteration.
*
* @param collection the elements to process
* @param zero the initial result
* @param operation the operation to invoke for each element
* @param promise the promise to notify of the final result
* @param the type of element
* @param the type of the result
*/
public static void run(Collection collection, R zero, Operation operation, Promise promise) {
Iterator iterator = collection.iterator();
if (!iterator.hasNext()) {
promise.succeed(zero);
} else {
IteratorLoop loop = new IteratorLoop<>(iterator, zero, operation, promise);
loop.run();
}
}
/**
* The operation to invoke for each element.
*
* @param the type of element
* @param the type of the result
*/
@FunctionalInterface
public interface Operation {
/**
* Processes the given {@code element}.
* The processing of the element must produce a result that is the reduction
* of the accumulated {@code result} with the result of the processing of the
* given {@code element}.
* After the processing of the given element, the implementation must decide
* whether to continue with or break out of the iteration (with the reduced
* result) or to fail the iteration (with an exception).
*
* @param result the accumulated result so far
* @param element the element to process
* @param loop the control object that continues or breaks the iteration
*/
void apply(R result, T element, Loop loop);
}
/**
* Controls the iteration over a sequence of elements, allowing to
* continue the iteration (with a result), break out of the iteration
* (with a result), or fail the iteration (with an exception).
*
* @param the type of the result
*/
public interface Loop {
/**
* Makes the loop proceed to the next element (or the end of the loop).
*
* @param r the result computed in the current iteration
*/
public default void proceed(R r) {
}
/**
* Makes the loop exit (similarly to the {@code break} statement).
*
* @param r the result computed in the current iteration
*/
public default void leave(R r) {
}
/**
* Makes the loop fail (similarly to throwing an exception).
*
* @param failure the failure of the current iteration
*/
public default void fail(Throwable failure) {
}
}
private enum State {
LOOP, ASYNC, PROCEED, LEAVE, FAIL
}
private static abstract class AbstractLoop implements Loop {
private final AtomicReference state = new AtomicReference<>(State.LOOP);
private final AtomicReference failure = new AtomicReference<>();
private final AtomicReference result;
private final Operation operation;
private final Promise promise;
private AbstractLoop(R zero, Operation operation, Promise promise) {
this.result = new AtomicReference<>(zero);
this.operation = operation;
this.promise = promise;
}
abstract boolean hasCurrent();
abstract T current();
abstract void next();
void run() {
while (hasCurrent()) {
state.set(State.LOOP);
operation.apply(result.get(), current(), this);
loop:
while (true) {
State current = state.get();
switch (current) {
case LOOP:
if (state.compareAndSet(current, State.ASYNC)) {
return;
}
break;
case PROCEED:
next();
break loop;
case LEAVE:
promise.succeed(result.get());
return;
case FAIL:
promise.fail(failure.get());
return;
default:
throw new IllegalStateException();
}
}
}
promise.succeed(result.get());
}
@Override
public void proceed(R r) {
result.set(r);
while (true) {
State current = state.get();
switch (current) {
case LOOP:
if (state.compareAndSet(current, State.PROCEED)) {
return;
}
break;
case ASYNC:
if (state.compareAndSet(current, State.PROCEED)) {
next();
run();
return;
}
break;
default:
throw new IllegalStateException();
}
}
}
@Override
public void leave(R r) {
result.set(r);
while (true) {
State current = state.get();
switch (current) {
case LOOP:
if (state.compareAndSet(current, State.LEAVE)) {
return;
}
break;
case ASYNC:
if (state.compareAndSet(current, State.LEAVE)) {
promise.succeed(result.get());
return;
}
break;
default:
throw new IllegalStateException();
}
}
}
@Override
public void fail(Throwable x) {
failure.compareAndSet(null, x);
while (true) {
State current = state.get();
switch (current) {
case LOOP:
if (state.compareAndSet(current, State.FAIL)) {
return;
}
break;
case ASYNC:
if (state.compareAndSet(current, State.FAIL)) {
promise.fail(x);
return;
}
break;
default:
throw new IllegalStateException();
}
}
}
}
private static class IndexedLoop extends AbstractLoop {
private final IntFunction element;
private final int size;
private int index;
private IndexedLoop(IntFunction element, int size, R zero, Operation operation, Promise promise) {
super(zero, operation, promise);
this.element = element;
this.size = size;
}
@Override
boolean hasCurrent() {
return index < size;
}
@Override
T current() {
return element.apply(index);
}
@Override
void next() {
++index;
}
}
private static class IteratorLoop extends AbstractLoop {
private final Iterator iterator;
private boolean hasCurrent;
private T current;
private IteratorLoop(Iterator iterator, R zero, Operation operation, Promise promise) {
super(zero, operation, promise);
this.iterator = iterator;
next();
}
@Override
boolean hasCurrent() {
return hasCurrent;
}
@Override
T current() {
if (!hasCurrent) {
throw new NoSuchElementException();
}
return current;
}
@Override
void next() {
hasCurrent = iterator.hasNext();
current = hasCurrent ? iterator.next() : null;
}
}
}