Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Throwing exception from CompletableFuture hangs get() and join() in Java 17

After I have switched from Java 11 to Java 17 (OpenJDK installed from Ubuntu 20.04 repository), the following code doesn't work:

import java.util.Objects;
import java.util.concurrent.CompletableFuture;

import org.apache.commons.lang.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.exception.ExceptionUtils;

public class TestClass {

    public static void main(String[] args) {
        CompletableFuture<String> streamFuture = CompletableFuture.supplyAsync(() -> {
            throw MyException.wrapIfNeeded(new Exception("MyException"));
        });
        String result = null;
        try {
            result = streamFuture.get();
        } catch (Exception e) {
            System.out.println("Exception: " + ExceptionUtils.getMessage(e));
        }
        System.out.println("Result: " + Objects.toString(result));
    }

    static class MyException extends RuntimeException {

        private static final long serialVersionUID = 3349188601484197015L;

        public MyException(Throwable cause) {
            super(cause == null ? null : cause.getMessage(), cause);
        }

        public static MyException wrapIfNeeded(Throwable e) {
            return e instanceof MyException ? (MyException) e : new MyException(e);
        }

        @Override
        public String toString() {
            return ReflectionToStringBuilder.toString(this);
        }
    }
}

There is a problem in streamFuture.get() - it hangs infinitely. I dug deeper and found that in java.util.concurrent.ForkJoinPool there is a method unmanagedBlock(ManagedBlocker blocker) which looks like

    /** ManagedBlock for external threads */
    private static void unmanagedBlock(ManagedBlocker blocker)
        throws InterruptedException {
        if (blocker == null) throw new NullPointerException();
        do {} while (!blocker.isReleasable() && !blocker.block());
    }

and the program hangs infinitely in the do-while loop.

EDIT: I found out the problem is cause by the toString() method added to my custom exception class. For some reason it started to be a problem after Java 11

like image 946
Chris Ociepa Avatar asked Nov 25 '21 13:11

Chris Ociepa


People also ask

Does CompletableFuture throw exception?

Exception Handling of CompletableFuture The call to get() throws an ExecutionException which causes the root Exception.

Does CompletableFuture get block?

The CompletableFuture. get() method is blocking. It waits until the Future is completed and returns the result after its completion.

What is Completable future join?

Overview. join() is an instance method of the CompletableFuture class. It is used to return the value when the future is complete or throws an unchecked exception if completed exceptionally.

What is CompletableFuture in Java?

A CompletableFuture is an extension to Java's Future API which was introduced in Java 8. A Future is used for asynchronous Programming. It provides two methods, isDone() and get(). The methods retrieve the result of the computation when it completes.


2 Answers

The problem is due to ReflectionToStringBuilder.toString use encapsulated java API.

We can try something like MyException.wrapIfNeeded(new NullPointerException("")).toString() And will see

Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field static final long java.lang.RuntimeException.serialVersionUID accessible: module java.base does not "opens java.lang" to unnamed module @b4c966a

Why will this cause hang infinitely?

In CompletableFuture.AsyncSupply#run method

    static final class AsyncSupply<T> extends ForkJoinTask<Void>
        implements Runnable, AsynchronousCompletionTask {
    ...
        public void run() {
            CompletableFuture<T> d; Supplier<? extends T> f;
            if ((d = dep) != null && (f = fn) != null) {
                dep = null; fn = null;
                if (d.result == null) {
                    try {
                        d.completeValue(f.get());
                    } catch (Throwable ex) {
                        d.completeThrowable(ex);
                    }
                }
                d.postComplete();
            }
        }

Since MyException is thrown, d.completeThrowable(ex); is called, inside this method, it will wrap the exception to CompletionException, which will call the constructor of Throwable with MyException as cause.

    public Throwable(Throwable cause) {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }

When cause.toString() is called, it will throw the exception mentioned in the beginning, so d.completeThrowable(ex); is not executed completely and hence the program will hang forever.

like image 84
samabcde Avatar answered Oct 19 '22 22:10

samabcde


I found out the problem is cause by the toString() method added to my custom exception class. For some reason it (or to be more precise: ReflectionToStringBuilder.toString(this);) started to be a problem after Java 11.

like image 23
Chris Ociepa Avatar answered Oct 19 '22 22:10

Chris Ociepa