This is an automated email from the ASF dual-hosted git repository.

Yicong-Huang pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new d2a108c19d46 [SPARK-59824][SQL] Guard top-level StringType/BinaryType 
inputs to pickle Python UDFs
d2a108c19d46 is described below

commit d2a108c19d4609b00901f1bca909216c73169e23
Author: Tengfei Huang <[email protected]>
AuthorDate: Fri Oct 9 08:42:42 2026 +0000

    [SPARK-59824][SQL] Guard top-level StringType/BinaryType inputs to pickle 
Python UDFs
    
    ### What changes were proposed in this pull request?
    **Problem**
    For non-Arrow Python UDFs, Spark evaluates the UDF input expressions and 
serializes the projected row using Java conversion followed by pickle 
serialization.
    
    A single row can contain a very large string or binary value. Because 
batching cannot split an individual row, converting and pickling it may create 
several large in-memory copies and cause an executor OOM before the row reaches 
the Python worker.
    
    Existing batch-size limits do not protect against this case.
    
    **Proposed change**
    Add an optional row-size guard for pickle-based Python UDFs.
    
    The guard should:
    - Run after UDF input expressions are projected, so computed values such as 
cast, concat, and substring are measured.
    - Run before Java conversion and pickle serialization.
    - Estimate the payload size of supported top-level variable-width 
arguments, initially strings and binary values.
    - Fail with a structured error when the estimated size exceeds a 
configurable threshold based on executor heap.
    - Introduce no per-row overhead when disabled or when the projected schema 
has no supported variable-width arguments.
    
    Pls note that: Nested arrays, maps, and structs are intentionally excluded 
initially to avoid a recursive traversal of every input value.
    
    ### Why are the changes needed?
    Avoid single giant row crashing executors.
    
    ### Does this PR introduce _any_ user-facing change?
    Yes.
    
    Previously, a single oversized input row for a pickle-serialized Python UDF 
was converted and serialized without a row-level size check, potentially 
causing executor OOM or a Python worker crash.
    
    This PR adds an opt-in guard that checks the combined payload size of 
top-level string and binary arguments after projection and before conversion 
and pickling. If the configured executor-heap fraction is exceeded, the query 
fails with
    UDF_LIMITS.ROW_SIZE.
    
    The guard is disabled by default, so default behavior remains unchanged. 
This capability is new relative to both released Spark versions and the current 
master branch.
    
    ### How was this patch tested?
    UTs
    
    ### Was this patch authored or co-authored using generative AI tooling?
    Generated-by: ClaudeCode Opus 4.8
    
    Closes #59122 from ivoson/SPARK-59824.
    
    Authored-by: Tengfei Huang <[email protected]>
    Signed-off-by: Yicong-Huang <[email protected]>
    (cherry picked from commit d1e511b6387fcca474d4a9beefc332114dfd2707)
    Signed-off-by: Yicong-Huang <[email protected]>
---
 .../src/main/resources/error/error-conditions.json |  13 ++
 .../spark/sql/errors/QueryExecutionErrors.scala    |   9 ++
 .../org/apache/spark/sql/internal/SQLConf.scala    |  27 ++++
 .../sql/execution/python/BatchEvalPythonExec.scala |  94 +++++++++++-
 .../python/BatchEvalPythonExecSuite.scala          | 167 ++++++++++++++++++++-
 5 files changed, 300 insertions(+), 10 deletions(-)

diff --git a/common/utils/src/main/resources/error/error-conditions.json 
b/common/utils/src/main/resources/error/error-conditions.json
index c4ecebab14a0..eed97372cfe3 100644
--- a/common/utils/src/main/resources/error/error-conditions.json
+++ b/common/utils/src/main/resources/error/error-conditions.json
@@ -8374,6 +8374,19 @@
     ],
     "sqlState" : "54011"
   },
+  "UDF_LIMITS" : {
+    "message" : [
+      "A UDF resource limit was exceeded."
+    ],
+    "subClass" : {
+      "ROW_SIZE" : {
+        "message" : [
+          "The projected input row size <actualRowSize> bytes exceeds the 
maximum allowed size <maxRowSize> bytes."
+        ]
+      }
+    },
+    "sqlState" : "54000"
+  },
   "UDTF_ALIAS_NUMBER_MISMATCH" : {
     "message" : [
       "The number of aliases supplied in the AS clause does not match the 
number of columns output by the UDTF.",
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
index f80cfecf7aa8..66662e9dc6bb 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/errors/QueryExecutionErrors.scala
@@ -198,6 +198,15 @@ private[sql] object QueryExecutionErrors extends 
QueryErrorsBase with ExecutionE
       cause = e)
   }
 
