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

Peter Levart peter.levart at gmail.com
Mon Nov 23 11:31:46 UTC 2015


Even more backwards-compatible. It keeps current behavior for 
Throwable.clone() method if subclasses use it.

Just one static method and implemented interface need to be added:

public class Throwable implements Serializable, Cloneable {

     /**
      * Clones given {@code exception} and returns it's clone 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 suppressed exceptions of the returned clone instance
      * don't affect the suppressed exceptions of original exception and 
vice versa.
      *
      * @param exception the exception to clone.
      * @param <T>       the type of exception
      * @return shallow clone of given exception with suppressed exception
      * list shallow-cloned
      * @since 1.9
      */
     @SuppressWarnings("unchecked")
     public static <T extends Throwable> T clone(T exception) {
         try {
             Throwable clone = (Throwable) exception.clone();
             if (clone.suppressedExceptions != null &&
                 clone.suppressedExceptions != SUPPRESSED_SENTINEL) {
                 clone.suppressedExceptions = new 
ArrayList<>(clone.suppressedExceptions);
             }
             return (T) clone;
         } catch (CloneNotSupportedException e) {
             throw new InternalError(e);
         }
     }


Regards, Peter

On 11/23/2015 12:07 PM, Peter Levart wrote:
> 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