Skip to content

Commit 5180712

Browse files
author
Gary Gregory
committed
[IO-510] Add and adapt ReadAheadInputStream and
BufferedFileChannelInputStream from Apache Spark.
1 parent 5af063a commit 5180712

6 files changed

Lines changed: 940 additions & 0 deletions

File tree

src/changes/changes.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ The <action> type attribute can be add,update,fix,remove.
8181
<action dev="ggregory" type="add" due-to="Gary Gregory">
8282
Let org.apache.commons.io.filefilter classes work with java.nio.file.Files#newDirectoryStream(Path, DirectoryStream.Filter).
8383
</action>
84+
<action issue-"IO-510" dev="ggregory" type="add" due-to="Gary Gregory, Apache Spark, David Mollitor">
85+
Add and adapt ReadAheadInputStream and BufferedFileChannelInputStream from Apache Spark.
86+
</action>
8487
<!-- UPDATES -->
8588
<action dev="ggregory" type="update" due-to="Dependabot">
8689
Update junit-jupiter from 5.6.2 to 5.7.0 #153.
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.apache.commons.io.input;
15+
16+
import java.io.File;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.lang.reflect.Field;
20+
import java.lang.reflect.Method;
21+
import java.nio.ByteBuffer;
22+
import java.nio.channels.FileChannel;
23+
import java.nio.file.Path;
24+
import java.nio.file.StandardOpenOption;
25+
import java.util.Objects;
26+
27+
import org.apache.commons.io.IOUtils;
28+
29+
import sun.misc.Cleaner;
30+
import sun.nio.ch.DirectBuffer;
31+
32+
/**
33+
* {@link InputStream} implementation which uses direct buffer to read a file to avoid extra copy of data between Java
34+
* and native memory which happens when using {@link java.io.BufferedInputStream}. Unfortunately, this is not something
35+
* already available in JDK, {@code sun.nio.ch.ChannelInputStream} supports reading a file using NIO, but does not
36+
* support buffering.
37+
* <p>
38+
* This class was ported and adapted from Apache Spark commit 933dc6cb7b3de1d8ccaf73d124d6eb95b947ed19 where it was
39+
* called {@code NioBufferedFileInputStream}.
40+
* </p>
41+
*
42+
* @since 2.9.0
43+
*/
44+
@SuppressWarnings("restriction")
45+
public final class BufferedFileChannelInputStream extends InputStream {
46+
47+
private final ByteBuffer byteBuffer;
48+
49+
private final FileChannel fileChannel;
50+
51+
/**
52+
* Constructs a new instance for the given File.
53+
*
54+
* @param file The file to stream.
55+
* @throws IOException If an I/O error occurs
56+
*/
57+
public BufferedFileChannelInputStream(final File file) throws IOException {
58+
this(file, IOUtils.DEFAULT_BUFFER_SIZE);
59+
}
60+
61+
/**
62+
* Constructs a new instance for the given File and buffer size.
63+
*
64+
* @param file The file to stream.
65+
* @param bufferSizeInBytes buffer size.
66+
* @throws IOException If an I/O error occurs
67+
*/
68+
public BufferedFileChannelInputStream(final File file, final int bufferSizeInBytes) throws IOException {
69+
this(file.toPath(), bufferSizeInBytes);
70+
}
71+
72+
/**
73+
* Constructs a new instance for the given Path.
74+
*
75+
* @param path The path to stream.
76+
* @throws IOException If an I/O error occurs
77+
*/
78+
public BufferedFileChannelInputStream(final Path path) throws IOException {
79+
this(path, IOUtils.DEFAULT_BUFFER_SIZE);
80+
}
81+
82+
/**
83+
* Constructs a new instance for the given Path and buffer size.
84+
*
85+
* @param path The path to stream.
86+
* @param bufferSizeInBytes buffer size.
87+
* @throws IOException If an I/O error occurs
88+
*/
89+
public BufferedFileChannelInputStream(final Path path, final int bufferSizeInBytes) throws IOException {
90+
Objects.requireNonNull(path, "path");
91+
fileChannel = FileChannel.open(path, StandardOpenOption.READ);
92+
byteBuffer = ByteBuffer.allocateDirect(bufferSizeInBytes);
93+
byteBuffer.flip();
94+
}
95+
96+
@Override
97+
public synchronized int available() throws IOException {
98+
return byteBuffer.remaining();
99+
}
100+
101+
/**
102+
* In Java 8, the type of DirectBuffer.cleaner() was sun.misc.Cleaner, and it was possible to access the method
103+
* sun.misc.Cleaner.clean() to invoke it. The type changed to jdk.internal.ref.Cleaner in later JDKs, and the
104+
* .clean() method is not accessible even with reflection. However sun.misc.Unsafe added a invokeCleaner() method in
105+
* JDK 9+ and this is still accessible with reflection.
106+
*/
107+
private void bufferCleaner(final DirectBuffer buffer) {
108+
//
109+
// Ported from StorageUtils.scala.
110+
//
111+
// private val bufferCleaner: DirectBuffer => Unit =
112+
// if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9)) {
113+
// val cleanerMethod =
114+
// Utils.classForName("sun.misc.Unsafe").getMethod("invokeCleaner", classOf[ByteBuffer])
115+
// val unsafeField = classOf[Unsafe].getDeclaredField("theUnsafe")
116+
// unsafeField.setAccessible(true)
117+
// val unsafe = unsafeField.get(null).asInstanceOf[Unsafe]
118+
// buffer: DirectBuffer => cleanerMethod.invoke(unsafe, buffer)
119+
// } else {
120+
// val cleanerMethod = Utils.classForName("sun.misc.Cleaner").getMethod("clean")
121+
// buffer: DirectBuffer => {
122+
// // Careful to avoid the return type of .cleaner(), which changes with JDK
123+
// val cleaner: AnyRef = buffer.cleaner()
124+
// if (cleaner != null) {
125+
// cleanerMethod.invoke(cleaner)
126+
// }
127+
// }
128+
// }
129+
//
130+
final String specVer = System.getProperty("java.specification.version");
131+
if ("1.8".equals(specVer)) {
132+
// On Java 8.
133+
final Cleaner cleaner = buffer.cleaner();
134+
if (cleaner != null) {
135+
cleaner.clean();
136+
}
137+
} else {
138+
// On Java 9 and up, but compiled on Java 8.
139+
try {
140+
final Class<?> cls = Class.forName("sun.misc.Unsafe");
141+
final Method cleanerMethod = cls.getMethod("invokeCleaner", ByteBuffer.class);
142+
final Field unsafeField = cls.getDeclaredField("theUnsafe");
143+
unsafeField.setAccessible(true);
144+
cleanerMethod.invoke(unsafeField.get(null), buffer);
145+
} catch (ReflectiveOperationException e) {
146+
throw new IllegalStateException(e);
147+
}
148+
}
149+
}
150+
151+
@Override
152+
public synchronized void close() throws IOException {
153+
try {
154+
fileChannel.close();
155+
} finally {
156+
dispose(byteBuffer);
157+
}
158+
}
159+
160+
/**
161+
* Attempts to clean up a ByteBuffer if it is direct or memory-mapped. This uses an *unsafe* Sun API that will cause
162+
* errors if one attempts to read from the disposed buffer. However, neither the bytes allocated to direct buffers
163+
* nor file descriptors opened for memory-mapped buffers put pressure on the garbage collector. Waiting for garbage
164+
* collection may lead to the depletion of off-heap memory or huge numbers of open files. There's unfortunately no
165+
* standard API to manually dispose of these kinds of buffers.
166+
*/
167+
private void dispose(final ByteBuffer buffer) {
168+
if (buffer instanceof sun.nio.ch.DirectBuffer) {
169+
bufferCleaner((sun.nio.ch.DirectBuffer) buffer);
170+
}
171+
}
172+
173+
@Override
174+
public synchronized int read() throws IOException {
175+
if (!refill()) {
176+
return -1;
177+
}
178+
return byteBuffer.get() & 0xFF;
179+
}
180+
181+
@Override
182+
public synchronized int read(final byte[] b, final int offset, int len) throws IOException {
183+
if (offset < 0 || len < 0 || offset + len < 0 || offset + len > b.length) {
184+
throw new IndexOutOfBoundsException();
185+
}
186+
if (!refill()) {
187+
return -1;
188+
}
189+
len = Math.min(len, byteBuffer.remaining());
190+
byteBuffer.get(b, offset, len);
191+
return len;
192+
}
193+
194+
/**
195+
* Checks whether data is left to be read from the input stream.
196+
*
197+
* @return true if data is left, false otherwise
198+
*/
199+
private boolean refill() throws IOException {
200+
if (!byteBuffer.hasRemaining()) {
201+
byteBuffer.clear();
202+
int nRead = 0;
203+
while (nRead == 0) {
204+
nRead = fileChannel.read(byteBuffer);
205+
}
206+
byteBuffer.flip();
207+
if (nRead < 0) {
208+
return false;
209+
}
210+
}
211+
return true;
212+
}
213+
214+
@Override
215+
public synchronized long skip(final long n) throws IOException {
216+
if (n <= 0L) {
217+
return 0L;
218+
}
219+
if (byteBuffer.remaining() >= n) {
220+
// The buffered content is enough to skip
221+
byteBuffer.position(byteBuffer.position() + (int) n);
222+
return n;
223+
}
224+
final long skippedFromBuffer = byteBuffer.remaining();
225+
final long toSkipFromFileChannel = n - skippedFromBuffer;
226+
// Discard everything we have read in the buffer.
227+
byteBuffer.position(0);
228+
byteBuffer.flip();
229+
return skippedFromBuffer + skipFromFileChannel(toSkipFromFileChannel);
230+
}
231+
232+
private long skipFromFileChannel(final long n) throws IOException {
233+
final long currentFilePosition = fileChannel.position();
234+
final long size = fileChannel.size();
235+
if (n > size - currentFilePosition) {
236+
fileChannel.position(size);
237+
return size - currentFilePosition;
238+
}
239+
fileChannel.position(currentFilePosition + n);
240+
return n;
241+
}
242+
243+
}

0 commit comments

Comments
 (0)