+  def pythonUDFRowSizeExceededError(
+      maxRowSize: Long, actualRowSize: Long): RuntimeException = {
+    new SparkRuntimeException(
+      errorClass = "UDF_LIMITS.ROW_SIZE",
+      messageParameters = Map(
+        "maxRowSize" -> maxRowSize.toString,
+        "actualRowSize" -> actualRowSize.toString))
+  }
+
   def divideByZeroError(context: QueryContext): ArithmeticException = {
     new SparkArithmeticException(
       errorClass = "DIVIDE_BY_ZERO",
diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
index de2f850f9d14..dff899c005ae 100644
--- a/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
+++ b/sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala
@@ -5485,6 +5485,33 @@ object SQLConf {
           "be -1 (no limit) or greater than zero and less than or equal to 
INT_MAX.")
       .createWithDefault(-1)
 
+  val PYTHON_UDF_ROW_SIZE_GUARD_ENABLED =
+    buildConf("spark.sql.execution.python.udf.rowSizeGuard.enabled")
+      .internal()
+      .doc("When true, guard pickle-serialized (non-Arrow) Python UDF 
evaluation against " +
+        "top-level string and binary argument payloads whose combined size 
exceeds " +
+        "rowSizeGuard.maxRowHeapFraction of executor heap. The guard checks 
the projected " +
+        "arguments before conversion and pickling. Nested inputs are not 
estimated.")
+      .version("4.4.0")
+      .withBindingPolicy(ConfigBindingPolicy.NOT_APPLICABLE)
+      .booleanConf
+      .createWithDefault(false)
+
+  val PYTHON_UDF_ROW_SIZE_GUARD_MAX_ROW_HEAP_FRACTION =
+    buildConf("spark.sql.execution.python.udf.rowSizeGuard.maxRowHeapFraction")
+      .internal()
+      .doc("Maximum combined byte size of one projected input row's top-level 
string and " +
+        "binary Python UDF arguments, as a fraction of executor max heap. The 
default 0.083 " +
+        "is approximately 1/12 of the executor heap; conversion and pickling 
can require " +
+        "multiple copies of the argument bytes.")
+      .version("4.4.0")
+      .withBindingPolicy(ConfigBindingPolicy.NOT_APPLICABLE)
+      .doubleConf
+      .checkValue(v => v > 0.0 && v <= 1.0,
+        "The value of 
spark.sql.execution.python.udf.rowSizeGuard.maxRowHeapFraction " +
+          "must be in (0.0, 1.0].")
+      .createWithDefault(0.083)
+
   val PYTHON_UDF_BUFFER_SIZE =
     buildConf("spark.sql.execution.python.udf.buffer.size")
       .doc(
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/python/BatchEvalPythonExec.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/python/BatchEvalPythonExec.scala
index 4c39ce98cf55..3707543ee57e 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/python/BatchEvalPythonExec.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/python/BatchEvalPythonExec.scala
@@ -27,11 +27,12 @@ import org.apache.spark.{JobArtifactSet, TaskContext}
 import org.apache.spark.api.python.{ChainedPythonFunctions, PythonEvalType}
 import org.apache.spark.sql.catalyst.InternalRow
 import org.apache.spark.sql.catalyst.expressions._
+import org.apache.spark.sql.errors.QueryExecutionErrors
 import org.apache.spark.sql.execution.SparkPlan
 import org.apache.spark.sql.execution.metric.SQLMetric
 import org.apache.spark.sql.execution.python.EvalPythonExec.ArgumentMetadata
 import org.apache.spark.sql.internal.SQLConf
-import org.apache.spark.sql.types.{StructField, StructType}
+import org.apache.spark.sql.types.{BinaryType, DataType, StringType, 
StructField, StructType}
 
 /**
  * A physical plan that evaluates a [[PythonUDF]]
@@ -60,7 +61,10 @@ case class BatchEvalPythonExec(udfs: Seq[PythonUDF], 
resultAttrs: Seq[Attribute]
       pythonMetrics,
       jobArtifactUUID,
       sessionUUID,
-      binaryAsBytes)
+      binaryAsBytes,
+      rowSizeGuardEnabled = 
conf.getConf(SQLConf.PYTHON_UDF_ROW_SIZE_GUARD_ENABLED),
+      rowSizeGuardMaxRowHeapFraction =
+        conf.getConf(SQLConf.PYTHON_UDF_ROW_SIZE_GUARD_MAX_ROW_HEAP_FRACTION))
   }
 
   override protected def withNewChildInternal(newChild: SparkPlan): 
BatchEvalPythonExec =
@@ -76,9 +80,43 @@ class BatchEvalPythonEvaluatorFactory(
     pythonMetrics: Map[String, SQLMetric],
     jobArtifactUUID: Option[String],
     sessionUUID: Option[String],
-    binaryAsBytes: Boolean)
+    binaryAsBytes: Boolean,
+    rowSizeGuardEnabled: Boolean = false,
+    rowSizeGuardMaxRowHeapFraction: Double = -1.0)
   extends EvalPythonEvaluatorFactory(childOutput, udfs, output) {
 
+  // This lazy val is first used in a task, even when the factory is built on 
the driver.
+  private lazy val rowSizeGuardMaxRowBytes = if 
(rowSizeGuardMaxRowHeapFraction > 0) {
+    (Runtime.getRuntime.maxMemory() * rowSizeGuardMaxRowHeapFraction).toLong
+  } else {
+    -1L
+  }
+
+  protected def projectedInputCheck(
+      inputTypes: Seq[DataType]): Option[InternalRow => Unit] = {
+    if (rowSizeGuardEnabled && rowSizeGuardMaxRowBytes >= 0) {
+      BatchEvalPythonExec.projectedInputPayloadSizeEstimator(inputTypes).map { 
estimate =>
+        row => {
+          val estimatedBytes = estimate(row)
+          if (estimatedBytes > rowSizeGuardMaxRowBytes) {
+            throw QueryExecutionErrors.pythonUDFRowSizeExceededError(
+              rowSizeGuardMaxRowBytes, estimatedBytes)
+          }
+        }
+      }
+    } else {
+      None
+    }
+  }
+
+  private[python] def getInputIterator(
+      iter: Iterator[InternalRow],
+      schema: StructType): Iterator[Array[Byte]] = {
+    BatchEvalPythonExec.getInputIterator(
+      iter, schema, batchSize, binaryAsBytes, maxBytesPerBatch, pythonMetrics,
+      projectedInputCheck(schema.map(_.dataType)))
+  }
+
   override def evaluate(
       funcs: Seq[(ChainedPythonFunctions, Long)],
       argMetas: Array[Array[ArgumentMetadata]],
@@ -88,8 +126,7 @@ class BatchEvalPythonEvaluatorFactory(
     EvaluatePython.registerPicklers() // register pickler for Row
 
     // Input iterator to Python.
-    val inputIterator = BatchEvalPythonExec.getInputIterator(
-      iter, schema, batchSize, binaryAsBytes, maxBytesPerBatch, pythonMetrics)
+    val inputIterator = getInputIterator(iter, schema)
 
     // Output iterator for results from Python.
     val outputIterator =
@@ -125,13 +162,48 @@ class BatchEvalPythonEvaluatorFactory(
 }
 
 object BatchEvalPythonExec {
+  /**
+   * Estimates top-level variable-width payloads in a projected UDF argument 
row. Nested inputs are
+   * intentionally excluded to avoid a recursive walk of every value.
+   */
+  private[python] def projectedInputPayloadSizeEstimator(
+      inputTypes: Seq[DataType]): Option[InternalRow => Long] = {
+    val projectedPayloads = inputTypes.zipWithIndex.flatMap { case (dataType, 
ordinal) =>
+      dataType match {
+        case _: StringType | BinaryType => Some((ordinal, dataType))
+        case _ => None
+      }
+    }.toArray
+    if (projectedPayloads.isEmpty) {
+      None
+    } else {
+      Some { row =>
+        var estimatedBytes = 0L
+        var i = 0
+        while (i < projectedPayloads.length) {
+          val (ordinal, dataType) = projectedPayloads(i)
+          if (!row.isNullAt(ordinal)) {
+            estimatedBytes += (dataType match {
+              case _: StringType => row.getUTF8String(ordinal).numBytes.toLong
+              case BinaryType => row.getBinary(ordinal).length.toLong
+              case _ => 0L
+            })
+          }
+          i += 1
+        }
+        estimatedBytes
+      }
+    }
+  }
+
   def getInputIterator(
       iter: Iterator[InternalRow],
       schema: StructType,
       batchSize: Int,
       binaryAsBytes: Boolean,
       maxBytesPerBatch: Long = -1L,
-      pythonMetrics: Map[String, SQLMetric] = Map.empty): 
Iterator[Array[Byte]] = {
+      pythonMetrics: Map[String, SQLMetric] = Map.empty,
+      projectedInputCheck: Option[InternalRow => Unit] = None): 
Iterator[Array[Byte]] = {
     val peakPickledBatchBytesMetric = 
pythonMetrics.get("pythonPeakPickledBatchBytes")
     val oversizedBatchMetric = pythonMetrics.get("pythonOversizedBatchCount")
     val estimatedInputBytesMetric = 
pythonMetrics.get("pythonEstimatedInputBytes")
@@ -155,7 +227,7 @@ object BatchEvalPythonExec {
     // Converts a row to the java object pickled to Python. When `sizeAcc` is 
defined, toJava also
     // accumulates the per-row pickled-size estimate at its leaf cases during 
this same traversal
     // (no second walk).
-    def convertRow(row: InternalRow, sizeAcc: Option[PickledSizeAccumulator]): 
Any = {
+    def convertRowUnchecked(row: InternalRow, sizeAcc: 
Option[PickledSizeAccumulator]): Any = {
       if (needConversion) {
         EvaluatePython.toJava(row, schema, binaryAsBytes, sizeAcc)
       } else {
@@ -171,6 +243,14 @@ object BatchEvalPythonExec {
         fields
       }
     }
+    val convertRow: (InternalRow, Option[PickledSizeAccumulator]) => Any =
+      projectedInputCheck match {
+        case Some(check) => (row, sizeAcc) => {
+          check(row)
+          convertRowUnchecked(row, sizeAcc)
+        }
+        case None => convertRowUnchecked
+      }
 
     // Input iterator to Python: input rows are grouped so we send them in 
batches to Python.
     val batchedIter: Iterator[Array[Any]] =
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/python/BatchEvalPythonExecSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/python/BatchEvalPythonExecSuite.scala
index 34d63df710cb..74b80cfc3202 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/python/BatchEvalPythonExecSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/python/BatchEvalPythonExecSuite.scala
@@ -20,13 +20,19 @@ package org.apache.spark.sql.execution.python
 import scala.collection.mutable.ArrayBuffer
 import scala.jdk.CollectionConverters._
 
-import org.apache.spark.api.python.{PythonEvalType, SimplePythonFunction}
+import org.apache.spark.{SparkRuntimeException, TaskContext}
+import org.apache.spark.api.python.{ChainedPythonFunctions, PythonEvalType, 
SimplePythonFunction}
 import org.apache.spark.sql.catalyst.{FunctionIdentifier, InternalRow}
-import org.apache.spark.sql.catalyst.expressions.{And, AttributeReference, 
GenericInternalRow, GreaterThan, In}
+import org.apache.spark.sql.catalyst.expressions.{
+  And, AttributeReference, Concat, GenericInternalRow, GreaterThan, In, 
PythonUDF,
+  UnsafeProjection}
 import org.apache.spark.sql.connector.catalog.CatalogManager
-import org.apache.spark.sql.execution.{FilterExec, InputAdapter, 
WholeStageCodegenExec}
+import org.apache.spark.sql.execution.{
+  FilterExec, InputAdapter, LocalTableScanExec, WholeStageCodegenExec}
 import org.apache.spark.sql.execution.adaptive.AdaptiveSparkPlanHelper
 import org.apache.spark.sql.execution.metric.{SQLMetric, SQLMetrics}
+import org.apache.spark.sql.execution.python.EvalPythonExec.ArgumentMetadata
+import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.test.{ExamplePointUDT, SharedSparkSession}
 import org.apache.spark.sql.types._
 import org.apache.spark.unsafe.types.UTF8String
@@ -81,6 +87,161 @@ class BatchEvalPythonExecSuite extends SharedSparkSession
     assert(metric.value === expectedPeak)
   }
 
+  test("SPARK-59824: projected UDF input payload estimator covers evaluated 
values") {
+    val inputTypes = Seq(StringType, BinaryType, IntegerType)
+    val projectedRow = InternalRow(
+      UTF8String.fromString("s" * 2000), Array.fill[Byte](4000)(1), 1)
+
+    val estimate = BatchEvalPythonExec
+      .projectedInputPayloadSizeEstimator(inputTypes)
+      .get
+    assert(estimate(projectedRow) === 6000L)
+    assert(estimate(InternalRow(null, null, 1)) === 0L)
+
+    // Fixed-width and nested inputs are intentionally not estimated.
+    assert(BatchEvalPythonExec
+      .projectedInputPayloadSizeEstimator(Seq(IntegerType, 
ArrayType(StringType)))
+      .isEmpty)
+  }
+
+  test("SPARK-59824: pickle UDF row-size guard rejects a computed projected 
argument") {
+    // Exercise the production evaluator wiring without starting a Python 
worker.
+    val errors = spark.sparkContext.parallelize(Seq(1), 1).mapPartitions { _ =>
+      val taskAttr = AttributeReference("s", StringType)()
+      val taskRow = UnsafeProjection.create(Array[DataType](StringType))
+        .apply(InternalRow(UTF8String.fromString("x" * 600)))
+      val projectedArg = Concat(Seq(taskAttr, taskAttr))
+      val pythonUdf = PythonUDF(
+        "dummy", new DummyUDF, StringType, Seq(projectedArg), 
PythonEvalType.SQL_BATCHED_UDF,
+        udfDeterministic = true)
+      val maxRowBytes = 1000L
+      val maxRowHeapFraction = maxRowBytes.toDouble / 
Runtime.getRuntime.maxMemory()
+      val expectedMaxRowSize =
+        (Runtime.getRuntime.maxMemory() * maxRowHeapFraction).toLong
+      val factory = new BatchEvalPythonEvaluatorFactory(
+        childOutput = Seq(taskAttr),
+        udfs = Seq(pythonUdf),
+        output = Seq.empty,
+        batchSize = 1,
+        maxBytesPerBatch = -1L,
+        pythonMetrics = Map.empty,
+        jobArtifactUUID = None,
+        sessionUUID = None,
+        binaryAsBytes = false,
+        rowSizeGuardEnabled = true,
+        rowSizeGuardMaxRowHeapFraction = maxRowHeapFraction) {
+        override def evaluate(
+            funcs: Seq[(ChainedPythonFunctions, Long)],
+            argMetas: Array[Array[ArgumentMetadata]],
+            iter: Iterator[InternalRow],
+            schema: StructType,
+            context: TaskContext): Iterator[InternalRow] = {
+          getInputIterator(iter, schema).next()
+          Iterator.empty
+        }
+      }
+      try {
+        factory.createEvaluator().eval(0, Iterator(taskRow)).hasNext
+        Iterator.empty[(SparkRuntimeException, Long)]
+      } catch {
+        case e: SparkRuntimeException => Iterator((e, expectedMaxRowSize))
+      }
+    }.collect().toSeq
+    assert(errors.length === 1)
+    val (exception, expectedMaxRowSize) = errors.head
+    checkError(
+      exception = exception,
+      condition = "UDF_LIMITS.ROW_SIZE",
+      parameters = Map(
+        "maxRowSize" -> expectedMaxRowSize.toString,
+        "actualRowSize" -> "1200"))
+  }
+
+  test("SPARK-59824: pickle UDF row-size guard uses SQLConf settings") {
+    val configuredMaxRowSize = 1000L
+    val maxRowHeapFraction = configuredMaxRowSize.toDouble / 
Runtime.getRuntime.maxMemory()
+    val expectedMaxRowSize =
+      (Runtime.getRuntime.maxMemory() * maxRowHeapFraction).toLong
+    withSQLConf(
+      SQLConf.PYTHON_UDF_ROW_SIZE_GUARD_ENABLED.key -> "true",
+      SQLConf.PYTHON_UDF_ROW_SIZE_GUARD_MAX_ROW_HEAP_FRACTION.key ->
+        maxRowHeapFraction.toString) {
+      val taskAttr = AttributeReference("s", StringType)()
+      val pythonUdf = PythonUDF(
+        "dummy", new DummyUDF, StringType, Seq(taskAttr), 
PythonEvalType.SQL_BATCHED_UDF,
+        udfDeterministic = true)
+      class TestBatchEvalPythonExec extends BatchEvalPythonExec(
+          Seq(pythonUdf),
+          Seq(AttributeReference("result", StringType)()),
+          LocalTableScanExec(Seq(taskAttr), Nil, None)) {
+        def testEvaluatorFactory: BatchEvalPythonEvaluatorFactory =
+          evaluatorFactory.asInstanceOf[BatchEvalPythonEvaluatorFactory]
+      }
+      val plan = new TestBatchEvalPythonExec
+      val oversizedRow = InternalRow(
+        UTF8String.fromString("x" * (expectedMaxRowSize.toInt + 1)))
+      checkError(
+        exception = intercept[SparkRuntimeException] {
+          plan.testEvaluatorFactory.getInputIterator(
+            Iterator(oversizedRow), StructType.fromDDL("s STRING")).next()
+        },
+        condition = "UDF_LIMITS.ROW_SIZE",
+        parameters = Map(
+          "maxRowSize" -> expectedMaxRowSize.toString,
+          "actualRowSize" -> (expectedMaxRowSize + 1).toString))
+    }
+  }
+
+  test("SPARK-59824: pickle UDF row-size guard disabled no-op and strict 
boundary") {
+    val taskAttr = AttributeReference("s", StringType)()
+    val fraction = 1000.toDouble / Runtime.getRuntime.maxMemory()
+    val threshold = (Runtime.getRuntime.maxMemory() * fraction).toLong
+    def checkerFor(
+        enabled: Boolean,
+        maxRowHeapFraction: Double = fraction): Option[InternalRow => Unit] =
+      (new BatchEvalPythonEvaluatorFactory(
+        childOutput = Seq(taskAttr),
+        udfs = Seq.empty,
+        output = Seq.empty,
+        batchSize = 1,
+        maxBytesPerBatch = -1L,
+        pythonMetrics = Map.empty,
+        jobArtifactUUID = None,
+        sessionUUID = None,
+        binaryAsBytes = false,
+        rowSizeGuardEnabled = enabled,
+        rowSizeGuardMaxRowHeapFraction = maxRowHeapFraction) {
+        def testCheck: Option[InternalRow => Unit] = 
projectedInputCheck(Seq(StringType))
+      }).testCheck
+    def rowOf(numBytes: Long): InternalRow =
+      InternalRow(UTF8String.fromString("x" * numBytes.toInt))
+    assert(checkerFor(enabled = false).isEmpty)
+
+    val check = checkerFor(enabled = true).get
+    check(rowOf(threshold))
+    checkError(
+      exception = intercept[SparkRuntimeException] {
+        check(rowOf(threshold + 1))
+      },
+      condition = "UDF_LIMITS.ROW_SIZE",
+      parameters = Map(
+        "maxRowSize" -> threshold.toString,
+        "actualRowSize" -> (threshold + 1).toString))
+
+    // A positive fraction can truncate to a zero-byte limit, which must still 
be enforced.
+    val subByteFraction = 0.5 / Runtime.getRuntime.maxMemory()
+    assert((Runtime.getRuntime.maxMemory() * subByteFraction).toLong === 0L)
+    val zeroThresholdCheck = checkerFor(
+      enabled = true, maxRowHeapFraction = subByteFraction).get
+    zeroThresholdCheck(rowOf(0))
+    checkError(
+      exception = intercept[SparkRuntimeException] {
+        zeroThresholdCheck(rowOf(1))
+      },
+      condition = "UDF_LIMITS.ROW_SIZE",
+      parameters = Map("maxRowSize" -> "0", "actualRowSize" -> "1"))
+  }
+
   test("SPARK-57593: ByteBoundedAsArrayIterator oversized-batch and 
estimated-bytes metrics") {
     def elems(sizes: Long*): Iterator[(Any, Long)] =
       sizes.iterator.map(s => (0.asInstanceOf[Any], s))


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to