[concurrency-interest] CompletableFuture as part of APIs and defensive copying

Vadim Shalts vshalts at gmail.com
Wed Feb 24 18:37:37 EST 2016


Hello,

I am thinking about creating API with CompletableFuture usage.
CompletableFuture is great candidate to be used in interfaces of async 
APIs for long running methods.
Also we often want to cache result (return the same heavy result a lot 
of times).
Lets imagine some simple API for that (only CompletableFuture usage is 
important here):

class SomeAPI {
     private CompletionStage<Integer> longRunningTask = null;
     private final ScheduledExecutorService scheduler = 
Executors.newScheduledThreadPool(1);

     public synchronized CompletionStage<Integer> longRunningEvenNumber() {
         if (longRunningTask == null) {
             final CompletableFuture<Integer> promise = new 
CompletableFuture();
             scheduler.schedule(() -> promise.complete(100), 1L, 
TimeUnit.SECONDS);
             longRunningTask = promise;
         }
         return longRunningTask;//.thenApply(x -> x);
     }

     public void breakApiContract() {
         CompletionStage<Integer> f = longRunningEvenNumber();
         f.toCompletableFuture().complete(101);
     }
}

I see three variants how this can be done:
1) return "promise" directly. Not good for public API due we can easily 
break contract by  toCompletableFuture().complete
2) Use defensive copying with thenApply(x -> x). Error-prone due easy to 
forget. And will cause memory allocation on every call even if we will 
not subscribe for results.
3) Implement custom read-only CompletionStage that will wrap "promise" 
and cache this wrapper instead of original "promise". Still error-prone, 
but I think it easier to understand and will no cost memory allocation 
per each call. But maybe some other hidden costs which I missed.

Because this task look like useful in many cases for me I think that 
third variant can be supported out of the box:

public synchronized CompletionStage<Integer> longRunningEvenNumber() {
         if (longRunningTask == null) {
             final CompletableFuture<Integer> promise = new 
CompletableFuture();
             scheduler.schedule(() -> promise.complete(100), 1L, 
TimeUnit.SECONDS);
             longRunningTask = promise.toReadOnlyStage();
         }
         return longRunningTask;
     }

Here imaginary method toReadOnlyStage() will return read-only 
CompletionStage (which will return new instances for each call of 
toCompletableFuture).

What do think?

Anyway, I believe that issue with toCompletableFuture().complete is not 
obvious (was not obvious for me) and people may not see that they need 
to use defensive copying in many cases when creating public APIs. Java 9 
will receive copy() method, but this is not enough to spot issue. Maybe 
some samples or notes should became part of documentation header of 
CompletableFuture?

Best regards,
   Vadim Shalts.


More information about the Concurrency-interest mailing list