2

I'm consuming an API that returns CompletableFutures for querying devices (similar to digitalpetri modbus).

I need to call this API with a couple of options to query a device and figure out what it is - this is basically trial and error until it succeeds. These are embedded device protocols that I cannot change, but you can think of the process as working similar to the following:

  1. Are you an apple?
  2. If not, then are you a pineapple?
  3. If not, then are you a pen?
  4. ...

While the API uses futures, in reality, the communications are serial (going over the same physical piece of wire), so they will never be executed synchronously. Once I know what it is, I want to be able to stop trying and let the caller know what it is.

I already know that I can get the result of only one of the futures with any (see below), but that may result in additional attempts that should be avoided.

Is there a pattern for chaining futures where you stop once one of them succeeds?

Similar, but is wasteful of very limited resources.

List<CompletableFuture<String>> futures = Arrays.asList(
    CompletableFuture.supplyAsync(() -> "attempt 1"),
    CompletableFuture.supplyAsync(() -> "attempt 2"),
    CompletableFuture.supplyAsync(() -> "attempt 3"));

CompletableFuture<String>[] futuresArray = (CompletableFuture<String>[]) futures.toArray();
CompletableFuture<Object> c = CompletableFuture.anyOf(futuresArray);
  • In this example, **what** is the correct result? And why are you putting them in a `List` at all? Just keep calling the "are you a" function, until you get a result... – Elliott Frisch May 24 '18 at 20:39
  • The list isn't really important here - the key is only doing one of them and then stopping the rest once one succeeds. (List was taken from examples that seem similar). – Garret Fick May 24 '18 at 21:11
  • Further investigation suggests the answer is to either complete the future or use Future which can be actively canceled - more to come. – Garret Fick May 25 '18 at 03:47
  • 1
    There seems to be a serious logical error. The answer to a question like “Are you a …” should be “yes (`true`)” or “no (`false`)”, not, to respond or to not respond. Apparently you want to stop when the first returns `true`, not when the first just responded. Your example with futures invariably returning the same `String` is not sufficient to explain what actual result you get and what you want to have at the end. – Holger May 25 '18 at 07:51
  • 1
    Also, you say "so they will never be executed synchronously"... do you mean "never be executed _a_synchronously". – daniu May 25 '18 at 08:44
  • Please take a look at https://stackoverflow.com/a/34163874/1155642 as this is quite nice solution to the mentioned problem. – jbochniak Jan 28 '19 at 13:33

2 Answers2

1

I think the best you can do is, after your retrieval of the result,

futures.forEach(f -> f.cancel(true));

This will not affect the one having produced the result, and tries its best to stop the others. Since IIUC you get them from an outside source, there's no guarantee it will actually interrupt their work.

However, since

this class has no direct control over the computation that causes it to be completed, cancellation is treated as just another form of exceptional completion

(from CompletableFuture doc), I doubt it will do what you actually want.

daniu
  • 14,137
  • 4
  • 32
  • 53
  • Clever, but you are right about not guaranteeing cancellation as a problem. In this case, I really do care the remaining ones do not execute (because they would prevent other calls on the same wire). I suspect I'm not thinking the problem the right way. – Garret Fick May 24 '18 at 21:14
  • @GarretFick You did say "I'm consuming an API that returns `CompletableFuture`s. If you're in control of the trial and error loop for each, that's really a different thing (and quite simple actually). – daniu May 25 '18 at 05:39
  • can you suggest the solution? – Garret Fick Jan 30 '19 at 01:23
1

Suppose that you have a method that is "pseudo-asynchronous" as you describe, i.e. it has an asynchronous API but requires some locking to perform:

private final static Object lock = new Object();

private static CompletableFuture<Boolean> pseudoAsyncCall(int input) {
    return CompletableFuture.supplyAsync(() -> {
                synchronized (lock) {
                    System.out.println("Executing for " + input);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    return input > 3;
                }
            });
}

And a List<Integer> of inputs that you want to check against this method, you can check each of them in sequence with recursive composition:

public static CompletableFuture<Integer> findMatch(List<Integer> inputs) {
    return findMatch(inputs, 0);
}

private static CompletableFuture<Integer> findMatch(List<Integer> inputs, int startIndex) {
    if (startIndex >= inputs.size()) {
        // no match found -- an exception could be thrown here if preferred
        return CompletableFuture.completedFuture(null);
    }
    return pseudoAsyncCall(inputs.get(startIndex))
            .thenCompose(result -> {
                if (result) {
                    return CompletableFuture.completedFuture(inputs.get(startIndex));
                } else {
                    return findMatch(inputs, startIndex + 1);
                }
            });
}

This would be used like this:

public static void main(String[] args) {
    List<Integer> inputs = Arrays.asList(0, 1, 2, 3, 4, 5);
    CompletableFuture<Integer> matching = findMatch(inputs);

    System.out.println("Found match: " + matching.join());
}

Output:

Executing for 0
Executing for 1
Executing for 2
Executing for 3
Executing for 4
Found match: 4

As you can see, it is not called for input 5, while your API (findMatch()) remains asynchronous.

Didier L
  • 18,905
  • 10
  • 61
  • 103