ratpack.exec.PromiseOperations Maven / Gradle / Ivy
/*
* Copyright 2014 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 ratpack.exec;
import ratpack.func.Action;
import ratpack.func.Function;
import ratpack.func.NoArgAction;
import ratpack.func.Predicate;
/**
* Operations that can be performed on promises to define an asynchronous data flow.
*
* These methods are available on {@link Promise} and {@link SuccessPromise}, but are defined on this separate interface for clarity.
*
* @param the type of promised value
*/
public interface PromiseOperations {
/**
* Transforms the promised value by applying the given function to it.
* {@code
* import ratpack.test.exec.ExecHarness;
* import ratpack.test.exec.ExecResult;
*
* import static org.junit.Assert.assertEquals;
*
* public class Example {
* public static void main(String... args) throws Exception {
* ExecResult result = ExecHarness.yieldSingle(c ->
* c.blocking(() -> "foo")
* .map(String::toUpperCase)
* .map(s -> s + "-BAR")
* );
*
* assertEquals("FOO-BAR", result.getValue());
* }
* }
* }
*
* @param transformer the transformation to apply to the promised value
* @param the type of the transformed object
* @return a promise for the transformed value
*/
Promise map(Function super T, ? extends O> transformer);
/**
* Like {@link #map(Function)}, but performs the transformation on a blocking thread.
*
* This is simply a more convenient form of using {@link ExecControl#blocking(java.util.concurrent.Callable)} and {@link #flatMap(Function)}.
*
* @param transformer the transformation to apply to the promised value, on a blocking thread
* @param the type of the transformed object
* @return a promise for the transformed value
*/
Promise blockingMap(Function super T, ? extends O> transformer);
/**
* Transforms the promised value by applying the given function to it that returns a promise for the transformed value.
*
* This is useful when the transformation involves an asynchronous operation.
*
{@code
* import ratpack.test.exec.ExecHarness;
* import ratpack.test.exec.ExecResult;
*
* import static org.junit.Assert.assertEquals;
*
* public class Example {
* public static void main(String[] args) throws Exception {
* ExecResult result = ExecHarness.yieldSingle(c ->
* c.blocking(() -> "foo")
* .flatMap(s -> c.blocking(s::toUpperCase))
* .map(s -> s + "-BAR")
* );
*
* assertEquals("FOO-BAR", result.getValue());
* }
* }
* }
*
* In the above example, {@code flatMap()} is being used because the transformation requires a blocking operation (it doesn't really in this case, but that's what the example is showing).
* In this case, it would be more convenient to use {@link #blockingMap(Function)}.
*
* @param transformer the transformation to apply to the promised value
* @param the type of the transformed object
* @see #blockingMap(Function)
* @return a promise for the transformed value
*/
Promise flatMap(Function super T, ? extends Promise> transformer);
/**
* Allows the promised value to be handled specially if it meets the given predicate, instead of being handled by the promise subscriber.
*
* This is typically used for validating values, centrally.
*
{@code
* import com.google.common.collect.Lists;
* import ratpack.test.exec.ExecHarness;
* import ratpack.test.exec.ExecResult;
*
* import java.util.List;
*
* import static org.junit.Assert.*;
*
* public class Example {
* public static ExecResult yield(int i, List collector) throws Exception {
* return ExecHarness.yieldSingle(c ->
* c.promise(f -> f.success(i))
* .route(v -> v > 5, collector::add)
* );
* }
*
* public static void main(String... args) throws Exception {
* List routed = Lists.newLinkedList();
*
* ExecResult result1 = yield(1, routed);
* assertEquals(new Integer(1), result1.getValue());
* assertFalse(result1.isComplete()); // false because promise returned a value before the execution completed
* assertTrue(routed.isEmpty());
*
* ExecResult result10 = yield(10, routed);
* assertNull(result10.getValue());
* assertTrue(result10.isComplete()); // true because the execution completed before the promised value was returned (i.e. it was routed)
* assertTrue(routed.contains(10));
* }
* }
* }
*
* Be careful about using this where the eventual promise subscriber is unlikely to know that the promise
* will routed as it can be surprising when neither the promised value nor an error appears.
*
* It can be useful at the handler layer to provide common validation.
*
{@code
* import ratpack.exec.Promise;
* import ratpack.handling.Context;
* import ratpack.test.embed.EmbeddedApp;
*
* import static org.junit.Assert.assertEquals;
*
* public class Example {
* public static Promise getAge(Context ctx) {
* return ctx
* .blocking(() -> 10) // e.g. fetch value from DB
* .route(
* i -> i < 21,
* i -> ctx.render(i + " is too young to be here!")
* );
* }
*
* public static void main(String... args) throws Exception {
* EmbeddedApp.fromHandler(ctx ->
* getAge(ctx).then(age -> ctx.render("welcome!"))
* ).test(httpClient -> {
* assertEquals("10 is too young to be here!", httpClient.getText());
* });
* }
* }
* }
*
* If the routed-to action throws an exception, it will be forwarded down the promise chain.
*
* @param predicate the condition under which the value should be routed
* @param action the terminal action for the value
* @return a routed promise
*/
Promise route(Predicate super T> predicate, Action super T> action);
/**
* A convenience shorthand for {@link #route(Predicate, Action) routing} {@code null} values.
*
* If the promised value is {@code null}, the given action will be called.
*
* @param action the action to route to if the promised value is null
* @return a routed promise
*/
Promise onNull(NoArgAction action);
/**
* Caches the promised value (or error) and returns it to all subscribers.
* {@code
* import ratpack.exec.Promise;
* import ratpack.test.exec.ExecHarness;
*
* import java.util.concurrent.atomic.AtomicInteger;
*
* import static org.junit.Assert.assertTrue;
*
* public class Example {
* public static void main(String... args) throws Exception {
* ExecHarness.runSingle(c -> {
* AtomicInteger counter = new AtomicInteger();
* Promise uncached = c.promise(f -> f.success(counter.getAndIncrement()));
*
* uncached.then(i -> assertTrue(i == 0));
* uncached.then(i -> assertTrue(i == 1));
* uncached.then(i -> assertTrue(i == 2));
*
* Promise cached = uncached.cache();
*
* cached.then(i -> assertTrue(i == 3));
* cached.then(i -> assertTrue(i == 3));
*
* uncached.then(i -> assertTrue(i == 4));
* cached.then(i -> assertTrue(i == 3));
* });
* }
* }
* }
*
* If the cached promise fails, the same exception will be returned every time.
*
{@code
* import ratpack.exec.Promise;
* import ratpack.test.exec.ExecHarness;
*
* import static org.junit.Assert.assertTrue;
*
* public class Example {
* public static void main(String... args) throws Exception {
* ExecHarness.runSingle(c -> {
* Throwable error = new Exception("bang!");
* Promise
*
* @return a caching promise.
*/
Promise cache();
/**
* Allows the execution of the promise to be deferred to a later time.
*
* When the returned promise is subscribed to, the given {@code releaser} action will be invoked.
* The execution of {@code this} promise is deferred until the runnable given to the {@code releaser} is run.
*
* It is generally more convenient to use {@link #throttled(Throttle)} or {@link #onYield(Runnable)} than this operation.
*
* @param releaser the action that will initiate the execution some time later
* @return a deferred promise
*/
Promise defer(Action super Runnable> releaser);
/**
* Registers a listener that is invoked when {@code this} promise is initiated.
* {@code
* import com.google.common.collect.Lists;
* import ratpack.test.exec.ExecHarness;
*
* import java.util.Arrays;
* import java.util.List;
*
* import static org.junit.Assert.assertEquals;
*
* public class Example {
* public static void main(String... args) throws Exception {
* List events = Lists.newLinkedList();
* ExecHarness.runSingle(c ->
* c.promise(f -> {
* events.add("promise");
* f.success("foo");
* })
* .onYield(() -> events.add("onYield"))
* .then(v -> events.add("then"))
* );
* assertEquals(Arrays.asList("onYield", "promise", "then"), events);
* }
* }
* }
*
* @param onYield the action to take when the promise is initiated
* @return effectively, {@code this} promise
*/
Promise onYield(Runnable onYield);
/**
* Registers a listener for the promise outcome.
* {@code
* import com.google.common.collect.Lists;
* import ratpack.test.exec.ExecHarness;
*
* import java.util.Arrays;
* import java.util.List;
*
* import static org.junit.Assert.assertEquals;
*
* public class Example {
* public static void main(String... args) throws Exception {
* List events = Lists.newLinkedList();
* ExecHarness.runSingle(c ->
* c.promise(f -> {
* events.add("promise");
* f.success("foo");
* })
* .wiretap(r -> events.add("wiretap: " + r.getValue()))
* .then(v -> events.add("then"))
* );
*
* assertEquals(Arrays.asList("promise", "wiretap: foo", "then"), events);
* }
* }
* }
*
* @param listener the result listener
* @return effectively, {@code this} promise
*/
Promise wiretap(Action super Result> listener);
/**
* Throttles {@code this} promise, using the given {@link Throttle throttle}.
*
* Throttling can be used to limit concurrency.
* Typically to limit concurrent use of an external resource, such as a HTTP API.
*
* Note that the {@link Throttle} instance given defines the actual throttling semantics.
*
{@code
* import ratpack.exec.Throttle;
* import ratpack.test.exec.ExecHarness;
* import ratpack.test.exec.ExecResult;
*
* import java.util.concurrent.atomic.AtomicInteger;
*
* import static org.junit.Assert.assertTrue;
*
* public class Example {
* public static void main(String... args) throws Exception {
* int numJobs = 1000;
* int maxAtOnce = 10;
*
* ExecResult result = ExecHarness.yieldSingle(c -> {
* AtomicInteger maxConcurrent = new AtomicInteger();
* AtomicInteger active = new AtomicInteger();
* AtomicInteger done = new AtomicInteger();
*
* Throttle throttle = Throttle.ofSize(maxAtOnce);
*
* // Launch numJobs forked executions, and return the maximum number that were executing at any given time
* return c.promise(f -> {
* for (int i = 0; i < numJobs; i++) {
* c.exec().start(e2 ->
* c
* .promise(f2 -> {
* int activeNow = active.incrementAndGet();
* int maxConcurrentVal = maxConcurrent.updateAndGet(m -> Math.max(m, activeNow));
* active.decrementAndGet();
* f2.success(maxConcurrentVal);
* })
* .throttled(throttle) // limit concurrency
* .then(max -> {
* if (done.incrementAndGet() == numJobs) {
* f.success(max);
* }
* }));
* }
* });
* });
*
* assertTrue(result.getValue() <= maxAtOnce);
* }
* }
* }
*
* @param throttle the particular throttle to use to throttle the operation
* @return the throttled promise
*/
Promise throttled(Throttle throttle);
}