Skip to content

Commit 57e73da

Browse files
viiryasunchao
authored andcommitted
[SPARK-45678][CORE] Cover BufferReleasingInputStream.available/reset under tryOrFetchFailedException
### What changes were proposed in this pull request? This patch proposes to wrap `BufferReleasingInputStream.available/reset` under `tryOrFetchFailedException`. So `IOException` during `available`/`reset` call will be rethrown as `FetchFailedException`. ### Why are the changes needed? We have encountered shuffle data corruption issue: ``` Caused by: java.io.IOException: FAILED_TO_UNCOMPRESS(5) at org.xerial.snappy.SnappyNative.throw_error(SnappyNative.java:112) at org.xerial.snappy.SnappyNative.rawUncompress(Native Method) at org.xerial.snappy.Snappy.rawUncompress(Snappy.java:504) at org.xerial.snappy.Snappy.uncompress(Snappy.java:543) at org.xerial.snappy.SnappyInputStream.hasNextChunk(SnappyInputStream.java:450) at org.xerial.snappy.SnappyInputStream.available(SnappyInputStream.java:497) at org.apache.spark.storage.BufferReleasingInputStream.available(ShuffleBlockFetcherIterator.scala:1356) ``` Spark shuffle has capacity to detect corruption for a few stream op like `read` and `skip`, such `IOException` in the stack trace will be rethrown as `FetchFailedException` that will re-try the failed shuffle task. But in the stack trace it is `available` that is not covered by the mechanism. So no-retry has been happened and the Spark application just failed. As the `available`/`reset` op will also involve data decompression and throw `IOException`, we should be able to check it like `read` and `skip` do. ### Does this PR introduce _any_ user-facing change? Yes. Data corruption during `available`/`reset` op is now causing `FetchFailedException` like `read` and `skip` that can be retried instead of `IOException`. ### How was this patch tested? Added test. ### Was this patch authored or co-authored using generative AI tooling? No Closes #43543 from viirya/add_available. Authored-by: Liang-Chi Hsieh <[email protected]> Signed-off-by: Chao Sun <[email protected]>
1 parent aa232f2 commit 57e73da

File tree

2 files changed

+68
-4
lines changed

2 files changed

+68
-4
lines changed

core/src/main/scala/org/apache/spark/storage/ShuffleBlockFetcherIterator.scala

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1354,7 +1354,8 @@ private class BufferReleasingInputStream(
13541354
}
13551355
}
13561356

1357-
override def available(): Int = delegate.available()
1357+
override def available(): Int =
1358+
tryOrFetchFailedException(delegate.available())
13581359

13591360
override def mark(readlimit: Int): Unit = delegate.mark(readlimit)
13601361

