The jproducers
library allows specifying asynchronous computations declaratively and letting the library do the scheduling and waiting for you. This is particularly useful to express computations which would otherwise need complex chains of Futures.transform
.
Here is an example class which declares a few producers.
static class Producers {
@Retention(RetentionPolicy.RUNTIME)
@BindingAnnotation
@interface Foo {}
@Produces
@Foo
public static ListenableFuture<String> produceFoo() {
return Futures.immediateFuture("hello!");
}
@Produces
public static long produceLength(@Foo Present<String> foo) throws ExecutionException {
return foo.get().length();
}
}
The code to execute a graph then looks like:
ProducerContext context = ProducerContext.forClasses(Producers.class);
ListenableFuture<Long> result = context
.newGraph(Key.get(Long.class))
.run();
In order to orchestrate a computation, jproducers
constructs a connected graph of producer nodes. Each node is called a producer
, and is defined by a Java method, e.g.:
@Produces
static String produceSomeString(Present<Long> someNumber) throws ExecutionException {
return "hello world: " + someNumber.get();
}
Producers may have arguments, which jproducers
uses to determine which other producers need to be run beforehand.
The return type of a producer method determines the produced type. If a producer returns a ListenableFuture
, the library takes care of waiting for the future before invoking downstream producers. The following producers all have produced type @Bar String
:
@Produces
@Bar
static ListenableFuture<String> produceAsyncBar() {
return Futures.immediateFailedFuture(new RuntimeException("failed!"));
}
@Produces
@Bar
static String produceBar() {
return "hello world";
}
A producer can indicate failure by throwing an exception (or returning a failed future). If this happens, downstream producers are passed an instance of Present
containing the error. This allows errors to be propagated throughout a producer graph.
@Produces
static long produceNumUsers(Present<UserInfoResponse> response) {
try {
return response.get().getNumUsers();
} catch (ExecutionException e) {
throw new RuntimeException("User info rpc failed", e);
}
}
It is possible to use @ProducesIntoSet
to declare that a producer produces and element in a set of values:
@ProducesIntoSet
static UserDetailsRequest produceOwnerDetailsRequest(Present<Long> ownerId)
throws ExecutionException {
return new UserDetailsRequest(ownerId.get());
}
@ProducesIntoSet
static UserDetailsRequest producerOtherDetailsRequest(Present<String> username)
throws ExecutionException {
return new UserDetailsRequest(username.get());
}
Downstream producers can then continue their computation of the full set:
@Produces
static ListenableFuture<ImmutableSet<UserDetails>> produceDetails(
Present<ImmutableSet<UserDetailsRequest>> requests,
Present<UserInfoClient> client) throws ExecutionException {
return client.makeRequests(requests.get());
}
- Because the graph is constructed based on a desired output type, only the necessary nodes are ever executed.
- Supports backing a
ProducerContext
with direct executors for testing.
To run the example, execute:
bazel run //src/main/java/me/dinowernli/jproducers/example
To run all tests, execute:
bazel test //src/...