viirya commented on code in PR #59327:
URL: https://github.com/apache/spark/pull/59327#discussion_r4238767477
##########
core/src/main/scala/org/apache/spark/deploy/history/FsHistoryProvider.scala:
##########
@@ -1772,6 +1763,55 @@ private[history] class FsHistoryProvider(conf:
SparkConf, clock: Clock)
private[spark] object FsHistoryProvider {
+ /**
+ * Positions `in` at the start of the first line that begins at or after
byte `target`
+ * (`target` > 0), returning a stream that starts there and the byte offset
of that line.
+ * If the stream ends first, the returned stream is at EOF and the offset is
the stream length.
+ *
+ * Since the skip may land anywhere, including inside a multi-byte UTF-8
character, the line
+ * boundary is found on raw bytes before any decoding: '\n' never occurs
inside a multi-byte
+ * UTF-8 sequence. Bytes read past the boundary are put back in front of the
stream.
+ */
+ private[history] def skipToLineStart(in: InputStream, target: Long):
(InputStream, Long) = {
Review Comment:
Changed in e0f71e55c10. `skipToLineStart` now returns `(in, 0L)` for a
non-positive `target`, and the caller calls it unconditionally (the `else`
branch is gone). The `skipToLineStart` test covers `0` and `-1`.
##########
core/src/test/scala/org/apache/spark/deploy/history/FsHistoryProviderSuite.scala:
##########
@@ -1248,6 +1248,124 @@ abstract class FsHistoryProviderSuite extends
SparkFunSuite with Matchers with P
}
}
+ test("end event reparse when the skip offset lands inside a multi-byte
character") {
+ val log = newLogFile("end-event-utf8", None, inProgress = false)
+ val bytes = writeRawLog(log, endEventTestLines("end-event-utf8",
+ jobStartLine(1, 0x4e2d.toChar.toString * 200)))
+ // The job description is written as raw UTF-8: skip to the second byte of
a character.
+ val leadByte = bytes.indexWhere(b => (b & 0xF0) == 0xE0)
+ assert(leadByte > 0)
+ val conf = createTestConf().set(END_EVENT_REPARSE_CHUNK_SIZE,
(bytes.length - leadByte - 1L))
+ updateAndCheck(new FsHistoryProvider(conf)) { list =>
+ assert(list.size === 1)
+ assert(list(0).attempts.head.completed)
+ }
+ }
+
+ test("end event reparse keeps a line that starts exactly at the skip
offset") {
+ val log = newLogFile("end-event-boundary", None, inProgress = false)
+ val lines = endEventTestLines("end-event-boundary")
+ val bytes = writeRawLog(log, lines)
+ // The application end event is the last line: skip exactly to its first
byte.
+ assert(bytes.length > lineBytes(lines.last))
+ val conf = createTestConf()
+ .set(END_EVENT_REPARSE_CHUNK_SIZE, lineBytes(lines.last).toLong)
+ val appender = new LogAppender("end event reparse")
+ withLogAppender(appender) {
+ updateAndCheck(new FsHistoryProvider(conf)) { list =>
+ assert(list.size === 1)
+ assert(list(0).attempts.head.completed)
+ }
+ }
+ // The end event is found by the reparse, not by the fallback that parses
the whole log.
+ val messages = appender.loggingEvents.map(_.getMessage.getFormattedMessage)
+ assert(!messages.exists(_.contains("since end event was not found")),
messages)
Review Comment:
Changed in e0f71e55c10. The log now has a malformed
`SparkListenerApplicationEnd` line between the environment update and the end
event. The listing pass halts before it and the tail reparse starts after it,
but the whole-log fallback fails on it, so the app is listed as completed only
if the tail reparse finds the end event. The log-message check is removed; the
test fails if the skip goes to `target` instead of `target - 1`.
##########
core/src/main/scala/org/apache/spark/scheduler/ReplayListenerBus.scala:
##########
@@ -215,20 +234,30 @@ private[spark] class ReplayListenerBus(
case _: EOFException if maybeTruncated => false
case jpe: JsonProcessingException =>
logError(log"Exception parsing Spark event log: ${MDC(PATH,
sourceName)} " +
- log"at line ${MDC(LINE_NUM, lineNumber)}")
+ log"at line ${MDC(LINE_NUM, lineNumber)}" + lineOrigin(startOffset))
throw jpe
case ioe: IOException =>
throw ioe
case e: Exception =>
logError(log"Exception parsing Spark event log: ${MDC(PATH,
sourceName)}", e)
val prefix = Option(currentLine).map(_.take(1024)).orNull
val length = Option(currentLine).fold(0)(_.length)
- logError(log"Malformed line #${MDC(LINE_NUM, lineNumber)}: ${MDC(LINE,
prefix)} " +
+ logError(log"Malformed line #${MDC(LINE_NUM, lineNumber)}" +
lineOrigin(startOffset) +
+ log": ${MDC(LINE, prefix)} " +
log"(line length: ${MDC(SIZE, length)} characters; showing at most
1024)\n")
false
}
}
+ /** Describes where line numbers start when the replay does not start at the
source start. */
+ private def lineOrigin(startOffset: Long): MessageWithContext = {
Review Comment:
Added `Replay from an offset reports where line numbers start` to
`ReplayListenerSuite` in e0f71e55c10. It calls `replayFromOffset` with offset
42 and checks the label on the over-long line warning, `Malformed line #N` and
the truncated-file `JsonParseException` warning, and that offset 0 leaves the
diagnostics unlabeled. The `JsonProcessingException` error is covered by the
`FsHistoryProviderSuite` test.
##########
core/src/test/scala/org/apache/spark/deploy/history/FsHistoryProviderSuite.scala:
##########
@@ -1248,6 +1248,124 @@ abstract class FsHistoryProviderSuite extends
SparkFunSuite with Matchers with P
}
}
+ test("end event reparse when the skip offset lands inside a multi-byte
character") {
+ val log = newLogFile("end-event-utf8", None, inProgress = false)
+ val bytes = writeRawLog(log, endEventTestLines("end-event-utf8",
+ jobStartLine(1, 0x4e2d.toChar.toString * 200)))
+ // The job description is written as raw UTF-8: skip to the second byte of
a character.
+ val leadByte = bytes.indexWhere(b => (b & 0xF0) == 0xE0)
+ assert(leadByte > 0)
+ val conf = createTestConf().set(END_EVENT_REPARSE_CHUNK_SIZE,
(bytes.length - leadByte - 1L))
+ updateAndCheck(new FsHistoryProvider(conf)) { list =>
+ assert(list.size === 1)
+ assert(list(0).attempts.head.completed)
+ }
+ }
+
+ test("end event reparse keeps a line that starts exactly at the skip
offset") {
+ val log = newLogFile("end-event-boundary", None, inProgress = false)
+ val lines = endEventTestLines("end-event-boundary")
+ val bytes = writeRawLog(log, lines)
+ // The application end event is the last line: skip exactly to its first
byte.
+ assert(bytes.length > lineBytes(lines.last))
+ val conf = createTestConf()
+ .set(END_EVENT_REPARSE_CHUNK_SIZE, lineBytes(lines.last).toLong)
+ val appender = new LogAppender("end event reparse")
+ withLogAppender(appender) {
+ updateAndCheck(new FsHistoryProvider(conf)) { list =>
+ assert(list.size === 1)
+ assert(list(0).attempts.head.completed)
+ }
+ }
+ // The end event is found by the reparse, not by the fallback that parses
the whole log.
+ val messages = appender.loggingEvents.map(_.getMessage.getFormattedMessage)
+ assert(!messages.exists(_.contains("since end event was not found")),
messages)
+ }
+
+ Seq(None, Some(CompressionCodec.LZ4)).foreach { codecName =>
+ test("end event reparse applies the line length limit and reports relative
line numbers " +
+ s"(codec = ${codecName.getOrElse("none")})") {
+ val log = newLogFile("end-event-limit", None, inProgress = false,
codecName)
+ val tail = Seq(jobStartLine(2, "x" * 2048), jobStartLine(3, "y"))
+ val lines = endEventTestLines("end-event-limit", tail: _*)
+ val codec = codecName.map(CompressionCodec.createCodec(new SparkConf(),
_))
+ writeRawLog(log, lines, codec)
+ // The skip runs on the uncompressed stream while the chunk size is
subtracted from the
+ // file length. Skip into the middle of the first line: replay then
starts at the second
+ // line, and the over-long line is at relative line
`lines.indexOf(tail.head)`.
+ val target = 5L
+ val conf = createTestConf()
+ .set(END_EVENT_REPARSE_CHUNK_SIZE, log.length() - target)
+ .set(EVENT_LOG_MAX_LINE_LENGTH.key, "1k")
+ val appender = new LogAppender("end event reparse")
+ withLogAppender(appender) {
+ updateAndCheck(new FsHistoryProvider(conf)) { list =>
+ assert(list.size === 1)
+ assert(list(0).attempts.head.completed)
+ }
+ }
+ val offset = lineBytes(lines.head)
+ val messages =
appender.loggingEvents.map(_.getMessage.getFormattedMessage)
+ assert(messages.exists(m => m.contains("Skipped event log lines longer
than 1024 bytes") &&
+ m.contains(s"${log.getName}; first skipped line:
${lines.indexOf(tail.head)} " +
+ s"(lines counted from uncompressed byte offset $offset)")), messages)
+ }
+ }
+
+ test("end event reparse reports JSON errors with the skip offset") {
+ val log = newLogFile("end-event-malformed", None, inProgress = false)
+ val malformed = "{\"Event\":\"SparkListenerApplicationEnd\",\"Timestamp\":"
+ val tail = Seq(jobStartLine(2, "x"), malformed, jobStartLine(3, "y"))
+ val lines = endEventTestLines("end-event-malformed", tail: _*)
+ val bytes = writeRawLog(log, lines)
+ val offset = lines.takeWhile(_ != tail.head).map(lineBytes).sum
+ val conf = createTestConf().set(END_EVENT_REPARSE_CHUNK_SIZE, bytes.length
- offset + 10L)
+ val appender = new LogAppender("end event reparse")
+ withLogAppender(appender) {
+ // A completed log fails on a malformed line that is not the last one,
so the application
+ // is not listed.
+ updateAndCheck(new FsHistoryProvider(conf)) { list =>
+ assert(list.isEmpty)
+ }
+ }
+ val messages = appender.loggingEvents.map(_.getMessage.getFormattedMessage)
+ assert(messages.exists(_.contains(s"${log.getName} at line 2 " +
+ s"(lines counted from uncompressed byte offset $offset)")), messages)
+ assert(!messages.exists(_.contains(s"${log.getName} (lines")), messages)
+ }
+
+ test("skipToLineStart aligns to the first line starting at or after the
target") {
+ def alignStream(in: InputStream, target: Long): (String, Long) = {
+ val (rest, offset) = FsHistoryProvider.skipToLineStart(in, target)
+ (new String(rest.readAllBytes(), StandardCharsets.UTF_8), offset)
+ }
+ def align(data: String, target: Long): (String, Long) = {
+ alignStream(new
ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8)), target)
+ }
+ // Inside a line, at its '\n', and exactly at the start of the next line.
+ assert(align("abc\ndef\n", 1) === (("def\n", 4L)))
+ assert(align("abc\ndef\n", 3) === (("def\n", 4L)))
+ assert(align("abc\ndef\n", 4) === (("def\n", 4L)))
+ assert(align("abc\ndef\n", 5) === (("", 8L)))
+ // CRLF line endings, and a target at the '\r'.
+ assert(align("ab\r\ncd", 2) === (("cd", 4L)))
+ assert(align("ab\r\ncd", 4) === (("cd", 4L)))
+ // No line starts at or after the target, or the target is beyond the end
of the stream.
+ assert(align("abc", 1) === (("", 3L)))
+ assert(align("abc\n", 10) === (("", 4L)))
+ // A partial line longer than the read buffer.
+ val long = "x" * 20000 + "\nnext"
+ assert(align(long, 1) === (("next", 20001L)))
+ // A stream whose skip() never advances, e.g. at the end of a decompressed
stream.
+ def noSkip(data: String): InputStream = {
+ new FilterInputStream(new
ByteArrayInputStream(data.getBytes(StandardCharsets.UTF_8))) {
+ override def skip(n: Long): Long = 0L
+ }
+ }
+ assert(alignStream(noSkip("abc\ndef"), 2) === (("def", 4L)))
+ assert(alignStream(noSkip("abc\n"), 100) === (("", 4L)))
Review Comment:
Added `end event reparse stops at the end of a stream shorter than the skip
target` in e0f71e55c10. It writes a one-line lz4 log whose compressed file is
larger than its content (asserted in the test), and uses
`END_EVENT_REPARSE_CHUNK_SIZE = 1`, so `LZ4BlockInputStream.skip` returns 0
before `target - 1`. The scan runs in a separate thread with a one-minute
timeout, so a non-terminating skip loop fails the test instead of hanging it;
it does fail that way when the EOF check is removed.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]