cloud-fan commented on code in PR #58704:
URL: https://github.com/apache/spark/pull/58704#discussion_r4057250910


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala:
##########
@@ -717,43 +746,107 @@ class JacksonParser(
       }
     } catch {
       case e: SparkUpgradeException => throw e
-      case e @ (_: RuntimeException | _: JsonProcessingException | _: 
MalformedInputException) =>
-        // JSON parser currently doesn't support partial results for corrupted 
records.
-        // For such records, all fields other than the field configured by
-        // `columnNameOfCorruptRecord` are set to `null`.
-        throw BadRecordException(() => recordLiteral(record), () => 
Array.empty, e)
       case e: CharConversionException if options.encoding.isEmpty =>
-        val msg =
-          """JSON parser cannot handle a character in its input.
-            |Specifying encoding as an input option explicitly might help to 
resolve the issue.
-            |""".stripMargin + e.getMessage
-        val wrappedCharException = new CharConversionException(msg)
-        wrappedCharException.initCause(e)
-        throw BadRecordException(() => recordLiteral(record), () => 
Array.empty,
-          wrappedCharException)
-      case PartialResultException(row, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => Array(row),
-          convertCauseForPartialResult(cause))
-      case PartialResultArrayException(rows, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => rows,
-          cause)
-      // These exceptions should never be thrown outside of JacksonParser.
-      // They are used for the control flow in the parser. We add them here 
for completeness
-      // since they also indicate a bad record.
-      case PartialArrayDataResultException(arrayData, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => Array(InternalRow(arrayData)),
-          convertCauseForPartialResult(cause))
-      case PartialMapDataResultException(mapData, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => Array(InternalRow(mapData)),
-          convertCauseForPartialResult(cause))
+        throw badRecord(e, () => recordLiteral(record))
+      case e @ (_: RuntimeException | _: JsonProcessingException | _: 
MalformedInputException |
+          _: PartialResultException | _: PartialResultArrayException |
+          _: PartialArrayDataResultException | _: 
PartialMapDataResultException) =>
+        throw badRecord(e, () => recordLiteral(record))
+    }
+  }
+
+  private[sql] def parseIterator[T](
+      record: T,
+      createParser: (JsonFactory, T) => JsonParser,
+      recordLiteral: T => UTF8String): Iterator[InternalRow] = {
+    val streamArray = allowArrayAsStructs && schema.isInstanceOf[StructType] &&
+      options.singleVariantColumn.isEmpty && 
options.explodeEmbeddedArray.isEmpty
+    val elementConverter = if (streamArray) makeConverter(schema) else null
+    val jsonParser = try {
+      createParser(factory, record)
+    } catch {
+      case e: SparkUpgradeException => throw e
+      case e: CharConversionException if options.encoding.isEmpty =>
+        throw badRecord(e, () => recordLiteral(record))
+      case e @ (_: RuntimeException | _: JsonProcessingException | _: 
MalformedInputException |
+          _: PartialResultException | _: PartialResultArrayException |
+          _: PartialArrayDataResultException | _: 
PartialMapDataResultException) =>
+        throw badRecord(e, () => recordLiteral(record))
+    }
+    // Abandoning the iterator before END_ARRAY (e.g. a LIMIT) skips 
finish()/fail(), so close the
+    // parser at task completion; close() is idempotent with those eager 
closes.
+    Option(TaskContext.get()).foreach(_.addTaskCompletionListener[Unit](_ => 
jsonParser.close()))
+    def fail(error: Throwable): Nothing = {
+      try jsonParser.close() catch {
+        case NonFatal(closeError) => error.addSuppressed(closeError)
+      }
+      throw badRecord(error, () => recordLiteral(record))
+    }
+    def handleFailure[T](recoverPartialResult: Boolean)(operation: => T): T = {
+      try operation catch {
+        case e: SparkUpgradeException => fail(e)
+        case e: CharConversionException if options.encoding.isEmpty => fail(e)
+        case e: PartialResultException
+            if recoverPartialResult && options.parseMode != FailFastMode =>
+          throw badRecord(e, () => recordLiteral(record)).copy(recoverable = 
true)
+        case e: PartialResultException =>
+          fail(e)
+        case e @ (_: RuntimeException | _: JsonProcessingException | _: 
MalformedInputException |

Review Comment:
   **Non-blocking (P2):** An incompatible scalar element reaches this terminal 
RuntimeException arm even though its token is already at a safe top-level 
array-element boundary. FailureSafeParser then replaces the source iterator 
with only the recovery result, so PERMISSIVE and DROPMALFORMED reads of an 
input such as [{"a":1},42,{"a":2}] silently lose the final valid row. Please 
classify this scalar-boundary failure as recoverable while keeping nested 
object and array mismatches terminal.
   
   **Recommended change:** Classify only scalar element-type conversion 
failures that remain at the current top-level element boundary as recoverable 
in non-FAILFAST modes, and cover scalars before, between, and after valid 
objects across parse modes and archive/file paths.
   
   **Why this works:** Inspect the current element token and error category at 
the array-loop owner. Convert a safe scalar mismatch into a recoverable 
BadRecordException so FailureSafeParser emits or drops that element and resumes 
the preserved iterator; keep nested containers and structurally ambiguous 
failures terminal.
   
   **Scope:** Complete element-boundary recovery without broadening recovery to 
undefined Jackson token positions.
   
   **Compatibility:** Existing partial-object recovery, terminal 
malformed-document handling, corrupt-record text, and the disabled eager path 
remain unchanged.
   
   **Risks:** An overly broad classification could resume from inside a 
malformed nested container and duplicate or misparse later tokens. An overly 
narrow classification could continue dropping later valid objects after other 
safely consumed scalar forms.
   
   **Constraints:** Resume only when parser position is known to be at a 
complete top-level element boundary. FAILFAST must still fail immediately. 
Terminal structural corruption must still close and stop the iterator.
   
   **Success:** In PERMISSIVE mode, an incompatible scalar produces one 
recovery row and later valid objects exactly once. In DROPMALFORMED mode, the 
incompatible scalar is omitted and later valid objects remain. In FAILFAST 
mode, the first incompatible scalar still raises the established 
malformed-record error. Nested or structurally ambiguous failures remain 
terminal and close the parser.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/json/JacksonParser.scala:
##########
@@ -717,43 +746,107 @@ class JacksonParser(
       }
     } catch {
       case e: SparkUpgradeException => throw e
-      case e @ (_: RuntimeException | _: JsonProcessingException | _: 
MalformedInputException) =>
-        // JSON parser currently doesn't support partial results for corrupted 
records.
-        // For such records, all fields other than the field configured by
-        // `columnNameOfCorruptRecord` are set to `null`.
-        throw BadRecordException(() => recordLiteral(record), () => 
Array.empty, e)
       case e: CharConversionException if options.encoding.isEmpty =>
-        val msg =
-          """JSON parser cannot handle a character in its input.
-            |Specifying encoding as an input option explicitly might help to 
resolve the issue.
-            |""".stripMargin + e.getMessage
-        val wrappedCharException = new CharConversionException(msg)
-        wrappedCharException.initCause(e)
-        throw BadRecordException(() => recordLiteral(record), () => 
Array.empty,
-          wrappedCharException)
-      case PartialResultException(row, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => Array(row),
-          convertCauseForPartialResult(cause))
-      case PartialResultArrayException(rows, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => rows,
-          cause)
-      // These exceptions should never be thrown outside of JacksonParser.
-      // They are used for the control flow in the parser. We add them here 
for completeness
-      // since they also indicate a bad record.
-      case PartialArrayDataResultException(arrayData, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => Array(InternalRow(arrayData)),
-          convertCauseForPartialResult(cause))
-      case PartialMapDataResultException(mapData, cause) =>
-        throw BadRecordException(
-          record = () => recordLiteral(record),
-          partialResults = () => Array(InternalRow(mapData)),
-          convertCauseForPartialResult(cause))
+        throw badRecord(e, () => recordLiteral(record))
+      case e @ (_: RuntimeException | _: JsonProcessingException | _: 
MalformedInputException |
+          _: PartialResultException | _: PartialResultArrayException |
+          _: PartialArrayDataResultException | _: 
PartialMapDataResultException) =>
+        throw badRecord(e, () => recordLiteral(record))
+    }
+  }
+
+  private[sql] def parseIterator[T](
+      record: T,
+      createParser: (JsonFactory, T) => JsonParser,
+      recordLiteral: T => UTF8String): Iterator[InternalRow] = {
+    val streamArray = allowArrayAsStructs && schema.isInstanceOf[StructType] &&
+      options.singleVariantColumn.isEmpty && 
options.explodeEmbeddedArray.isEmpty
+    val elementConverter = if (streamArray) makeConverter(schema) else null
+    val jsonParser = try {
+      createParser(factory, record)
+    } catch {
+      case e: SparkUpgradeException => throw e
+      case e: CharConversionException if options.encoding.isEmpty =>
+        throw badRecord(e, () => recordLiteral(record))
+      case e @ (_: RuntimeException | _: JsonProcessingException | _: 
MalformedInputException |
+          _: PartialResultException | _: PartialResultArrayException |
+          _: PartialArrayDataResultException | _: 
PartialMapDataResultException) =>
+        throw badRecord(e, () => recordLiteral(record))
+    }
+    // Abandoning the iterator before END_ARRAY (e.g. a LIMIT) skips 
finish()/fail(), so close the
+    // parser at task completion; close() is idempotent with those eager 
closes.
+    Option(TaskContext.get()).foreach(_.addTaskCompletionListener[Unit](_ => 
jsonParser.close()))
+    def fail(error: Throwable): Nothing = {
+      try jsonParser.close() catch {

Review Comment:
   **Non-blocking (P2):** The malformed and truncated-array tests do not 
observe this terminal cleanup side effect. If fail() stopped closing the 
parser, they would still return the same rows and errors while retaining the 
input until task completion. Please add a regression assertion that terminal 
advancement failures close the parser and underlying input synchronously.
   
   **Recommended change:** Introduce one clearable parser-lifecycle owner 
captured by TaskContext, route every eager close through it, and add 
resource-sensitive JSON lifecycle coverage for empty input, empty arrays, 
normal exhaustion, terminal failure, and abandonment, including repeated 
archive entries.
   
   **Why this works:** The task listener captures only a small owner that 
atomically consumes its current parser. Normal completion, terminal failure, 
and eager fallback use the same close-and-clear operation so a completed parser 
becomes unreachable immediately; task completion consumes and closes only a 
parser still owned because its iterator was abandoned.
   
   **Scope:** Unify streaming parser ownership and verify every lifecycle 
transition without changing row or parse-mode semantics.
   
   **Compatibility:** The enabled path remains lazy; disabled and excluded 
option paths remain eager; recovery and terminal error semantics do not change.
   
   **Risks:** Clearing ownership before close completes could lose the only 
cleanup backstop after a close failure. Independent close routes could race or 
mask the original parse exception if they do not share one consume operation.
   
   **Constraints:** Normal exhaustion and terminal failures must still close 
synchronously. Task completion must still close an iterator abandoned before 
END_ARRAY. Existing recoverable-element and parse-mode row behavior must remain 
unchanged.
   
   **Success:** After normal, zero-row, eager-fallback, or terminal completion, 
TaskContext no longer retains the completed parser. An abandoned iterator still 
closes its current parser when the task completes. Repeated completed archive 
entries do not accumulate parser or entry-buffer ownership until task 
completion. Rows, corrupt-record values, and parse-mode errors remain unchanged.



-- 
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]

Reply via email to