Repair file channel when it's closed by interruption

When an interrupted that calls FileChannel.read, the channel is
closed and the read fails with a ClosedByInterruptException. The
closure of the channel makes it unusable by other threads. To
allow other threads to read from the data block, this commit
recreates the FileChannel when a read fails on an interrupted
thread with a ClosedByInterruptException. The exception is then
rethrown to continue the thread's interruption.

Closes gh-38154
This commit is contained in:
Andy Wilkinson 2023-10-31 18:29:08 +00:00
parent 173e6543fd
commit 890a3e72ac
2 changed files with 43 additions and 2 deletions

View File

@ -18,6 +18,7 @@ package org.springframework.boot.loader.zip;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
@ -179,7 +180,13 @@ class FileChannelDataBlock implements CloseableDataBlock {
synchronized (this.lock) {
if (position < this.bufferPosition || position >= this.bufferPosition + this.bufferSize) {
this.buffer.clear();
this.bufferSize = this.fileChannel.read(this.buffer, position);
try {
this.bufferSize = this.fileChannel.read(this.buffer, position);
}
catch (ClosedByInterruptException ex) {
repairFileChannel();
throw ex;
}
this.bufferPosition = position;
}
if (this.bufferSize <= 0) {
@ -193,6 +200,16 @@ class FileChannelDataBlock implements CloseableDataBlock {
}
}
private void repairFileChannel() throws IOException {
if (tracker != null) {
tracker.closedFileChannel(this.path, this.fileChannel);
}
this.fileChannel = FileChannel.open(this.path, StandardOpenOption.READ);
if (tracker != null) {
tracker.openedFileChannel(this.path, this.fileChannel);
}
}
void open() throws IOException {
synchronized (this.lock) {
if (this.referenceCount == 0) {
@ -231,7 +248,7 @@ class FileChannelDataBlock implements CloseableDataBlock {
<E extends Exception> void ensureOpen(Supplier<E> exceptionSupplier) throws E {
synchronized (this.lock) {
if (this.referenceCount == 0) {
if (this.referenceCount == 0 || !this.fileChannel.isOpen()) {
throw exceptionSupplier.get();
}
}

View File

@ -19,9 +19,11 @@ package org.springframework.boot.loader.zip;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -74,6 +76,28 @@ class FileChannelDataBlockTests {
}
}
@Test
void readReadsFileWhenAnotherThreadHasBeenInterrupted() throws IOException, InterruptedException {
try (FileChannelDataBlock block = createAndOpenBlock()) {
ByteBuffer buffer = ByteBuffer.allocate(CONTENT.length);
AtomicReference<IOException> failure = new AtomicReference<>();
Thread thread = new Thread(() -> {
Thread.currentThread().interrupt();
try {
block.read(ByteBuffer.allocate(CONTENT.length), 0);
}
catch (IOException ex) {
failure.set(ex);
}
});
thread.start();
thread.join();
assertThat(failure.get()).isInstanceOf(ClosedByInterruptException.class);
assertThat(block.read(buffer, 0)).isEqualTo(6);
assertThat(buffer.array()).containsExactly(CONTENT);
}
}
@Test
void readDoesNotReadPastEndOfFile() throws IOException {
try (FileChannelDataBlock block = createAndOpenBlock()) {