Throwable support for cloning: was Re: RFR: jsr166 jdk9 integration wave 2

Peter Levart peter.levart at gmail.com
Mon Nov 23 11:07:03 UTC 2015


Hi,

Until Throwable.addSuppressed() was added in JDK7 to support 
try-with-resources statement, Throwable has been a more-or-less 
immutable from the outside (except for initCause which is a one-of 
method meant to be called right after construction and before throwing 
and can't be called multiple times).

addSuppressed() is different. It allows a Throwable instance to be 
modified after it has been constructed and thrown. In 
try-with-resources, the caught exception from the body of 
try-with-resources statement is modified with possible exception thrown 
from the AutoCloesable.close(). This all happens in the same thread, so 
there's no problem. The final exception that is thrown from the method 
or handled (printed) contains all suppressed exceptions added so-far.

CompletionStage::whenComplete has been designed to act as a cleanup 
action equivalent to AutoCloseable.close() in try-with-resources. If 
cleanup action throws exception, it would be nice if it could be added 
to the exception of the completing stage as a suppressed exception. The 
problem with duplicating this behavior from try-with-resources is in the 
CompletionStage (CompletableFuture) design where it allows multiple 
continuations (cleanup actions for example) to be attached to a single 
completion stage. It would be desirable for those cleanup actions to not 
affect the exceptional result of the  stage they are appended to. 
There's also a problem if those continuations are asynchronous as they 
would execute Throwable::addSuppressed from multiple threads.

I suggest adding support for cloning the Throwable instances. It could 
be added in a backwards compatible way. The changes are very simple:

- add Cloneable to the implements clause of Throwable:

public class Throwable implements Serializable, Cloneable {

- add the following two methods to Throwable:

     /**
      * Clones this exception so that it shares all state with original 
exception
      * (shallow clone) except for the possible list of already
      * {@link #addSuppressed(Throwable) added} {@link #getSuppressed() 
suppressed}
      * exceptions. The suppressed exception instances are not cloned, 
just the
      * list containing them. Further {@link #addSuppressed(Throwable) 
additions}
      * to the list of suppressed exceptions of the returned clone therefore
      * don't affect the original (this) suppressed exception list and 
vice versa.
      *
      * @return a shallow clone of this exception except for the 
suppressed exception
      * list which is shallow-cloned.
      * @throws CloneNotSupportedException never thrown, but declared to 
keep source
      *                                    compatibility with possible 
subclasses
      *                                    that declare that they are 
{@link Cloneable}
      *                                    themselves and call {@code 
super.clone()}.
      * @since 1.9
      */
     @Override
     protected Object clone() throws CloneNotSupportedException {
         Throwable clone = (Throwable) super.clone();
         if (clone.suppressedExceptions != null &&
             clone.suppressedExceptions != SUPPRESSED_SENTINEL) {
             clone.suppressedExceptions = new 
ArrayList<>(clone.suppressedExceptions);
         }
         return clone;
     }

     /**
      * Invokes protected {@link #clone()} on the passed-in {@code 
exception}
      * and returns the result.
      *
      * @param exception the exception to clone.
      * @param <T>       the type of exception
      * @return the result of {@link #clone()} invoked on the passed-in 
{@code exception}
      * @since 1.9
      */
     @SuppressWarnings("unchecked")
     public static <T extends Throwable> T clone(T exception) {
         try {
             return (T) exception.clone();
         } catch (CloneNotSupportedException e) {
             throw new InternalError(e);
         }
     }


I think that this addition would enable CompletableFuture to mimic the 
logic of to try-with-resources statement and might prove useful in other 
similar designs.


Regards, Peter


On 11/23/2015 10:54 AM, Peter Levart wrote:
>
> On 11/16/2015 10:39 PM, Martin Buchholz wrote:
>> Smaller than wave 1, but still large.  Nothing like a looming deadline to
>> spur work ...
>>
>> Oracle folks will need to help with API review.
>>
>> https://bugs.openjdk.java.net/issues/?jql=(subcomponent%20%3D%20java.util.concurrent)%20AND%20status%20%3D%20%22In%20Progress%22%20ORDER%20BY%20updatedDate%20DESC
>> http://cr.openjdk.java.net/~martin/webrevs/openjdk9/jsr166-jdk9-integration/
>>
>> The primary focus is making jtreg tests more robust and faster.
>> It also contains the changes to j.u.c.atomic that Aleksey is waiting for.
>
> Hi,
>
> In CompletableFuture.uniWhenComplete method, the possible exception 
> thrown from BiConsumer action is added as suppressed exception to the 
> exception of the previous stage. This updated exception is then passed 
> as completion result to next stage. When previous stage is appended 
> with more than one asynchronous continuation:
>
>         CompletableFuture<Void> cf0 = new CompletableFuture<>();
>
>         CompletableFuture<Void> cf1 = cf0.whenCompleteAsync((v, x) -> {
>             throw new RuntimeException("Secondary 1");
>         });
>
>         CompletableFuture<Void> cf2 = cf0.whenCompleteAsync((v, x) -> {
>             throw new RuntimeException("Secondary 2");
>         });
>
>         cf0.completeExceptionally(new RuntimeException("Primary"));
>
>         try {
>             cf1.get();
>         } catch (Exception e) {
>             System.err.println("\ncf1 exception:\n");
>             e.printStackTrace();
>         }
>
>         try {
>             cf2.get();
>         } catch (Exception e) {
>             System.err.println("\ncf2 exception:\n");
>             e.printStackTrace();
>         }
>
>
> ...then both secondary exceptions are added as suppressed to the same 
> primary exception:
>
>
> cf1 exception:
>
> java.util.concurrent.ExecutionException: java.lang.RuntimeException: 
> Primary
>         at 
> java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
>         at 
> java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1948)
>         at CFTest.main(CFTest.java:22)
> Caused by: java.lang.RuntimeException: Primary
>         at CFTest.main(CFTest.java:19)
>         Suppressed: java.lang.RuntimeException: Secondary 2
>                 at CFTest.lambda$main$1(CFTest.java:16)
>                 at 
> java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:795)
>                 at 
> java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:771)
>                 at 
> java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:478)
>                 at 
> java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:281)
>                 at 
> java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1149)
>                 at 
> java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1985)
>                 at 
> java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1933)
>                 at 
> java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157) 
>
>         Suppressed: java.lang.RuntimeException: Secondary 1
>                 at CFTest.lambda$main$0(CFTest.java:12)
>                 at 
> java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:795) 
>
>                 at 
> java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:771) 
>
>                 at 
> java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:478) 
>
>                 at 
> java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:281)
>                 at 
> java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1149) 
>
>                 at 
> java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1985)
>                 at 
> java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1933)
>                 at 
> java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
>
> cf2 exception:
>
> java.util.concurrent.ExecutionException: java.lang.RuntimeException: 
> Primary
>         at 
> java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:386)
>         at 
> java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1948)
>         at CFTest.main(CFTest.java:29)
> Caused by: java.lang.RuntimeException: Primary
>         at CFTest.main(CFTest.java:19)
>         Suppressed: java.lang.RuntimeException: Secondary 2
>                 at CFTest.lambda$main$1(CFTest.java:16)
>                 at 
> java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:795)
>                 at 
> java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:771)
>                 at 
> java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:478)
>                 at 
> java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:281)
>                 at 
> java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1149)
>                 at 
> java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1985)
>                 at 
> java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1933)
>                 at 
> java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
>         Suppressed: java.lang.RuntimeException: Secondary 1
>                 at CFTest.lambda$main$0(CFTest.java:12)
>                 at 
> java.util.concurrent.CompletableFuture.uniWhenComplete(CompletableFuture.java:795)
>                 at 
> java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:771)
>                 at 
> java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:478)
>                 at 
> java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:281)
>                 at 
> java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1149)
>                 at 
> java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1985)
>                 at 
> java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1933)
>                 at 
> java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
>
>
> This is not nice for two reasons:
>
> - Throwable::addSuppressed is not thread-safe
> - The consumer of the result of one CompletableFuture can see the 
> exceptional result being modified as it observes it.
>
>
> When Chris Purcell reported the issue of discarding the whenComplete 
> action exception on concurrency-interest list:
>
> "CompletionStage.whenComplete discards exceptions if both input stage 
> and action fail. It would be less surprising if, like try/finally, it 
> added the action exception to the input exception as a suppressed 
> exception. This can be done safely by cloning the input exception (all 
> Throwables are Serializable). I don't think performance should be a 
> blocker, as this is a rare edge case, and we are providing a very 
> useful service for the cost."
>
> ...she suggested to deep-clone the input stage exception before adding 
> suppressed exception to it. Each "branch" of continuations would then 
> proceed with it's own copy of input-stage exception. This might work 
> most of the times, but can fail if input-stage exception references 
> non-serializable objects.
>
> The reason for not doing it the other way around which would be more 
> natural to forking stages (adding input-stage exception as a 
> suppressed exception to the action exception and pass the modified 
> action exception as a result of next stage) is the specification of 
> whenCompleteAsync:
>
> "Returns a new CompletionStage with the same result or exception as 
> this stage, that executes the given action using this stage's default 
> asynchronous execution facility when this stage completes.
> When this stage is complete, the given action is invoked with the 
> result (or null if none) and the exception (or null if none) of this 
> stage as arguments. The returned stage is completed when the action 
> returns. If the supplied action itself encounters an exception, then 
> the returned stage exceptionally completes with this exception unless 
> this stage also completed exceptionally."
>
> Could specification be tweaked a bit? The last statement leaves it 
> open to what actually happens when "this stage also completes 
> exceptionally". Could this unspecified case be spelled out like this:
>
> ... If the supplied action itself encounters an exception, then the 
> returned stage exceptionally completes with this exception unless this 
> stage also completed exceptionally *in which case the returned stage 
> exceptionally completes with the exception thrown from the supplied 
> action to which this stage's exception is appended as suppressed 
> exception.
> *
>
> Regards, Peter
> *
> * 




More information about the core-libs-dev mailing list