November 12, 2019

FileChannel Closes When Interrupted

Recently I worked on some threaded IO where a control thread interrupts the IO threads once in a while. Periodically the app started to fail with a java.nio.channels.ClosedChannelException.

FileChannel Closes When Interrupted
Figure 1. FileChannel Closes When Interrupted

Let’s look at a example code which reproduces my issue:

// Toy example reproducing the issue
try(var evenLog = FileChannel.open(Paths.get("/dev/null"), StandardOpenOption.CREATE, StandardOpenOption.WRITE)){
    final var ReproduceIssueTurns = 100;
    for (int i = 0; i < ReproduceIssueTurns; i++) {
        var ioThread = runInThread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                var line = "Example data " + System.currentTimeMillis();
                var asBytes = line.getBytes(StandardCharsets.UTF_8);
                try {
                    var len = evenLog.write(ByteBuffer.wrap(asBytes));
                    System.out.println("Wrote " + len + " bytes.");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        // Once in a while, the IO thread is stopped
        Thread.sleep(100);
        ioThread.interrupt();
        ioThread.join();
    }
}


private static Thread runInThread(Runnable block) {
    Thread thread = new Thread(block);
    thread.setDaemon(true);
    thread.start();
    return thread;
}
Result:
java.nio.channels.ClosedChannelException
   at java.base/sun.nio.ch.FileChannelImpl.ensureOpen(FileChannelImpl.java:150)
   at java.base/sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:266)
   at info.gamlor.examples.InterruptExample.lambda$withChannel$1(InterruptExample.java:52)
   at java.base/java.lang.Thread.run(Thread.java:830)

Unlike this example, in the real application, I got a java.nio.channels.ClosedChannelException exception storm after some time. After I couldn’t find an obvious mistake like I closing the file channel to early I looked at the FileChannel source code. The FileChannel.ensureOpen checks the isOpen getter which is backed by a simple boolean field. From that, I searched the places which set the open field to true. Besides the obvious close method I found the 'begin' method which also closes the channel. I set a breakpoint there and indeed that is what closes the channel. Yes, interrupting a file channel during a read or write closes that channel. That was a sunrise for me. I’d expected that the specific read/write operation is interrupted, but not that the channel is closed. It is written in the Javadoc of the read/write methods. There is a bigger hint which I missed. When the write or read operation is interrupted it throws a java.nio.channels.ClosedByInterruptException and later operations then fail with a plain java.nio.channels.ClosedChannelException. I didn’t notice the difference and therefore debugged a bit longer.

Streams are not Closed

This surprise made me curious if file streams also act like that. Here’s the example code with regular Java file streams:

Works Fine With Streams:
try (var eventLog = new FileOutputStream("/dev/null")) {
    for (int i = 0; i < 100; i++) {
        var t = runInThread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                var line = "Example data " + System.currentTimeMillis();
                var asBytes = line.getBytes(StandardCharsets.UTF_8);
                try {
                    eventLog.write(asBytes);
                    System.out.println("Wrote " + asBytes.length + " bytes.");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });

        // Once in a while, the IO thread is stopped
        Thread.sleep(100);
        t.interrupt();
        t.join();
    }
}
// Snip
Wrote 26 bytes.
Wrote 26 bytes.
Wrote 26 bytes.
Wrote 26 bytes.

Process finished with exit code 0

Nope, Java file streams are not closed when interrupted. That is why I was surprised by the file channel behavior.

Make the Channel Uninterruptible?

When reading the file channel code you find the setUninterruptible method. Ah, a solution to my issue. Unfortunately, while the method is public it is not exposed in the actual public FileChannel API. You shouldn’t use it. Let’s be a naughty programmer and use reflection to call it and see if it would fix our issue:

Reflection Calling FileChannelImpl.setUninterruptible():
try {
    // Call the implementation detail method. FileChannelImpl.setUninterruptible()
    var method = evenLog.getClass().getMethod("setUninterruptible");
    method.invoke(evenLog);
} catch (Exception e) {
    System.err.println("Evil doing failed");
    e.printStackTrace();
    return;
}
// Previous code to write to the file channel
// Snip
Wrote 26 bytes.
Wrote 26 bytes.
Wrote 26 bytes.
Wrote 26 bytes.

Process finished with exit code 0

When using this, the file channel behaves as I expect. It doesn’t get closed on interrupts. It ignores interrupts and it doesn’t close. Anyway, while this fixes the issue it relies on fragile non-public API.

Right Solution, Using a Stop Flag

The proper way to fix my issue was to use a stop flag instead of using the Thread.interrupt() and Thread.isInterrupted(). The IO thread checks a volatile or atomic flag to find out if it should keep running.

Proper Fix:
var running = new AtomicBoolean(true);
var ioThread = runInThread(() -> {
    while (running.get()) {
        var line = "Example data " + System.currentTimeMillis();
        var asBytes = line.getBytes(StandardCharsets.UTF_8);
        try {
            var len = evenLog.write(ByteBuffer.wrap(asBytes));
            System.out.println("Wrote " + len + " bytes.");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});

// Once in a while, the IO thread is stopped
Thread.sleep(100);
running.set(false);
ioThread.join();
Tags: Java