This is an automated email from the ASF dual-hosted git repository.
viirya pushed a commit to branch branch-3.5
in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/branch-3.5 by this push:
new f2be14e9a51b [SPARK-59804][3.5][UI] Fix UTF-8 byte limits and physical
line numbers during event log replay
f2be14e9a51b is described below
commit f2be14e9a51b2d9bb5a3b88a4fccf3611eaad857
Author: Liang-Chi Hsieh <[email protected]>
AuthorDate: Wed Sep 30 17:30:52 2026 -0700
[SPARK-59804][3.5][UI] Fix UTF-8 byte limits and physical line numbers
during event log replay
### What changes were proposed in this pull request?
Backport #59074 (SPARK-59804, master commit
719947a5652f83a80492db50a6f1e5fe14d9e6b8) to `branch-3.5`.
Adapt the replay diagnostics to the string-based logging API in Spark 3.5,
preserving the messages and exception behavior. Add explicit filter parameter
types where Scala 2.12 cannot infer them for overloaded replay calls. Retain
the branch's `3.5.10` configuration version.
The backport includes UTF-8 byte accounting, physical-line diagnostics, the
256 MiB default and 512 MiB maximum, the restored no-argument constructor,
early release of skipped-line buffers, bounded malformed-line excerpts,
documentation, and regression tests. Independent follow-ups from the original
PR remain outside this backport.
### Why are the changes needed?
`branch-3.5` contains SPARK-59407 (8defa3c9418), so it has the same
byte-limit, diagnostic, and constructor regressions fixed by #59074. The master
patch needs logging API adaptation on this branch.
### Does this PR introduce _any_ user-facing change?
Yes, the same fixes as #59074: limits measure UTF-8 content bytes excluding
line endings; the default is 256 MiB; non-positive and above-maximum settings
resolve to 512 MiB. Replay preserves physical line numbers across skipped
lines, bounds malformed-line excerpts, and restores the JVM no-argument
constructor.
### How was this patch tested?
On branch-3.5 with Java 17.0.16 and Scala 2.12.18, using the company Maven
proxy:
```
build/sbt 'core/testOnly *ReplayListenerSuite'
build/sbt 'core/Compile/scalastyle' 'core/Test/scalastyle'
```
All 19 tests and both scalastyle checks passed. The tests cover UTF-8
boundaries, normalized constructor replay, reflective no-argument construction,
buffer release, physical-line diagnostics, bounded error excerpts, and IO
exception handling. Existing compressed/end-to-end tests retain the assertion
limitations documented in #59074; their independent follow-up remains outside
this backport.
Also checked the compiled class with `javap`: both constructors and the
Scala default-argument method are present.
### Was this patch authored or co-authored using generative AI tooling?
Generated-by: OpenAI Codex (GPT-6)
Closes #59166 from viirya/codex/replay-line-limits-35.
Authored-by: Liang-Chi Hsieh <[email protected]>
Signed-off-by: Liang-Chi Hsieh <[email protected]>
---
.../org/apache/spark/internal/config/History.scala | 17 +-
.../apache/spark/scheduler/ReplayListenerBus.scala | 144 ++++++++++----
.../spark/scheduler/ReplayListenerSuite.scala | 219 +++++++++++++++++++++
docs/monitoring.md | 14 +-
4 files changed, 342 insertions(+), 52 deletions(-)
diff --git a/core/src/main/scala/org/apache/spark/internal/config/History.scala
b/core/src/main/scala/org/apache/spark/internal/config/History.scala
index 6d5c5b80f354..d620dd38a734 100644
--- a/core/src/main/scala/org/apache/spark/internal/config/History.scala
+++ b/core/src/main/scala/org/apache/spark/internal/config/History.scala
@@ -122,17 +122,24 @@ private[spark] object History {
.bytesConf(ByteUnit.BYTE)
.createWithDefaultString("1m")
+ val EVENT_LOG_MAX_LINE_LENGTH_LIMIT: Int = 512 * 1024 * 1024
+
val EVENT_LOG_MAX_LINE_LENGTH =
ConfigBuilder("spark.history.fs.eventLog.maxLineLength")
- .doc("Maximum length of a single event log line during replay. Lines
longer than " +
- "this are skipped with a warning instead of being read into memory,
bounding the " +
- "memory replay can use when an event log is corrupt or unexpectedly
large. Setting " +
- "this to 0 or a negative value disables the limit. " +
+ .doc("Maximum UTF-8 byte length of a single event log line during
replay, excluding " +
+ "the line ending. Longer lines are skipped with a warning, bounding
the " +
+ "memory replay can use when an event log is corrupt or unexpectedly
large. Buffer " +
+ "growth, UTF-16 storage and JSON parsing can require several times
this limit in heap " +
+ "space. Buffer capacity grows in steps: reducing 256m to 200m or 150m
may not reduce " +
+ "the retained buffer; use 128m to reach a smaller capacity. Values at
or below 0, " +
+ s"or above $EVENT_LOG_MAX_LINE_LENGTH_LIMIT, use the maximum supported
limit of " +
+ s"$EVENT_LOG_MAX_LINE_LENGTH_LIMIT bytes (512 MiB). This cap avoids
JVM array-size " +
+ "limits but does not guarantee sufficient heap space. " +
"Introduced in 4.3.0; also available in 3.5.10, 4.0.5, 4.1.4 and
4.2.1; and in " +
"all versions after 4.3.0.")
.version("3.5.10")
.bytesConf(ByteUnit.BYTE)
- .createWithDefaultString("512m")
+ .createWithDefaultString("256m")
private[spark] val EVENT_LOG_ROLLING_MAX_FILES_TO_RETAIN =
ConfigBuilder("spark.history.fs.eventLog.rolling.maxFilesToRetain")
diff --git
a/core/src/main/scala/org/apache/spark/scheduler/ReplayListenerBus.scala
b/core/src/main/scala/org/apache/spark/scheduler/ReplayListenerBus.scala
index c38561f45a88..e715fe5969f5 100644
--- a/core/src/main/scala/org/apache/spark/scheduler/ReplayListenerBus.scala
+++ b/core/src/main/scala/org/apache/spark/scheduler/ReplayListenerBus.scala
@@ -22,7 +22,7 @@ import java.nio.charset.{CodingErrorAction, StandardCharsets}
import scala.annotation.tailrec
-import com.fasterxml.jackson.core.JsonParseException
+import com.fasterxml.jackson.core.{JsonParseException, JsonProcessingException}
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException
import org.apache.spark.SparkConf
@@ -34,15 +34,21 @@ import org.apache.spark.util.JsonProtocol
/**
* A SparkListenerBus that can be used to replay events from serialized event
data.
*
- * @param maxLineLength Maximum number of bytes (1/2 chars) of a single event
log line that will be
- * materialized during replay. Longer lines are drained,
skipped and
- * logged, bounding the memory replay can use when an
event log is
- * corrupt or unexpectedly large.
+ * @param maxLineLength Maximum UTF-8 byte length of a single event log line,
excluding its line
+ * ending. Longer lines are drained, skipped and logged,
bounding the
+ * memory replay can use when an event log is corrupt or
unexpectedly large.
+ * Values at or below zero or above MAX_LINE_LENGTH use
MAX_LINE_LENGTH
+ * (512 MiB).
*/
private[spark] class ReplayListenerBus(
maxLineLength: Int = ReplayListenerBus.DEFAULT_MAX_LINE_LENGTH)
extends SparkListenerBus with Logging {
+ def this() = this(ReplayListenerBus.DEFAULT_MAX_LINE_LENGTH)
+
+ private[scheduler] val effectiveMaxLineLength =
+ ReplayListenerBus.normalizeMaxLineLength(maxLineLength)
+
/**
* Replay each event in the order maintained in the given stream. The stream
is expected to
* contain one JSON-encoded SparkListenerEvent per line.
@@ -66,22 +72,26 @@ private[spark] class ReplayListenerBus(
maybeTruncated: Boolean = false,
eventsFilter: ReplayEventsFilter = SELECT_ALL_FILTER): Boolean = {
val lines = boundedLines(logData, sourceName)
- replay(lines, sourceName, maybeTruncated, eventsFilter)
+ replayEntries(lines, sourceName, maybeTruncated, eventsFilter)
}
/**
- * Reads '\n'-terminated lines like Source.getLines(), but never
materializes more than
- * `maxLineLength` bytes of a single line. An over-long line is drained and
skipped
- * with a warning instead of being turned into a String.
+ * Reads '\n'-terminated lines and retains their original zero-based
indices. The limit
+ * measures UTF-8 content bytes, excluding the line ending, rather than JVM
heap usage.
+ * The character buffer is bounded by the limit plus one possible trailing
CR. An over-long
+ * line is drained and skipped with a warning instead of being turned into a
String.
*/
- private def boundedLines(logData: InputStream, sourceName: String):
Iterator[String] = {
+ private def boundedLines(
+ logData: InputStream,
+ sourceName: String): Iterator[(String, Int)] = {
// Fail on malformed input like Source.getLines() does instead of
replacing it.
val decoder = StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
val reader = new BufferedReader(new InputStreamReader(logData, decoder))
- new Iterator[String] {
- private var nextLine: String = _
+ new Iterator[(String, Int)] {
+ private var nextLine: (String, Int) = _
+ private var lineIndex = 0
private var lineFetched = false
private var warned = false
@@ -93,7 +103,7 @@ private[spark] class ReplayListenerBus(
nextLine != null
}
- override def next(): String = {
+ override def next(): (String, Int) = {
if (!hasNext) {
throw new NoSuchElementException("No more lines")
}
@@ -103,34 +113,30 @@ private[spark] class ReplayListenerBus(
line
}
- @tailrec private def fetchLine(): String = {
- val sb = new java.lang.StringBuilder()
- var overLong = false
+ @tailrec private def fetchLine(): (String, Int) = {
+ val buffer = new BoundedLineBuffer(effectiveMaxLineLength)
var c = reader.read()
if (c == -1) {
null
} else {
+ val index = lineIndex
+ lineIndex += 1
while (c != -1 && c != '\n') {
- if (sb.length() * 2 < maxLineLength) {
- sb.append(c.toChar)
- } else {
- overLong = true
- }
+ buffer.append(c.toChar)
c = reader.read()
}
- if (overLong) {
+ val maybeLine = buffer.result()
+ if (maybeLine.isEmpty) {
if (!warned) {
- logWarning(s"Skipped event log lines longer than $maxLineLength
" +
- s"characters in $sourceName")
+ logWarning("Skipped event log lines longer than " +
+ s"$effectiveMaxLineLength bytes in " +
+ s"$sourceName; first skipped line: ${index + 1}")
warned = true
}
+ logDebug(s"Skipped event log line ${index + 1} in $sourceName")
fetchLine()
} else {
- // Handle CRLF line endings like Source.getLines() does.
- if (sb.length() > 0 && sb.charAt(sb.length() - 1) == '\r') {
- sb.setLength(sb.length() - 1)
- }
- sb.toString
+ (maybeLine.get, index)
}
}
}
@@ -146,15 +152,21 @@ private[spark] class ReplayListenerBus(
sourceName: String,
maybeTruncated: Boolean,
eventsFilter: ReplayEventsFilter): Boolean = {
+ replayEntries(lines.zipWithIndex, sourceName, maybeTruncated, eventsFilter)
+ }
+
+ private def replayEntries(
+ lines: Iterator[(String, Int)],
+ sourceName: String,
+ maybeTruncated: Boolean,
+ eventsFilter: ReplayEventsFilter): Boolean = {
var currentLine: String = null
var lineNumber: Int = 0
val unrecognizedEvents = new scala.collection.mutable.HashSet[String]
val unrecognizedProperties = new scala.collection.mutable.HashSet[String]
try {
- val lineEntries = lines
- .zipWithIndex
- .filter { case (line, _) => eventsFilter(line) }
+ val lineEntries = lines.filter { case (line, _) => eventsFilter(line) }
while (lineEntries.hasNext) {
try {
@@ -199,11 +211,18 @@ private[spark] class ReplayListenerBus(
// Just stop replay.
false
case _: EOFException if maybeTruncated => false
+ case jpe: JsonProcessingException =>
+ logError(s"Exception parsing Spark event log: $sourceName " +
+ s"at line $lineNumber")
+ throw jpe
case ioe: IOException =>
throw ioe
case e: Exception =>
logError(s"Exception parsing Spark event log: $sourceName", e)
- logError(s"Malformed line #$lineNumber: $currentLine\n")
+ val prefix = Option(currentLine).map(_.take(1024)).orNull
+ val length = Option(currentLine).fold(0)(_.length)
+ logError(s"Malformed line #$lineNumber: $prefix " +
+ s"(line length: $length characters; showing at most 1024)\n")
false
}
}
@@ -222,21 +241,62 @@ private[spark] class HaltReplayException extends
RuntimeException
private[spark] object ReplayListenerBus {
- /**
- * Default per-line cap during replay: far above any legitimate event line,
so replay
- * memory stays bounded even for corrupt logs. Matches the default of
- * spark.history.fs.eventLog.maxLineLength.
- */
- val DEFAULT_MAX_LINE_LENGTH: Int = 512 * 1024 * 1024
+ /** Default UTF-8 content limit shared with the history server
configuration. */
+ val DEFAULT_MAX_LINE_LENGTH: Int =
History.EVENT_LOG_MAX_LINE_LENGTH.defaultValue.get.toInt
+
+ // Bound StringBuilder growth so UTF-16 inflation stays below the JVM
array-size limit.
+ val MAX_LINE_LENGTH: Int = History.EVENT_LOG_MAX_LINE_LENGTH_LIMIT
- /** Resolves the replay line-length cap from configuration; <= 0 disables
the cap. */
+ /** Resolves the byte limit, using 512 MiB for non-positive or larger
values. */
def maxLineLength(conf: SparkConf): Int = {
- val configured = conf.get(History.EVENT_LOG_MAX_LINE_LENGTH)
- if (configured <= 0 || configured > Int.MaxValue) Int.MaxValue else
configured.toInt
+ normalizeMaxLineLength(conf.get(History.EVENT_LOG_MAX_LINE_LENGTH))
+ }
+
+ private def normalizeMaxLineLength(configured: Long): Int = {
+ if (configured <= 0 || configured > MAX_LINE_LENGTH) MAX_LINE_LENGTH else
configured.toInt
}
type ReplayEventsFilter = (String) => Boolean
// utility filter that selects all event logs during replay
val SELECT_ALL_FILTER: ReplayEventsFilter = { (eventString: String) => true }
+
+ /** Accumulates a bounded prefix of one decoded line, allowing a possible
trailing CR. */
+ private[scheduler] class BoundedLineBuffer(maxLineLength: Int) {
+ private val buffer = new java.lang.StringBuilder()
+ private var byteLength = 0L
+ private val bufferLimit = maxLineLength.toLong + 1
+
+ def length: Int = buffer.length()
+
+ def capacity: Int = buffer.capacity()
+
+ def append(c: Char): Unit = {
+ if (byteLength <= bufferLimit) {
+ // The strict decoder emits valid surrogate pairs: count two bytes per
half.
+ byteLength += (if (c < 0x80) 1 else if (c < 0x800 ||
Character.isSurrogate(c)) 2 else 3)
+ // Reserve one extra byte for a possible CR in the line ending.
+ if (byteLength <= bufferLimit) {
+ buffer.append(c)
+ } else {
+ // The line will be skipped; release the retained prefix while
draining.
+ buffer.setLength(0)
+ buffer.trimToSize()
+ }
+ }
+ }
+
+ def result(): Option[String] = {
+ val trailingCR = length > 0 && buffer.charAt(length - 1) == '\r'
+ val contentLength = byteLength - (if (trailingCR) 1 else 0)
+ if (contentLength > maxLineLength) {
+ None
+ } else if (trailingCR) {
+ Some(buffer.substring(0, length - 1))
+ } else {
+ Some(buffer.toString)
+ }
+ }
+ }
+
}
diff --git
a/core/src/test/scala/org/apache/spark/scheduler/ReplayListenerSuite.scala
b/core/src/test/scala/org/apache/spark/scheduler/ReplayListenerSuite.scala
index eba646fc9fae..7823afe8521e 100644
--- a/core/src/test/scala/org/apache/spark/scheduler/ReplayListenerSuite.scala
+++ b/core/src/test/scala/org/apache/spark/scheduler/ReplayListenerSuite.scala
@@ -23,13 +23,17 @@ import java.util.concurrent.atomic.AtomicInteger
import scala.collection.mutable.ArrayBuffer
+import com.fasterxml.jackson.core.{JsonParseException, JsonProcessingException}
+import com.fasterxml.jackson.databind.exc.MismatchedInputException
import org.apache.hadoop.fs.Path
+import org.apache.logging.log4j.Level
import org.scalatest.BeforeAndAfter
import org.apache.spark._
import org.apache.spark.deploy.SparkHadoopUtil
import org.apache.spark.deploy.history.EventLogFileReader
import org.apache.spark.deploy.history.EventLogTestHelper._
+import org.apache.spark.internal.config.History
import org.apache.spark.io.{CompressionCodec, LZ4CompressionCodec}
import org.apache.spark.util.{JsonProtocol, JsonProtocolSuite, Utils}
@@ -109,6 +113,221 @@ class ReplayListenerSuite extends SparkFunSuite with
BeforeAndAfter with LocalSp
assert(eventMonster.loggedEvents(1) ===
JsonProtocol.sparkEventToJsonString(applicationEnd))
}
+ test("SPARK-59804: Replay line limit uses UTF-8 bytes") {
+ val start = SparkListenerApplicationStart("x" * 6000, None, 125L, "user",
None)
+ val json = JsonProtocol.sparkEventToJsonString(start)
+ assert(json.getBytes(StandardCharsets.UTF_8).length < 8 * 1024)
+ val listener = new EventBufferingListener
+ val conf = new SparkConf(false).set(History.EVENT_LOG_MAX_LINE_LENGTH.key,
"8k")
+ val bus = new ReplayListenerBus(ReplayListenerBus.maxLineLength(conf))
+ bus.addListener(listener)
+ val input = new ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8))
+ assert(bus.replay(input, "ascii"))
+ assert(listener.loggedEvents.toSeq == Seq(json))
+ }
+
+ test("SPARK-59804: Replay line limit handles raw UTF-8 boundaries and line
endings") {
+ // scalastyle:off nonascii
+ val suffixes = Seq("x", "\u00e9", "\u4e2d", "\ud83d\ude00")
+ // scalastyle:on nonascii
+ for {
+ suffix <- suffixes
+ padding <- Seq(8, 8190, 8191, 8192)
+ ending <- Seq("\n", "\r\n", "")
+ delta <- -3 to 2
+ } {
+ // Put the limit inside the final code point, and split UTF-8 sequences
across read buffers.
+ val line = "x" * padding + suffix
+ val bytes = line.getBytes(StandardCharsets.UTF_8)
+ val limit = bytes.length + delta
+ val seen = new ArrayBuffer[String]
+ val bus = new ReplayListenerBus(limit)
+ val input = new ByteArrayInputStream((line +
ending).getBytes(StandardCharsets.UTF_8))
+ withClue(s"codePoint=${suffix.codePointAt(0)} padding=$padding " +
+ s"ending=${ending.map(_.toInt).mkString(",")} delta=$delta: ") {
+ // Observe raw lines before JSON parsing, without letting Jackson
escape the test input.
+ assert(bus.replay(input, "utf8", eventsFilter = line => {
+ seen += line
+ false
+ }))
+ assert(seen.toSeq == (if (delta >= 0) Seq(line) else Seq.empty))
+ }
+ }
+ }
+
+ test("SPARK-59804: Replay line buffer stops retaining oversized content") {
+ for (limit <- Seq(0, 1, 8, 1024)) {
+ val buffer = new ReplayListenerBus.BoundedLineBuffer(limit)
+ for (_ <- 0 until 10000) {
+ buffer.append('x')
+ assert(buffer.length <= limit + 1)
+ }
+ assert(buffer.length == 0)
+ assert(buffer.capacity == 0)
+ assert(buffer.result().isEmpty)
+ }
+ }
+
+ test("SPARK-59804: Replay retains a public no-argument constructor") {
+ val bus = classOf[ReplayListenerBus].getConstructor().newInstance()
+ assert(bus.effectiveMaxLineLength ==
ReplayListenerBus.DEFAULT_MAX_LINE_LENGTH)
+ }
+
+ test("SPARK-59804: Replay reports the physical location of skipped lines") {
+ val end =
JsonProtocol.sparkEventToJsonString(SparkListenerApplicationEnd(1000L))
+ val input = new ByteArrayInputStream(
+ Seq(end, "x" * 2048, end, "x" * 2048).mkString("\n")
+ .getBytes(StandardCharsets.UTF_8))
+ val appender = new LogAppender
+ appender.setThreshold(Level.DEBUG)
+ withLogAppender(appender, level = Some(Level.DEBUG)) {
+ assert(new ReplayListenerBus(1024).replay(input, "skipped-lines"))
+ }
+ val warnings = appender.loggingEvents.filter(_.getLevel == Level.WARN)
+ assert(warnings.size == 1)
+ assert(warnings.head.getMessage.getFormattedMessage.contains("first
skipped line: 2"))
+ val debug = appender.loggingEvents.filter(_.getLevel == Level.DEBUG)
+ .map(_.getMessage.getFormattedMessage)
+ assert(debug.contains("Skipped event log line 2 in skipped-lines"))
+ assert(debug.contains("Skipped event log line 4 in skipped-lines"))
+ }
+
+ test("SPARK-59804: Replay bounds malformed line diagnostics") {
+ val line = """{"Event":"SparkListenerJobStart","Job ID":1,"Stage
IDs":[],""" +
+ """"Properties":{"bad":1},"padding":"""" + "x" * 10000 + "tail-marker\"}"
+ val appender = new LogAppender
+ withLogAppender(appender) {
+ assert(!new ReplayListenerBus().replay(
+ new ByteArrayInputStream(line.getBytes(StandardCharsets.UTF_8)),
"semantic-error"))
+ }
+ val diagnostic =
appender.loggingEvents.map(_.getMessage.getFormattedMessage)
+ .find(_.startsWith("Malformed line #1:")).get
+ assert(diagnostic.contains(s"line length: ${line.length} characters"))
+ assert(!diagnostic.contains("tail-marker"))
+ assert(diagnostic.length < 1200)
+ }
+
+ test("SPARK-59804: Replay line limit defaults and maximum supported
configuration") {
+ val conf = new SparkConf(false)
+ assert(conf.get(History.EVENT_LOG_MAX_LINE_LENGTH) == 256L * 1024 * 1024)
+ assert(ReplayListenerBus.maxLineLength(conf) ==
ReplayListenerBus.DEFAULT_MAX_LINE_LENGTH)
+ for (value <- Seq("0", "-1", "512m", "536870913", "576m", "600m", "1g",
+ "2147483647", "3g")) {
+ conf.set(History.EVENT_LOG_MAX_LINE_LENGTH.key, value)
+ assert(ReplayListenerBus.maxLineLength(conf) ==
ReplayListenerBus.MAX_LINE_LENGTH)
+ }
+ }
+
+ test("SPARK-59804: Replay constructor normalizes the effective line limit") {
+ val maximum = ReplayListenerBus.MAX_LINE_LENGTH
+ for (limit <- Seq(Int.MinValue, -1, 0, maximum, maximum + 1,
Int.MaxValue)) {
+ val bus = new ReplayListenerBus(limit)
+ assert(bus.effectiveMaxLineLength == maximum)
+ val listener = new EventBufferingListener
+ bus.addListener(listener)
+ val json =
JsonProtocol.sparkEventToJsonString(SparkListenerApplicationEnd(1000L))
+ assert(bus.replay(new
ByteArrayInputStream(json.getBytes(StandardCharsets.UTF_8)),
+ "normalized"))
+ assert(listener.loggedEvents.toSeq == Seq(json))
+ }
+ for (limit <- Seq(1, 8192, ReplayListenerBus.DEFAULT_MAX_LINE_LENGTH,
maximum - 1)) {
+ assert(new ReplayListenerBus(limit).effectiveMaxLineLength == limit)
+ val conf = new SparkConf(false).set(History.EVENT_LOG_MAX_LINE_LENGTH,
limit.toLong)
+ assert(ReplayListenerBus.maxLineLength(conf) == limit)
+ }
+ assert(new ReplayListenerBus().effectiveMaxLineLength ==
+ ReplayListenerBus.DEFAULT_MAX_LINE_LENGTH)
+ }
+
+ test("SPARK-59804: Replay preserves physical line numbers after skipping
long lines") {
+ val end =
JsonProtocol.sparkEventToJsonString(SparkListenerApplicationEnd(1000L))
+ val appender = new LogAppender
+ val bus = new ReplayListenerBus(1024)
+ val input = new ByteArrayInputStream(
+ (end + "\n" + "x" * 2048 + "\n" + "x" * 2048 + "\n{}\n")
+ .getBytes(StandardCharsets.UTF_8))
+ withLogAppender(appender) {
+ assert(!bus.replay(input, "line-numbers"))
+ }
+ assert(appender.loggingEvents.exists(_.getMessage.getFormattedMessage
+ .contains("Malformed line #4: {}")))
+ }
+
+ test("SPARK-59804: Replay logs physical line numbers before rethrowing JSON
errors") {
+ val end =
JsonProtocol.sparkEventToJsonString(SparkListenerApplicationEnd(1000L))
+ val mappingError =
+
"""{"Event":"org.apache.spark.util.TestListenerEvent","foo":"x","bar":[]}"""
+ for ((malformed, errorClass) <- Seq(
+ ("{bad", classOf[JsonParseException]),
+ (mappingError, classOf[MismatchedInputException]))) {
+ val appender = new LogAppender
+ val bus = new ReplayListenerBus(1024)
+ val input = new ByteArrayInputStream(
+ Seq(end, "x" * 2048, "x" * 2048, malformed, end).mkString("\n")
+ .getBytes(StandardCharsets.UTF_8))
+ withLogAppender(appender) {
+ val error = intercept[JsonProcessingException] {
+ bus.replay(input, "json-errors", maybeTruncated = true)
+ }
+ assert(errorClass.isInstance(error))
+ }
+ val diagnostics =
appender.loggingEvents.filter(_.getMessage.getFormattedMessage
+ .contains("Exception parsing Spark event log: json-errors at line 4"))
+ assert(diagnostics.size == 1)
+ assert(diagnostics.head.getThrown == null)
+ }
+ }
+
+ test("SPARK-59804: Replay propagates stream IO errors without reporting a
stale parse line") {
+ val error = new IOException("read failure")
+ val end =
JsonProtocol.sparkEventToJsonString(SparkListenerApplicationEnd(1000L))
+ val input = new ByteArrayInputStream((end +
"\n").getBytes(StandardCharsets.UTF_8)) {
+ override def read(bytes: Array[Byte], offset: Int, length: Int): Int = {
+ if (available() == 0) throw error
+ super.read(bytes, offset, length)
+ }
+ }
+ val bus = new ReplayListenerBus()
+ val listener = new EventBufferingListener
+ bus.addListener(listener)
+ val appender = new LogAppender
+ withLogAppender(appender) {
+ assert(intercept[IOException] {
+ bus.replay(input, "read-error")
+ } eq error)
+ }
+ assert(listener.loggedEvents.toSeq == Seq(end))
+ assert(!appender.loggingEvents.exists(_.getMessage.getFormattedMessage
+ .contains("Exception parsing Spark event log")))
+ }
+
+ test("SPARK-59804: Replay preserves line numbers for truncated logs and
filtered iterators") {
+ val end =
JsonProtocol.sparkEventToJsonString(SparkListenerApplicationEnd(1000L))
+ val truncated = "{\"Event\":"
+ val appender = new LogAppender
+ val bus = new ReplayListenerBus(1024)
+ val input = new ByteArrayInputStream(
+ ("x" * 2048 + "\n" + end + "\n" +
truncated).getBytes(StandardCharsets.UTF_8))
+ withLogAppender(appender) {
+ assert(bus.replay(input, "truncated", maybeTruncated = true,
+ eventsFilter = (line: String) => line != end))
+ assert(bus.replay(Iterator(end, end, truncated), "iterator",
maybeTruncated = true,
+ eventsFilter = (line: String) => line != end))
+ }
+ val warnings = appender.loggingEvents.map(_.getMessage.getFormattedMessage)
+ .filter(_.contains("Got JsonParseException"))
+ assert(warnings.size == 2)
+ assert(warnings.forall(_.contains("at line 3,")))
+ }
+
+ test("SPARK-59804: Replay still rejects malformed UTF-8 in skipped lines") {
+ val bytes = Array.fill[Byte](20 * 1024)('x'.toByte) ++ Array(0xff.toByte,
'\n'.toByte)
+ val bus = new ReplayListenerBus(1024)
+ intercept[java.nio.charset.MalformedInputException] {
+ bus.replay(new ByteArrayInputStream(bytes), "invalid-utf8")
+ }
+ }
+
/**
* Test replaying compressed spark history file that internally throws an
EOFException. To
* avoid sensitivity to the compression specifics the test forces an
EOFException to occur
diff --git a/docs/monitoring.md b/docs/monitoring.md
index 088fe34f148f..2969667247a2 100644
--- a/docs/monitoring.md
+++ b/docs/monitoring.md
@@ -385,12 +385,16 @@ Security options for the Spark History Server are covered
more detail in the
</tr>
<tr>
<td>spark.history.fs.eventLog.maxLineLength</td>
- <td>512m</td>
+ <td>256m</td>
<td>
- Maximum length of a single event log line during replay. Lines longer
than this are
- skipped with a warning instead of being read into memory, which bounds
the memory replay
- can use when an event log is corrupt or unexpectedly large. Setting this
to 0 or a
- negative value disables the limit.<br/>
+ Maximum UTF-8 byte length of a single event log line during replay,
excluding the line
+ ending. Longer lines are skipped with a warning, which bounds the memory
replay
+ can use when an event log is corrupt or unexpectedly large. Buffer
growth, UTF-16 storage
+ and JSON parsing can require several times this limit in heap space.
Buffer capacity grows
+ in steps: reducing 256m to 200m or 150m may not reduce the retained
buffer; use 128m to
+ reach a smaller capacity. Values at or below
+ 0, or above 536870912, use the maximum supported limit of 536870912
bytes (512 MiB).
+ This cap avoids JVM array-size limits but does not guarantee sufficient
heap space.<br/>
Introduced in 4.3.0; also available in 3.5.10, 4.0.5, 4.1.4 and 4.2.1;
and in all
versions after 4.3.0.
</td>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]