Search code examples
javaandroidguava

Compose variable number of ListenableFuture


I'm quite new to Futures and am stuck on chaining calls and create a list of objects. I'm using Android, API min is 19.

I want to code the method getAllFoo() below:

ListenableFuture<List<Foo>> getAllFoo() {
    // ...    
}

I have these 2 methods available:

ListenableFuture<Foo> getFoo(int index) {
    // gets a Foo by its index
}

ListenableFuture<Integer> getNbFoo() {
    // gets the total number of Foo objects
}

Method Futures.allAsList() would work nicely here, but my main constraint is that each call to getFoo(int index) cannot occur until the previous one is completed.

As far as I understand it (and tested it), Futures.allAsList() "fans-out" the calls (all the calls start at the same time), so I can't use something like that:

ListenableFuture<List<Foo>> getAllFoo() {
    // ...

    List<ListenableFuture<Foo>> allFutureFoos = new ArrayList<>();
    for (int i = 0; i < size; i++) {
        allFutureFoos.add(getFoo(i));
    }

    ListenableFuture<List<Foo>> allFoos = Futures.allAsList(allFutureFoos);

    return allFoos;
}

I have this kind of (ugly) solution (that works):

// ...
final SettableFuture<List<Foo>> future = SettableFuture.create();

List<Foo> listFoos = new ArrayList<>();
addApToList(future, 0, nbFoo, listFoos);

// ...
private ListenableFuture<List<Foo>> addFooToList(SettableFuture future, int idx, int size, List<Foo> allFoos) {

    Futures.addCallback(getFoo(idx), new FutureCallback<Foo>() {
        @Override
        public void onSuccess(Foo foo) {
            allFoos.add(foo);
            if ((idx + 1) < size) {
                addFooToList(future, idx + 1, size, allFoos);
            } else {
                future.set(allFoos);
            }
        }

        @Override
        public void onFailure(Throwable throwable) {
            future.setException(throwable);
        }
    });
    return future;
}

How can I implement that elegantly using ListenableFuture ?
I found multiple related topics (like this or that), but these are using "coded" transform, and are not based on a variable number of transformations.

How can I compose ListenableFutures and get the same return value as Futures.allAsList(), but by chaining calls (fan-in)?

Thanks !


Solution

  • As a general rule, it's better to chain derived futures together with transform/catching/whennAllSucceed/whenAllComplete than with manual addListener/addCallback calls. The transformation methods can do some more for you:

    • present fewer opportunities to forget to set an output, thus hanging the program
    • propagate cancellation
    • avoid retaining memory longer than needed
    • do tricks to reduce the chance of stack overflows

    Anyway, I'm not sure there's a particularly elegant way to do this, but I suggest something along these lines (untested!):

    ListenableFuture<Integer> countFuture = getNbFoo();
    return countFuture.transformAsync(
        count -> {
          List<ListenableFuture<Foo>> results = new ArrayList<>();
          ListenableFuture<?> previous = countFuture;
          for (int i = 0; i < count; i++) {
            final int index = i;
            ListenableFuture<Foo> current = previous.transformAsync(
                unused -> getFoo(index),
                directExecutor());
            results.add(current);
            previous = current;
          }
          return allAsList(results);
        },
        directExecutor());