Skip to content

Commit 9b0593e

Browse files
committed
Fallback to RandomAccessFile on ClosedByInterruptException
Refine the fix for gh-38611 so that `ClosedByInterruptException` no longer retries in a loop. Our previous fix was flawed due to the fact that another interrupt could occur after we clear the first and whilst we are reading data. If this happens 10 times in a row, we raise an exception and end up causing NoClassDefFoundError errors. Our new approach retains the use of `FileChannel` and a direct buffer up to the point that a `ClosedByInterruptException` is raised or the thread is detected as interrupted. At that point, we temporarily switch to using a `RandomAccessFile` to access the data. This will block the thread until the data has been read. Fixes gh-40096
1 parent 4203e1f commit 9b0593e

File tree

5 files changed

+98
-71
lines changed

5 files changed

+98
-71
lines changed

spring-boot-project/spring-boot-tools/spring-boot-loader/src/main/java/org/springframework/boot/loader/zip/FileDataBlock.java

Lines changed: 72 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.io.File;
2020
import java.io.IOException;
21+
import java.io.RandomAccessFile;
2122
import java.nio.ByteBuffer;
2223
import java.nio.channels.ClosedByInterruptException;
2324
import java.nio.channels.ClosedChannelException;
@@ -39,22 +40,22 @@ class FileDataBlock implements CloseableDataBlock {
3940

4041
private static final DebugLogger debug = DebugLogger.get(FileDataBlock.class);
4142

42-
static Tracker tracker;
43+
static Tracker tracker = Tracker.NONE;
4344

44-
private final ManagedFileChannel channel;
45+
private final FileAccess fileAccess;
4546

4647
private final long offset;
4748

4849
private final long size;
4950

5051
FileDataBlock(Path path) throws IOException {
51-
this.channel = new ManagedFileChannel(path);
52+
this.fileAccess = new FileAccess(path);
5253
this.offset = 0;
5354
this.size = Files.size(path);
5455
}
5556

56-
FileDataBlock(ManagedFileChannel channel, long offset, long size) {
57-
this.channel = channel;
57+
FileDataBlock(FileAccess fileAccess, long offset, long size) {
58+
this.fileAccess = fileAccess;
5859
this.offset = offset;
5960
this.size = size;
6061
}
@@ -79,7 +80,7 @@ public int read(ByteBuffer dst, long pos) throws IOException {
7980
originalDestinationLimit = dst.limit();
8081
dst.limit(dst.position() + remaining);
8182
}
82-
int result = this.channel.read(dst, this.offset + pos);
83+
int result = this.fileAccess.read(dst, this.offset + pos);
8384
if (originalDestinationLimit != -1) {
8485
dst.limit(originalDestinationLimit);
8586
}
@@ -92,7 +93,7 @@ public int read(ByteBuffer dst, long pos) throws IOException {
9293
* @throws IOException on I/O error
9394
*/
9495
void open() throws IOException {
95-
this.channel.open();
96+
this.fileAccess.open();
9697
}
9798

9899
/**
@@ -102,7 +103,7 @@ void open() throws IOException {
102103
*/
103104
@Override
104105
public void close() throws IOException {
105-
this.channel.close();
106+
this.fileAccess.close();
106107
}
107108

108109
/**
@@ -112,7 +113,7 @@ public void close() throws IOException {
112113
* @throws E if the channel is closed
113114
*/
114115
<E extends Exception> void ensureOpen(Supplier<E> exceptionSupplier) throws E {
115-
this.channel.ensureOpen(exceptionSupplier);
116+
this.fileAccess.ensureOpen(exceptionSupplier);
116117
}
117118

118119
/**
@@ -145,14 +146,14 @@ FileDataBlock slice(long offset, long size) {
145146
if (size < 0 || offset + size > this.size) {
146147
throw new IllegalArgumentException("Size must not be negative and must be within bounds");
147148
}
148-
debug.log("Slicing %s at %s with size %s", this.channel, offset, size);
149-
return new FileDataBlock(this.channel, this.offset + offset, size);
149+
debug.log("Slicing %s at %s with size %s", this.fileAccess, offset, size);
150+
return new FileDataBlock(this.fileAccess, this.offset + offset, size);
150151
}
151152

152153
/**
153154
* Manages access to underlying {@link FileChannel}.
154155
*/
155-
static class ManagedFileChannel {
156+
static class FileAccess {
156157

157158
static final int BUFFER_SIZE = 1024 * 10;
158159

@@ -162,6 +163,10 @@ static class ManagedFileChannel {
162163

163164
private FileChannel fileChannel;
164165

166+
private boolean fileChannelInterrupted;
167+
168+
private RandomAccessFile randomAccessFile;
169+
165170
private ByteBuffer buffer;
166171

167172
private long bufferPosition = -1;
@@ -170,7 +175,7 @@ static class ManagedFileChannel {
170175

171176
private final Object lock = new Object();
172177

173-
ManagedFileChannel(Path path) {
178+
FileAccess(Path path) {
174179
if (!Files.isRegularFile(path)) {
175180
throw new IllegalArgumentException(path + " must be a regular file");
176181
}
@@ -194,34 +199,45 @@ int read(ByteBuffer dst, long position) throws IOException {
194199
}
195200

196201
private void fillBuffer(long position) throws IOException {
197-
for (int i = 0; i < 10; i++) {
198-
boolean interrupted = (i != 0) ? Thread.interrupted() : false;
199-
try {
200-
this.buffer.clear();
201-
this.bufferSize = this.fileChannel.read(this.buffer, position);
202-
this.bufferPosition = position;
203-
return;
204-
}
205-
catch (ClosedByInterruptException ex) {
202+
if (Thread.currentThread().isInterrupted()) {
203+
fillBufferUsingRandomAccessFile(position);
204+
return;
205+
}
206+
try {
207+
if (this.fileChannelInterrupted) {
206208
repairFileChannel();
209+
this.fileChannelInterrupted = false;
207210
}
208-
finally {
209-
if (interrupted) {
210-
Thread.currentThread().interrupt();
211-
}
212-
}
211+
this.buffer.clear();
212+
this.bufferSize = this.fileChannel.read(this.buffer, position);
213+
this.bufferPosition = position;
214+
}
215+
catch (ClosedByInterruptException ex) {
216+
this.fileChannelInterrupted = true;
217+
fillBufferUsingRandomAccessFile(position);
213218
}
214-
throw new ClosedByInterruptException();
215219
}
216220

217-
private void repairFileChannel() throws IOException {
218-
if (tracker != null) {
219-
tracker.closedFileChannel(this.path, this.fileChannel);
221+
private void fillBufferUsingRandomAccessFile(long position) throws IOException {
222+
if (this.randomAccessFile == null) {
223+
this.randomAccessFile = new RandomAccessFile(this.path.toFile(), "r");
224+
tracker.openedFileChannel(this.path);
220225
}
221-
this.fileChannel = FileChannel.open(this.path, StandardOpenOption.READ);
222-
if (tracker != null) {
223-
tracker.openedFileChannel(this.path, this.fileChannel);
226+
byte[] bytes = new byte[BUFFER_SIZE];
227+
this.randomAccessFile.seek(position);
228+
int len = this.randomAccessFile.read(bytes);
229+
this.buffer.clear();
230+
if (len > 0) {
231+
this.buffer.put(bytes, 0, len);
224232
}
233+
this.bufferSize = len;
234+
this.bufferPosition = position;
235+
}
236+
237+
private void repairFileChannel() throws IOException {
238+
tracker.closedFileChannel(this.path);
239+
this.fileChannel = FileChannel.open(this.path, StandardOpenOption.READ);
240+
tracker.openedFileChannel(this.path);
225241
}
226242

227243
void open() throws IOException {
@@ -230,9 +246,7 @@ void open() throws IOException {
230246
debug.log("Opening '%s'", this.path);
231247
this.fileChannel = FileChannel.open(this.path, StandardOpenOption.READ);
232248
this.buffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
233-
if (tracker != null) {
234-
tracker.openedFileChannel(this.path, this.fileChannel);
235-
}
249+
tracker.openedFileChannel(this.path);
236250
}
237251
this.referenceCount++;
238252
debug.log("Reference count for '%s' incremented to %s", this.path, this.referenceCount);
@@ -251,18 +265,21 @@ void close() throws IOException {
251265
this.bufferPosition = -1;
252266
this.bufferSize = 0;
253267
this.fileChannel.close();
254-
if (tracker != null) {
255-
tracker.closedFileChannel(this.path, this.fileChannel);
256-
}
268+
tracker.closedFileChannel(this.path);
257269
this.fileChannel = null;
270+
if (this.randomAccessFile != null) {
271+
this.randomAccessFile.close();
272+
tracker.closedFileChannel(this.path);
273+
this.randomAccessFile = null;
274+
}
258275
}
259276
debug.log("Reference count for '%s' decremented to %s", this.path, this.referenceCount);
260277
}
261278
}
262279

263280
<E extends Exception> void ensureOpen(Supplier<E> exceptionSupplier) throws E {
264281
synchronized (this.lock) {
265-
if (this.referenceCount == 0 || !this.fileChannel.isOpen()) {
282+
if (this.referenceCount == 0) {
266283
throw exceptionSupplier.get();
267284
}
268285
}
@@ -280,9 +297,21 @@ public String toString() {
280297
*/
281298
interface Tracker {
282299

283-
void openedFileChannel(Path path, FileChannel fileChannel);
300+
Tracker NONE = new Tracker() {
301+
302+
@Override
303+
public void openedFileChannel(Path path) {
304+
}
305+
306+
@Override
307+
public void closedFileChannel(Path path) {
308+
}
309+
310+
};
311+
312+
void openedFileChannel(Path path);
284313

285-
void closedFileChannel(Path path, FileChannel fileChannel);
314+
void closedFileChannel(Path path);
286315

287316
}
288317

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/jar/NestedJarFileTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ void cleanupFromReleasesResources() throws IOException {
303303
Cleanable cleanable = mock(Cleanable.class);
304304
given(cleaner.register(any(), action.capture())).willReturn(cleanable);
305305
try (NestedJarFile jar = new NestedJarFile(this.file, null, null, false, cleaner)) {
306-
Object channel = Extractors.byName("resources.zipContent.data.channel").apply(jar);
306+
Object channel = Extractors.byName("resources.zipContent.data.fileAccess").apply(jar);
307307
assertThat(channel).extracting("referenceCount").isEqualTo(1);
308308
action.getValue().run();
309309
assertThat(channel).extracting("referenceCount").isEqualTo(0);

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/AssertFileChannelDataBlocksClosedExtension.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.io.Closeable;
2020
import java.io.IOException;
2121
import java.lang.ref.Cleaner.Cleanable;
22-
import java.nio.channels.FileChannel;
2322
import java.nio.file.Path;
2423
import java.util.ArrayList;
2524
import java.util.LinkedHashSet;
@@ -52,7 +51,7 @@ public void beforeEach(ExtensionContext context) throws Exception {
5251
@Override
5352
public void afterEach(ExtensionContext context) throws Exception {
5453
tracker.assertAllClosed();
55-
FileDataBlock.tracker = null;
54+
FileDataBlock.tracker = Tracker.NONE;
5655
}
5756

5857
private static final class OpenFilesTracker implements Tracker {
@@ -64,12 +63,12 @@ private static final class OpenFilesTracker implements Tracker {
6463
private final List<Closeable> close = new ArrayList<>();
6564

6665
@Override
67-
public void openedFileChannel(Path path, FileChannel fileChannel) {
66+
public void openedFileChannel(Path path) {
6867
this.paths.add(path);
6968
}
7069

7170
@Override
72-
public void closedFileChannel(Path path, FileChannel fileChannel) {
71+
public void closedFileChannel(Path path) {
7372
this.paths.remove(path);
7473
}
7574

spring-boot-project/spring-boot-tools/spring-boot-loader/src/test/java/org/springframework/boot/loader/zip/FileChannelDataBlockManagedFileChannel.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616

1717
package org.springframework.boot.loader.zip;
1818

19-
import org.springframework.boot.loader.zip.FileDataBlock.ManagedFileChannel;
19+
import org.springframework.boot.loader.zip.FileDataBlock.FileAccess;
2020

2121
/**
22-
* Test access to {@link ManagedFileChannel} details.
22+
* Test access to {@link FileAccess} details.
2323
*
2424
* @author Phillip Webb
2525
*/
@@ -28,6 +28,6 @@ public final class FileChannelDataBlockManagedFileChannel {
2828
private FileChannelDataBlockManagedFileChannel() {
2929
}
3030

31-
public static int BUFFER_SIZE = FileDataBlock.ManagedFileChannel.BUFFER_SIZE;
31+
public static int BUFFER_SIZE = FileDataBlock.FileAccess.BUFFER_SIZE;
3232

3333
}

0 commit comments

Comments
 (0)