Thursday, December 27, 2012

Self-suppression not permitted

A few weeks ago I ran into a very interesting bug that manifested itself with an exception I had never seen before: Exception in thread "main" java.lang.IllegalArgumentException: Self-suppression not permitted.

The strange thing was that the exception was thrown from a line with no code.
The real code executes a Groovy script written by the end-user in a background thread. Managing the thread, the queue and executing a Groovy script is a bit too long for this post so I wrote a simplified use case:
public class SelfSuppression {
    public static void main(String[] args) throws Exception {
        try (MyCloseable myCloseable = new MyCloseable()) {
            for (int i = 0; i < 5; i++) {
                myCloseable.enqueueWork();
            }
        } // <== IllegalArgumentException
    }
}

At first I thought I had the wrong version of the source so I looked at the history but I found out I had the right version and that there was only one recent change. The last version known to work used a try/finally instead of a try-with-resources
Here is the version that worked:
public class SelfSuppression {
    public static void main(String[] args) throws Exception {
        MyCloseable myCloseable = new MyCloseable();
        try {
            for (int i = 0; i < 5; i++) {
                myCloseable.enqueueWork();
            }
        } finally {
            myCloseable.close();
        }
    }
}
As I said, in my case this executes code written by the end-user so very often it will throw an exception, typically a MissingPropertyException, informing the user of an error in his script. So when I say "the version that worked", I mean a version that reports the error in the Groovy script instead of throwing the IllegalArgumentException.

The culprit is the MyCloseable: enqueueWork() throws an exception so you leave the try block and Java implicitely executes myCloseable.close(). But that method also throws an exception so Java wants to report two exceptions at the same time
To do so, Java 7 introduced the "suppressedExceptions" to Throwable. The problem is that MyCloseable throws the same Exception object in the two methods:
public class MyCloseable implements AutoCloseable {
    private Exception _lastException;

    public void enqueueWork() throws Exception {
        checkHasException();
        doSomethingInBackgroundThread();
    }

    @Override
    public void close() throws Exception {
        checkHasException();
    }

    private void checkHasException() throws Exception {
        if (_lastException != null) {
            throw _lastException;
        }
    }

    private void doSomethingInBackgroundThread(){
        _lastException = new NullPointerException();
    }
}
So Java tries something like "throwable.addSuppressed(throwable)" which is not allowed so Throwable itself throws the IllegalArgumentException and the real exception is lost.
The solution? Reporting the exception once is enough.
    private void checkHasException() throws Exception {
        if (_lastException != null) {
            try {
                throw _lastException;
            } finally {
                _lastException = null;
            }
        }
    }