Skip to content

RFC: Future API

Julien Viet edited this page Mar 4, 2020 · 48 revisions

Intro

Migrate Vert.x to expose a future like API compatible.

The API will expose dual future/callback style.

class MyApi {
  // The same method exposed with both styles
  CompletionStage<T> doSomething();
  void doSomething(Handler<AsyncResult<T>>);
}

Composing a resolved future should trampoline on the event loop instead of doing a direct execution, in order to provide a non-racy model that people can reason about.

Polyglot is affected since the new programming model needs to be translated into other languages.

  • Java API languages shall be fine out of the box

  • Ruby, JS should have their model upgraded to a promise or keep the original wrapping?

  • Code translation needs to be adapted as well

Codegen model ☑️

The io.vertx.core.Future type is used as asynchronous type.

Any method returning Future<T> will be erased from the codegen methods when there is a corresponding method with an Handler<AsyncResult<T>>.

void method(String s, Handler<AsyncResult<Integer>> handler);
Future<Integer> method(Strings); // Is erased

This choice has been made since method carrying an Handler<AsyncResult<T>> are clearly identified as asynchronous methods. Methods returning Future<T> are currently ambiguous, e.g the Future<U> Future#map(Function<T, U>) is not an asynchronous method.

  • asynchronous methods are declared as a callback method (like in 3.x)

  • any method returning a Future with the same signature than a callback method with its last parameter is erased and does not appear in the codegen class model

  • other Future methods appear as regular methods

Asynchronous type ☑️

Re-use the existing Vert.x Future<T>.

We introduced a new Promise<T> type that handles the completion side and expose a Future<T>.

  • 3.8

    • deprecate write methods in Future

    • introduce a Promise that exposes a Future

    • overload the Verticle start/stop methods with the Promise type

    • replace the executeBlocking(Handler<Future<T>> handler,…​) methods by executeBlocking(Handler<Promise<T>> handler,…​)`

    • replace the VerticleFactory#resolve(…​, Future<String> fut) by VerticleFactory#resolve(…​, Promise<String> fut)

  • 4.0

    • same as 3.8

    • remove Future write methods and it won’t extend anymore Handler<AsyncResult<T>>

    • remove Verticle start / stop methods with Future<T>

Context promises ☑️

Until now Promise are created out of thin air and the corresponding future does not provide a predictable callback thread when used outside the event-loop model, when the future callback is set by another thread

  • before the promise resolution, the callback is executed on the promise resolver thread

  • after the promise resolution, the callback is executed on the current thread

In Vert.x 4 we add context promises, i.e promise capturing a Vert.x context that allows to always execute the future callback on the context thread.

Such promise are created from a ContextInternal and shall be used (at least initially) by the API implementations:

Promise<String> promise = context.promise();

When the promise is resolved, the future handler will be executed according to the current thread and context association

  • on the same context

    • on event-loop context

      • on event-loop thread ⤇ execute handler on event-loop thread

      • otherwise ⤇ run handler on context (i.e resolving a promise in an execute blocking handler)

    • on worker thread

      • on event-loop thread ⤇ should it ever happen ?

      • on worker thread ⤇ execute handler on worker thread

  • on event-loop thread ⤇ execute handler on event-loop thread setting the context association (i.e resolving a promise from Netty callback)

  • otherwise ⤇ run handler on context

Handler failure are never be propagated to the caller, instead such failure is reported to the context promise.

Future propagates the context on asynchronous operations such as maop, compose, otherwise, …​

Asynchronous API

Issues to address:

  • Currently Vert.x future only supports a single handler, we want to support instead a list of listeners

  • API improvements

Terminal operations ☑️

Future operations that are pure events (they are fluent and won’t return a new future), e.g

  • onComplete(Handler<AsyncResult<T>>) (currently setHandler)

  • onComplete(Handler<T>, Handler<Throwable>)

  • onSuccess(Handler<T>)

  • onFailure(Handler<Throwable>)

The existing setHandler will be deprecated in 3.9.0 and removed in 4.0.

Multiple listeners ☑️

Now the Future interface supports adding multiple listeners per future.

Remapping a future ☑️

Extending the current compose operation:

// Existing in 3.x
<U> Future<U> compose(Function<T, Future<U>> mapper);

// New
<U> Future<U> compose(Function<T, Future<U>> successMapper, Function<Throwable, Future<U>> failureMapper);

Future flatMap ☑️

Add a flatMap(Function<T, Future<U>>) delegating to compose(Function<T, Future<U>>).

// Existing in 3.x
<U> Future<U> compose(Function<T, Future<U>> mapper);

// New
default <U> Future<U> flatMap(Function<T, Future<U>> mapper) {
  return compose(mapper);
}

The flatMap operation is the common name used for such operation.

CompletionStage interop

Two operations helping out interactions with CompletionStage:

  • Future#toCompletionStage()

  • Future#fromCompletionStage(CompletionStage stage)

Promise API improvements

A Promise<T> can be completed as a success or failure.

Currently we do have

  • fail(Throwable) will set the failure completed state

  • complete(T) will set the success completed state

This looks asymetric, we could have instead

  • fail(Throwable set the failure completed state

  • succeed(T) set the success completed state

  • complete(Object) set either failure (when Object is instance of Throwable) or success completed state

Vert.x stack futurisation ☑️

Polyglot aspect ☑️

JavaScript and Ruby will return a Vert.x Future when no callback is provided, e.g

future = $vertx.create_net_client.connect(port, address)
future.set_handler { |err,res| ... }

This will allow code translation to operate on futures.

As the Vert.x Future type is used, the methods are code generated and thus are regular Vert.x types which is the ideal case for code generation.

Here is an example of code translation from Java:

Future<String> fut = methodThatReturnsAFutureOfString();

fut.onSuccess(s -> {
  System.out.println("Got the string " + s);
}).onFailure(err -> {
  err.printStackTrace();
});

fut
  .flatMap(res -> Future.failedFuture("fail it anyway"))
  .onComplete(ar -> {
    if (ar.succeeded()) {
      System.out.println("Got the string " + ar.result());
    } else {
      ar.cause().printStackTrace();
    }
  });

To JavaScript:

var Future = require("vertx-js/future");

var fut = methodThatReturnsAFutureOfString();

fut.onSuccess(function (s) {
  console.log("Got the string " + s);
}).onFailure(function (err) {
  err.printStackTrace();
});

fut.flatMap(function (res) {
  Future.failedFuture("fail it anyway");
}).onComplete(function (ar, ar_err) {
  if (ar_err == null) {
    console.log("Got the string " + ar);
  } else {
    ar_err.printStackTrace();
  }
});

Open questions / todo

  • Promise succeed/complete operations

  • missing stuff ?

  • CompositeFuture behavior …​

  • Context extends Executor ⇒ runOnContext …​ ⇒ see benefits (RxJava, CompletionStage async methods)

  • sticky event loop per non vertx thread

Clone this wiki locally