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
.
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;
}
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:
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:
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.
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();