From 890a3e72ac0d38f5bc6dde2a120d9bc6260db1af Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 31 Oct 2023 18:29:08 +0000 Subject: [PATCH] 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 --- .../boot/loader/zip/FileChannelDataBlock.java | 21 ++++++++++++++-- .../loader/zip/FileChannelDataBlockTests.java | 24 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java index 1824281ede3..0346a87d4d0 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileChannelDataBlock.java @@ -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 { void ensureOpen(Supplier exceptionSupplier) throws E { synchronized (this.lock) { - if (this.referenceCount == 0) { + if (this.referenceCount == 0 || !this.fileChannel.isOpen()) { throw exceptionSupplier.get(); } } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java index 9beb4aa314d..df015ff68d5 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockTests.java @@ -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 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()) {