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