Skip to content

Commit ac4118d

Browse files
committed
[SPARK-11424] Guard against double-close() of RecordReaders
**TL;DR**: We can rule out one rare but potential cause of input stream corruption via defensive programming. ## Background [MAPREDUCE-5918](https://issues.apache.org/jira/browse/MAPREDUCE-5918) is a bug where an instance of a decompressor ends up getting placed into a pool multiple times. Since the pool is backed by a list instead of a set, this can lead to the same decompressor being used in different places at the same time, which is not safe because those decompressors will overwrite each other's buffers. Sometimes this buffer sharing will lead to exceptions but other times it will might silently result in invalid / garbled input. That Hadoop bug is fixed in Hadoop 2.7 but is still present in many Hadoop versions that we wish to support. As a result, I think that we should try to work around this issue in Spark via defensive programming to prevent RecordReaders from being closed multiple times. So far, I've had a hard time coming up with explanations of exactly how double-`close()`s occur in practice, but I do have a couple of explanations that work on paper. For instance, it looks like #7424, added in 1.5, introduces at least one extremely~rare corner-case path where Spark could double-close() a LineRecordReader instance in a way that triggers the bug. Here are the steps involved in the bad execution that I brainstormed up: * [The task has finished reading input, so we call close()](https://github.com/apache/spark/blob/v1.5.1/core/src/main/scala/org/apache/spark/rdd/NewHadoopRDD.scala#L168). * [While handling the close call and trying to close the reader, reader.close() throws an exception]( https://github.com/apache/spark/blob/v1.5.1/core/src/main/scala/org/apache/spark/rdd/NewHadoopRDD.scala#L190) * We don't set `reader = null` after handling this exception, so the [TaskCompletionListener also ends up calling NewHadoopRDD.close()](https://github.com/apache/spark/blob/v1.5.1/core/src/main/scala/org/apache/spark/rdd/NewHadoopRDD.scala#L156), which, in turn, closes the record reader again. In this hypothetical situation, `LineRecordReader.close()` could [fail with an exception if its InputStream failed to close](https://github.com/apache/hadoop/blob/release-1.2.1/src/mapred/org/apache/hadoop/mapred/LineRecordReader.java#L212). I googled for "Exception in RecordReader.close()" and it looks like it's possible for a closed Hadoop FileSystem to trigger an error there: [SPARK-757](https://issues.apache.org/jira/browse/SPARK-757), [SPARK-2491](https://issues.apache.org/jira/browse/SPARK-2491) Looking at [SPARK-3052](https://issues.apache.org/jira/browse/SPARK-3052), it seems like it's possible to get spurious exceptions there when there is an error reading from Hadoop. If the Hadoop FileSystem were to get into an error state _right_ after reading the last record then it looks like we could hit the bug here in 1.5. ## The fix This patch guards against these issues by modifying `HadoopRDD.close()` and `NewHadoopRDD.close()` so that they set `reader = null` even if an exception occurs in the `reader.close()` call. In addition, I modified `NextIterator. closeIfNeeded()` to guard against double-close if the first `close()` call throws an exception. I don't have an easy way to test this, since I haven't been able to reproduce the bug that prompted this patch, but these changes seem safe and seem to rule out the on-paper reproductions that I was able to brainstorm up. Author: Josh Rosen <[email protected]> Closes #9382 from JoshRosen/hadoop-decompressor-pooling-fix and squashes the following commits: 5ec97d7 [Josh Rosen] Add SqlNewHadoopRDD.unsetInputFileName() that I accidentally deleted. ae46cf4 [Josh Rosen] Merge remote-tracking branch 'origin/master' into hadoop-decompressor-pooling-fix 087aa63 [Josh Rosen] Guard against double-close() of RecordReaders.
1 parent 97b3c8f commit ac4118d

File tree

4 files changed

+66
-52
lines changed

4 files changed

+66
-52
lines changed

core/src/main/scala/org/apache/spark/rdd/HadoopRDD.scala

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -251,8 +251,21 @@ class HadoopRDD[K, V](
251251
}
252252

253253
override def close() {
254-
try {
255-
reader.close()
254+
if (reader != null) {
255+
// Close the reader and release it. Note: it's very important that we don't close the
256+
// reader more than once, since that exposes us to MAPREDUCE-5918 when running against
257+
// Hadoop 1.x and older Hadoop 2.x releases. That bug can lead to non-deterministic
258+
// corruption issues when reading compressed input.
259+
try {
260+
reader.close()
261+
} catch {
262+
case e: Exception =>
263+
if (!ShutdownHookManager.inShutdown()) {
264+
logWarning("Exception in RecordReader.close()", e)
265+
}
266+
} finally {
267+
reader = null
268+
}
256269
if (bytesReadCallback.isDefined) {
257270
inputMetrics.updateBytesRead()
258271
} else if (split.inputSplit.value.isInstanceOf[FileSplit] ||
@@ -266,12 +279,6 @@ class HadoopRDD[K, V](
266279
logWarning("Unable to get input size to set InputMetrics for task", e)
267280
}
268281
}
269-
} catch {
270-
case e: Exception => {
271-
if (!ShutdownHookManager.inShutdown()) {
272-
logWarning("Exception in RecordReader.close()", e)
273-
}
274-
}
275282
}
276283
}
277284
}

core/src/main/scala/org/apache/spark/rdd/NewHadoopRDD.scala

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -184,30 +184,32 @@ class NewHadoopRDD[K, V](
184184
}
185185

