peter-toth commented on code in PR #58615:
URL: https://github.com/apache/spark/pull/58615#discussion_r3987125426


##########
sql/hive/src/main/scala/org/apache/spark/sql/hive/HiveUtils.scala:
##########
@@ -200,6 +200,24 @@ private[spark] object HiveUtils extends Logging {
     .booleanConf
     .createWithDefault(false)
 
+  val INITIALIZE_METASTORE_FORMAT_CLASSES =
+    buildConf("spark.sql.hive.initializeMetastoreFormatClasses")
+      .doc("When true, an InputFormat/OutputFormat class name stored in the 
Hive metastore is " +
+        "resolved with its static initializer run at resolution time. When 
false, the class is " +
+        "resolved without running its static initializer, which then runs 
later when the format " +
+        "is instantiated for a scan/write. Only the class name is needed when 
converting " +
+        "metastore metadata, so setting this to false avoids running a format 
class's static " +
+        "initializer during a metadata operation. Note that the conversion 
also runs when " +
+        "planning a scan or write and when inferring the schema of a Hive 
serde table, so with " +
+        "false a format class whose static initializer fails no longer fails 
fast on the driver " +
+        "at resolution time; the failure instead surfaces later on an executor 
when the format " +
+        "is instantiated (as NoClassDefFoundError: Could not initialize class 
...). Keep this " +
+        "true (the default) to preserve the fail-fast behavior.")

Review Comment:
   **Finding 4.** This sentence is mine from round 1 and it is wrong. For a 
Hive serde scan the InputFormat is instantiated on the driver, not on an 
executor.
   
   `HadoopRDD.getPartitions` calls `getInputFormat(jobConf)`, which is 
`ReflectionUtils.newInstance` 
(`core/src/main/scala/org/apache/spark/rdd/HadoopRDD.scala:217`). 
`NewHadoopRDD.getPartitions` calls 
`inputFormatClass.getConstructor().newInstance()` 
(`core/src/main/scala/org/apache/spark/rdd/NewHadoopRDD.scala:149`). Both run 
on the driver, when the job is submitted. So with `false` a broken static 
initializer still fails on the driver for a scan. What moves is the timing, 
from analysis or DDL to split computation.
   
   The write side does reach an executor. `HiveFileFormat.prepareWrite` only 
puts the class name into the conf 
(`sql/hive/src/main/scala/org/apache/spark/sql/hive/execution/HiveFileFormat.scala:78`),
 and `jobConf.value.getOutputFormat` at `:103` instantiates it inside the 
`OutputWriterFactory`, on the task side.
   
   Two more things worth saying while the doc is being rewritten:
   
   - `false` still loads the class, so a missing class still throws 
`ClassNotFoundException` right here. Only the static initializer is deferred. 
As written, a reader can take `false` to mean the resolution cannot fail at all.
   - With `convertMetastoreParquet` / `convertMetastoreOrc` the format is never 
instantiated, so the initializer never runs anywhere. That is the case where 
`false` actually buys something, and the doc does not mention it.
   
   ```suggestion
         .doc("When true, an InputFormat/OutputFormat class name stored in the 
Hive metastore is " +
           "resolved with its static initializer run at resolution time. When 
false, the class is " +
           "still loaded, so a missing class still fails here, but its static 
initializer is not " +
           "run. Only the class name is needed when converting metastore 
metadata, so setting " +
           "this to false avoids running a format class's static initializer 
during a metadata " +
           "operation. Note that the conversion also runs when planning a scan 
or write and when " +
           "inferring the schema of a Hive serde table, so with false a format 
class whose static " +
           "initializer fails no longer fails fast at resolution time. It fails 
when the format " +
           "is instantiated instead (as NoClassDefFoundError: Could not 
initialize class ...), " +
           "which for a Hive serde scan is on the driver when the input splits 
are computed, and " +
           "for a Hive serde write is on an executor. A table read through the 
built-in " +
           "Parquet/ORC reader never instantiates the format, so there the 
initializer never " +
           "runs. Keep this true (the default) to preserve the fail-fast 
behavior.")
   ```
   



##########
sql/hive/src/test/scala/org/apache/spark/sql/hive/client/HiveClientImplSuite.scala:
##########
@@ -20,9 +20,41 @@ package org.apache.spark.sql.hive.client
 import org.apache.hadoop.hive.metastore.api.FieldSchema
 
 import org.apache.spark.{SparkFunSuite, SparkUnsupportedOperationException}
+import org.apache.spark.sql.catalyst.TableIdentifier
+import org.apache.spark.sql.catalyst.catalog.{CatalogStorageFormat, 
CatalogTable, CatalogTableType}
+import org.apache.spark.sql.hive.{HiveUtils, StaticInitFlags, 
StaticInitInputFormat}
+import org.apache.spark.sql.internal.SQLConf
+import org.apache.spark.sql.types.StructType
 
 class HiveClientImplSuite extends SparkFunSuite {
 
+  test("SPARK-59330: toHiveTable skips the format class static initializer 
when " +
+    "spark.sql.hive.initializeMetastoreFormatClasses is false") {
+    val table = CatalogTable(
+      identifier = TableIdentifier("t", Some("default")),
+      tableType = CatalogTableType.MANAGED,
+      storage = CatalogStorageFormat.empty.copy(
+        inputFormat = Some(classOf[StaticInitInputFormat].getName)),
+      schema = new StructType().add("a", "int"))
+
+    def toHiveTableWith(initialize: Boolean): Unit = {
+      val conf = new SQLConf()
+      conf.setConf(HiveUtils.INITIALIZE_METASTORE_FORMAT_CLASSES, initialize)
+      SQLConf.withExistingConf(conf) {
+        HiveClientImpl.toHiveTable(table)
+      }
+    }
+
+    // The false half must run first: class initialization is one-way per JVM. 
Resolving the
+    // format class name without initializing it must not run the static 
initializer.
+    toHiveTableWith(initialize = false)
+    assert(!StaticInitFlags.inputFormatInitialized)
+
+    // With initialization enabled (the default), resolving the class name 
runs the initializer.
+    toHiveTableWith(initialize = true)
+    assert(StaticInitFlags.inputFormatInitialized)

Review Comment:
   **Finding 5.** `toOutputFormat` took the same `initialize = 
initializeFormatClasses` change at 
`sql/hive/src/main/scala/org/apache/spark/sql/hive/client/HiveClientImpl.scala:1180`,
 and nothing runs it. The two call sites are independent, so an edit to one of 
them is unguarded.
   
   Same shape as the input format, sketch below is untested. Add 
`outputFormatInitialized` to `StaticInitFlags`, add a `StaticInitOutputFormat 
implements org.apache.hadoop.hive.ql.io.HiveOutputFormat<Void, Void>` whose 
static block flips it and whose `getHiveRecordWriter` / `getRecordWriter` / 
`checkOutputSpecs` throw `UnsupportedOperationException`, then put it on the 
same storage format:
   
   ```scala
         storage = CatalogStorageFormat.empty.copy(
           inputFormat = Some(classOf[StaticInitInputFormat].getName),
           outputFormat = Some(classOf[StaticInitOutputFormat].getName)),
   ```
   
   and assert both flags in each half.
   



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