-
Bug
-
Resolution: Unresolved
-
P4
-
11, 12, 13
-
generic
-
generic
ADDITIONAL SYSTEM INFORMATION :
tmaret-macOS:java tmaret$ java --version
java 11.0.3 2019-04-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.3+12-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.3+12-LTS, mixed mode)
System Version: macOS 10.12.6 (16G2016)
Kernel Version: Darwin 16.7.0
Boot Mode: Normal
Secure Virtual Memory: Enabled
A DESCRIPTION OF THE PROBLEM :
Assume two threads referencing a GZIPInputStream instance. Without coordination, the first thread reads from the stream in a loop, the second thread closes the stream.
When the stream is closed, the first thread usually throws an IOException which is expected according to the GZIPInputStream Javadoc [0]. However, sometimes the fist thread throws an NullPointerException, see the stack trace below
java.util.concurrent.ExecutionException: java.lang.NullPointerException: Inflater has been closed
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:192)
at test.GZIPInputStreamReadNPE.test(GZIPInputStreamReadNPE.java:62)
at test.GZIPInputStreamReadNPE.main(GZIPInputStreamReadNPE.java:25)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.lang.NullPointerException: Inflater has been closed
at java.util.zip.Inflater.ensureOpen(Inflater.java:389)
at java.util.zip.Inflater.inflate(Inflater.java:257)
at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:152)
at java.util.zip.GZIPInputStream.read(GZIPInputStream.java:117)
at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:122)
at test.GZIPInputStreamReadNPE.lambda$test$2(GZIPInputStreamReadNPE.java:57)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Throwing a NPE instead of an IOE is unexpected and violates the read contract [0] where a NPE is documented to be thrown in a different senario (`If b is null`).
The InflaterInputStream is affected in the same way.
I looked in the database for similar issues but could find it.
[0] https://docs.oracle.com/javase/8/docs/api/java/util/zip/GZIPInputStream.html#read-byte:A-int-int- 
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
I have attached a piece of code that allows reproducing the issue.
In a nutshell, the code does the following
1. Build an infinite GZIPInputStream stream
2. Run a `reader` thread that busy reads from the infinite stream
3. Run a `closer` thread that close the stream
4. Keep track of the exception thrown by the `reader` thread and check if it's a IOE
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Based on the GZIPInputStream#read Javadoc, I would expect the exception thrown by the `reader` thread to be a java.io.IOException.
ACTUAL -
Often, the `reader` thread throws an unexpected java.lang.NullPointerException.
---------- BEGIN SOURCE ----------
package test;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Iterator;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import static java.lang.String.format;
import static java.lang.Thread.currentThread;
import static java.util.concurrent.Executors.newScheduledThreadPool;
import static java.util.concurrent.TimeUnit.SECONDS;
public class GZIPInputStreamReadNPE {
private static final ScheduledExecutorService EXECUTOR = newScheduledThreadPool(8);
public static void main(String[] args) {
try {
test();
} catch (Exception e) {
throw new RuntimeException("Test failed its execution", e);
} finally {
EXECUTOR.shutdownNow();
}
}
private static void test() throws Exception {
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
GZIPOutputStream gos = new GZIPOutputStream(pos, true);
GZIPInputStream gis = new GZIPInputStream(pis);
EXECUTOR.submit(() -> {
Iterator<Integer> values = new Random().ints(0, 256).iterator();
while (!currentThread().isInterrupted()) {
gos.write(values.next());
gos.flush();
}
return null;
});
EXECUTOR.schedule(() -> {
gis.close();
return null;
}, 1, SECONDS);
Future<Void> reader =
EXECUTOR.submit(() -> {
while (gis.read() != -1);
return null;
});
try {
reader.get();
} catch (ExecutionException e) {
e.printStackTrace();
if (e.getCause() instanceof IOException) {
System.out.println("The test passed, GZIPInputStream#read threw a java.io.IOException when reading from a closed stream.");
} else {
System.out.println(format("The test failed, GZIPInputStream#read threw an unexpected %s when reading from a closed stream.", (e.getCause() != null) ? e.getCause().getClass() : null));
}
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
The workaround is for the caller code to catch the NPE and parse the exception message to distinguish between two cases
1. b (the buffer passed to the read method) is null
2. the stream is closed
FREQUENCY : often
tmaret-macOS:java tmaret$ java --version
java 11.0.3 2019-04-16 LTS
Java(TM) SE Runtime Environment 18.9 (build 11.0.3+12-LTS)
Java HotSpot(TM) 64-Bit Server VM 18.9 (build 11.0.3+12-LTS, mixed mode)
System Version: macOS 10.12.6 (16G2016)
Kernel Version: Darwin 16.7.0
Boot Mode: Normal
Secure Virtual Memory: Enabled
A DESCRIPTION OF THE PROBLEM :
Assume two threads referencing a GZIPInputStream instance. Without coordination, the first thread reads from the stream in a loop, the second thread closes the stream.
When the stream is closed, the first thread usually throws an IOException which is expected according to the GZIPInputStream Javadoc [0]. However, sometimes the fist thread throws an NullPointerException, see the stack trace below
java.util.concurrent.ExecutionException: java.lang.NullPointerException: Inflater has been closed
at java.util.concurrent.FutureTask.report(FutureTask.java:122)
at java.util.concurrent.FutureTask.get(FutureTask.java:192)
at test.GZIPInputStreamReadNPE.test(GZIPInputStreamReadNPE.java:62)
at test.GZIPInputStreamReadNPE.main(GZIPInputStreamReadNPE.java:25)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
Caused by: java.lang.NullPointerException: Inflater has been closed
at java.util.zip.Inflater.ensureOpen(Inflater.java:389)
at java.util.zip.Inflater.inflate(Inflater.java:257)
at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:152)
at java.util.zip.GZIPInputStream.read(GZIPInputStream.java:117)
at java.util.zip.InflaterInputStream.read(InflaterInputStream.java:122)
at test.GZIPInputStreamReadNPE.lambda$test$2(GZIPInputStreamReadNPE.java:57)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
Throwing a NPE instead of an IOE is unexpected and violates the read contract [0] where a NPE is documented to be thrown in a different senario (`If b is null`).
The InflaterInputStream is affected in the same way.
I looked in the database for similar issues but could find it.
[0] https://docs.oracle.com/javase/8/docs/api/java/util/zip/GZIPInputStream.html#read-byte:A-int-int- 
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
I have attached a piece of code that allows reproducing the issue.
In a nutshell, the code does the following
1. Build an infinite GZIPInputStream stream
2. Run a `reader` thread that busy reads from the infinite stream
3. Run a `closer` thread that close the stream
4. Keep track of the exception thrown by the `reader` thread and check if it's a IOE
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Based on the GZIPInputStream#read Javadoc, I would expect the exception thrown by the `reader` thread to be a java.io.IOException.
ACTUAL -
Often, the `reader` thread throws an unexpected java.lang.NullPointerException.
---------- BEGIN SOURCE ----------
package test;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Iterator;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import static java.lang.String.format;
import static java.lang.Thread.currentThread;
import static java.util.concurrent.Executors.newScheduledThreadPool;
import static java.util.concurrent.TimeUnit.SECONDS;
public class GZIPInputStreamReadNPE {
private static final ScheduledExecutorService EXECUTOR = newScheduledThreadPool(8);
public static void main(String[] args) {
try {
test();
} catch (Exception e) {
throw new RuntimeException("Test failed its execution", e);
} finally {
EXECUTOR.shutdownNow();
}
}
private static void test() throws Exception {
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
GZIPOutputStream gos = new GZIPOutputStream(pos, true);
GZIPInputStream gis = new GZIPInputStream(pis);
EXECUTOR.submit(() -> {
Iterator<Integer> values = new Random().ints(0, 256).iterator();
while (!currentThread().isInterrupted()) {
gos.write(values.next());
gos.flush();
}
return null;
});
EXECUTOR.schedule(() -> {
gis.close();
return null;
}, 1, SECONDS);
Future<Void> reader =
EXECUTOR.submit(() -> {
while (gis.read() != -1);
return null;
});
try {
reader.get();
} catch (ExecutionException e) {
e.printStackTrace();
if (e.getCause() instanceof IOException) {
System.out.println("The test passed, GZIPInputStream#read threw a java.io.IOException when reading from a closed stream.");
} else {
System.out.println(format("The test failed, GZIPInputStream#read threw an unexpected %s when reading from a closed stream.", (e.getCause() != null) ? e.getCause().getClass() : null));
}
}
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
The workaround is for the caller code to catch the NPE and parse the exception message to distinguish between two cases
1. b (the buffer passed to the read method) is null
2. the stream is closed
FREQUENCY : often