186186
private def close() {
187-
try {
188-
if (reader != null) {
189-
// Close reader and release it
187+
if (reader != null) {
188+
// Close the reader and release it. Note: it's very important that we don't close the
189+
// reader more than once, since that exposes us to MAPREDUCE-5918 when running against
190+
// Hadoop 1.x and older Hadoop 2.x releases. That bug can lead to non-deterministic
191+
// corruption issues when reading compressed input.
192+
try {
190193
reader.close()
191-
reader = null
192-
193-
if (bytesReadCallback.isDefined) {
194-
inputMetrics.updateBytesRead()
195-
} else if (split.serializableHadoopSplit.value.isInstanceOf[FileSplit] ||
196-
split.serializableHadoopSplit.value.isInstanceOf[CombineFileSplit]) {
197-
// If we can't get the bytes read from the FS stats, fall back to the split size,
198-
// which may be inaccurate.
199-
try {
200-
inputMetrics.incBytesRead(split.serializableHadoopSplit.value.getLength)
201-
} catch {
202-
case e: java.io.IOException =>
203-
logWarning("Unable to get input size to set InputMetrics for task", e)
194+
} catch {
195+
case e: Exception =>
196+
if (!ShutdownHookManager.inShutdown()) {
197+
logWarning("Exception in RecordReader.close()", e)
204198
}
205-
}
199+
} finally {
200+
reader = null
206201
}
207-
} catch {
208-
case e: Exception => {
209-
if (!ShutdownHookManager.inShutdown()) {
210-
logWarning("Exception in RecordReader.close()", e)
202+
if (bytesReadCallback.isDefined) {
203+
inputMetrics.updateBytesRead()
204+
} else if (split.serializableHadoopSplit.value.isInstanceOf[FileSplit] ||
205+
split.serializableHadoopSplit.value.isInstanceOf[CombineFileSplit]) {
206+
// If we can't get the bytes read from the FS stats, fall back to the split size,
207+
// which may be inaccurate.
208+
try {
209+
inputMetrics.incBytesRead(split.serializableHadoopSplit.value.getLength)
210+
} catch {
211+
case e: java.io.IOException =>
212+
logWarning("Unable to get input size to set InputMetrics for task", e)
211213
}
212214
}
213215
}

core/src/main/scala/org/apache/spark/rdd/SqlNewHadoopRDD.scala

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -189,32 +189,35 @@ private[spark] class SqlNewHadoopRDD[V: ClassTag](
189189
}
190190

191191
private def close() {
192-
try {
193-
if (reader != null) {
192+
if (reader != null) {
193+
SqlNewHadoopRDD.unsetInputFileName()
194+
// Close the reader and release it. Note: it's very important that we don't close the
195+
// reader more than once, since that exposes us to MAPREDUCE-5918 when running against
196+
// Hadoop 1.x and older Hadoop 2.x releases. That bug can lead to non-deterministic
197+
// corruption issues when reading compressed input.
198+
try {
194199
reader.close()
195-
reader = null
196-
197-
SqlNewHadoopRDD.unsetInputFileName()
198-
199-
if (bytesReadCallback.isDefined) {
200-
inputMetrics.updateBytesRead()
201-
} else if (split.serializableHadoopSplit.value.isInstanceOf[FileSplit] ||
202-
split.serializableHadoopSplit.value.isInstanceOf[CombineFileSplit]) {
203-
// If we can't get the bytes read from the FS stats, fall back to the split size,
204-
// which may be inaccurate.
205-
try {
206-
inputMetrics.incBytesRead(split.serializableHadoopSplit.value.getLength)
207-
} catch {
208-
case e: java.io.IOException =>
209-
logWarning("Unable to get input size to set InputMetrics for task", e)
200+
} catch {
201+
case e: Exception =>
202+
if (!ShutdownHookManager.inShutdown()) {
203+
logWarning("Exception in RecordReader.close()", e)
210204
}
211-
}
205+
} finally {
206+
reader = null
212207
}
213-
} catch {
214-
case e: Exception =>
215-
if (!ShutdownHookManager.inShutdown()) {
216-
logWarning("Exception in RecordReader.close()", e)
208+
if (bytesReadCallback.isDefined) {
209+
inputMetrics.updateBytesRead()
210+
} else if (split.serializableHadoopSplit.value.isInstanceOf[FileSplit] ||
211+
split.serializableHadoopSplit.value.isInstanceOf[CombineFileSplit]) {
212+
// If we can't get the bytes read from the FS stats, fall back to the split size,
213+
// which may be inaccurate.
214+
try {
215+
inputMetrics.incBytesRead(split.serializableHadoopSplit.value.getLength)
216+
} catch {
217+
case e: java.io.IOException =>
218+
logWarning("Unable to get input size to set InputMetrics for task", e)
217219
}
220+
}
218221
}
219222
}
220223
}

core/src/main/scala/org/apache/spark/util/NextIterator.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,10 @@ private[spark] abstract class NextIterator[U] extends Iterator[U] {
6060
*/
6161
def closeIfNeeded() {
6262
if (!closed) {
63-
close()
63+
// Note: it's important that we set closed = true before calling close(), since setting it
64+
// afterwards would permit us to call close() multiple times if close() threw an exception.
6465
closed = true
66+
close()
6567
}
6668
}
6769

0 commit comments

Comments
 (0)