@@ -1369,12 +1370,13 @@ private class BufferReleasingInputStream(
13691370
override def read(b: Array[Byte], off: Int, len: Int): Int =
13701371
tryOrFetchFailedException(delegate.read(b, off, len))
13711372

1372-
override def reset(): Unit = delegate.reset()
1373+
override def reset(): Unit = tryOrFetchFailedException(delegate.reset())
13731374

13741375
/**
13751376
* Execute a block of code that returns a value, close this stream quietly and re-throwing
13761377
* IOException as FetchFailedException when detectCorruption is true. This method is only
1377-
* used by the `read` and `skip` methods inside `BufferReleasingInputStream` currently.
1378+
* used by the `available`, `read` and `skip` methods inside `BufferReleasingInputStream`
1379+
* currently.
13781380
*/
13791381
private def tryOrFetchFailedException[T](block: => T): T = {
13801382
try {

core/src/test/scala/org/apache/spark/storage/ShuffleBlockFetcherIteratorSuite.scala

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ class ShuffleBlockFetcherIteratorSuite extends SparkFunSuite with PrivateMethodT
182182
blocksByAddress: Map[BlockManagerId, Seq[(BlockId, Long, Int)]],
183183
taskContext: Option[TaskContext] = None,
184184
streamWrapperLimitSize: Option[Long] = None,
185+
corruptAtAvailableReset: Boolean = false,
185186
blockManager: Option[BlockManager] = None,
186187
maxBytesInFlight: Long = Long.MaxValue,
187188
maxReqsInFlight: Int = Int.MaxValue,
@@ -201,7 +202,14 @@ class ShuffleBlockFetcherIteratorSuite extends SparkFunSuite with PrivateMethodT
201202
blockManager.getOrElse(createMockBlockManager()),
202203
mapOutputTracker,
203204
blocksByAddress.iterator,
204-
(_, in) => streamWrapperLimitSize.map(new LimitedInputStream(in, _)).getOrElse(in),
205+
(_, in) => {
206+
val limited = streamWrapperLimitSize.map(new LimitedInputStream(in, _)).getOrElse(in)
207+
if (corruptAtAvailableReset) {
208+
new CorruptAvailableResetStream(limited)
209+
} else {
210+
limited
211+
}
212+
},
205213
maxBytesInFlight,
206214
maxReqsInFlight,
207215
maxBlocksInFlightPerAddress,
@@ -712,6 +720,16 @@ class ShuffleBlockFetcherIteratorSuite extends SparkFunSuite with PrivateMethodT
712720
corruptBuffer
713721
}
714722

723+
private class CorruptAvailableResetStream(in: InputStream) extends InputStream {
724+
override def read(): Int = in.read()
725+
726+
override def read(dest: Array[Byte], off: Int, len: Int): Int = in.read(dest, off, len)
727+
728+
override def available(): Int = throw new IOException("corrupt at available")
729+
730+
override def reset(): Unit = throw new IOException("corrupt at reset")
731+
}
732+
715733
private class CorruptStream(corruptAt: Long = 0L) extends InputStream {
716734
var pos = 0
717735
var closed = false
@@ -1879,4 +1897,48 @@ class ShuffleBlockFetcherIteratorSuite extends SparkFunSuite with PrivateMethodT
18791897
blockManager = Some(blockManager), streamWrapperLimitSize = Some(100))
18801898
verifyLocalBlocksFromFallback(iterator)
18811899
}
1900+
1901+
test("SPARK-45678: retry corrupt blocks on available() and reset()") {
1902+
val remoteBmId = BlockManagerId("test-client-1", "test-client-1", 2)
1903+
val blocks = Map[BlockId, ManagedBuffer](
1904+
ShuffleBlockId(0, 0, 0) -> createMockManagedBuffer()
1905+
)
1906+
1907+
// Semaphore to coordinate event sequence in two different threads.
1908+
val sem = new Semaphore(0)
1909+
1910+
answerFetchBlocks { invocation =>
1911+
val listener = invocation.getArgument[BlockFetchingListener](4)
1912+
Future {
1913+
listener.onBlockFetchSuccess(
1914+
ShuffleBlockId(0, 0, 0).toString, createMockManagedBuffer())
1915+
sem.release()
1916+
}
1917+
}
1918+
1919+
val iterator = createShuffleBlockIteratorWithDefaults(
1920+
Map(remoteBmId -> toBlockList(blocks.keys, 1L, 0)),
1921+
streamWrapperLimitSize = Some(100),
1922+
detectCorruptUseExtraMemory = false, // Don't use `ChunkedByteBufferInputStream`.
1923+
corruptAtAvailableReset = true,
1924+
checksumEnabled = false
1925+
)
1926+
1927+
sem.acquire()
1928+
1929+
val (id1, stream) = iterator.next()
1930+
assert(id1 === ShuffleBlockId(0, 0, 0))
1931+
1932+
val err1 = intercept[FetchFailedException] {
1933+
stream.available()
1934+
}
1935+
1936+
assert(err1.getMessage.contains("corrupt at available"))
1937+
1938+
val err2 = intercept[FetchFailedException] {
1939+
stream.reset()
1940+
}
1941+
1942+
assert(err2.getMessage.contains("corrupt at reset"))
1943+
}
18821944
}

0 commit comments

Comments
 (0)