HyukjinKwon commented on code in PR #59320:
URL: https://github.com/apache/spark/pull/59320#discussion_r4258683487


##########
sql/catalyst/benchmarks/RetainedResolutionConfigsBenchmark-results.txt:
##########
@@ -0,0 +1,382 @@
+================================================================================================
+view: 32 settings, 25% retained, warm
+================================================================================================
+
+OpenJDK 64-Bit Server VM 17.0.15+6-Ubuntu-0ubuntu120.04 on Linux 
5.4.0-1163-aws-fips

Review Comment:
   **Nit (P3):** These numbers come from a local machine (Ubuntu OpenJDK 
17.0.15 on Linux 5.4.0-aws-fips, Xeon 8375C). The other result sets in 
`sql/catalyst/benchmarks/` are generated by the "Run benchmarks" GitHub Actions 
workflow and include `-jdk21-results.txt` and `-jdk25-results.txt`, so this 
file can't be compared with them or regenerated the same way. Could you 
regenerate it with that workflow for JDK 17, 21, and 25? The `create-commit` 
option can push the results to this branch.
   
   **Verification:**
   - **Inspection:** All three JDK result files exist for the benchmark, and 
their environment headers match the workflow runner environment used by the 
other committed catalyst benchmark results.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/Analyzer.scala:
##########
@@ -326,15 +326,30 @@ object Analyzer {
       newConf: SQLConf,
       existingConf: SQLConf,
       createSparkVersion: String = ""): Unit = {
-    existingConf.getAllConfs.foreach { case (key, value) =>
-      if (key.startsWith("spark.sql.catalog.") || 
SQLConf.isSessionBindingPolicy(key)) {
-        newConf.settings.put(key, value)
+    // The retained configs are memoized on `existingConf` unless
+    // `ANALYZER_MEMOIZE_RETAINED_RESOLUTION_CONFIGS` is off, which recomputes 
them on every build.
+    if 
(existingConf.getConf(SQLConf.ANALYZER_MEMOIZE_RETAINED_RESOLUTION_CONFIGS)) {
+      newConf.settings.putAll(existingConf.retainedResolutionConfigs)
+    } else {
+      existingConf.getAllConfs.foreach { case (key, value) =>
+        if (bodyFollowsCaller(key)) {
+          newConf.settings.put(key, value)
+        }
       }
     }
 
     trySetAnsiValue(newConf, createSparkVersion)
   }
 
+  /**
+   * True if a view or SQL function body takes the caller's value for `key` 
rather than the

Review Comment:
   **Nit (P3):** This reads as if the body always takes the caller's value for 
these keys, but the overlay copies only keys the caller has explicitly set, and 
only where session overrides are applied. For example, if a view is created 
while a SESSION-bound key such as 
`spark.sql.analyzer.lowerEmptyGroupingSetToGlobalAggregate.enabled` is 
explicitly `false` and the caller later `RESET`s it, the body keeps the saved 
`false`. Function confs built by `SessionCatalog` (`applySessionOverrides = 
false`) also keep saved values. Could the wording be narrowed, e.g. "True if 
the caller's explicitly set value for `key` is overlaid onto view and SQL 
function body confs (when session overrides apply), replacing the value saved 
with the body: catalog configs and SQL configs bound SESSION or 
NOT_APPLICABLE."?
   
   **Verification:**
   - **Inspection:** The revised doc matches the overlay behavior: only 
explicitly set caller keys selected by the predicate override saved values, and 
only on paths that call retainResolutionConfigsForAnalysis.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/SQLConf.scala:
##########
@@ -2893,6 +2935,19 @@ object SQLConf {
       .booleanConf
       .createWithDefault(true)
 
+  val ANALYZER_MEMOIZE_RETAINED_RESOLUTION_CONFIGS =
+    buildConf("spark.sql.analyzer.memoizeRetainedResolutionConfigs.enabled")
+      .internal()
+      .version("4.4.0")
+      .withBindingPolicy(ConfigBindingPolicy.SESSION)

Review Comment:
   **Nit (P3):** The `ConfigBindingPolicy` guidance for new configs says: "Can 
the config change the result of resolving the body of a view/UDF/procedure, 
i.e. the resolved plan? If not, use NOT_APPLICABLE." Since both values of this 
flag produce the same body conf, `NOT_APPLICABLE` seems like the right label. 
`isSessionBindingPolicy` retains `NOT_APPLICABLE` keys too, so nested bodies 
would still inherit the caller's explicitly set value.
   
   **Verification:**
   - **Inspection:** The config's declared binding policy is NOT_APPLICABLE, as 
the ConfigBindingPolicy selection rule requires for a config that cannot change 
the resolved body plan.
   - **Compatibility:** Memo-on and memo-off view and SQL function body confs 
remain identical, and an explicitly set flag value still propagates into nested 
body confs.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/internal/ModificationCountingMap.scala:
##########
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.spark.sql.internal
+
+import java.util
+import java.util.function.{BiConsumer, BiFunction, Function => JFunction}
+
+/**
+ * A thread-safe map of [[SQLConf]] settings that counts the mutating calls on 
it, so values
+ * derived from its entries can be reused while [[modificationCount]] is 
unchanged.
+ *
+ * Like the `java.util.Collections.synchronizedMap` it replaces, every method 
of the map
+ * synchronizes on the map itself, so a caller iterating the map must hold its 
monitor. Unlike it,
+ * the key, value, and entry views are read-only, so every mutation goes 
through a method that
+ * advances the count, and unsynchronized, so a caller must hold the monitor 
while using them.
+ */
+private[sql] final class ModificationCountingMap extends util.Map[String, 
String] {
+
+  private val underlying = new util.HashMap[String, String]
+
+  private val readOnlyUnderlying = util.Collections.unmodifiableMap[String, 
String](underlying)
+
+  @volatile private var mutations = 0L
+
+  /**
+   * Number of mutating calls so far; a call may advance it without changing 
the entries. Values
+   * derived from the entries stay valid while it is unchanged; use
+   * [[snapshotWithModificationCount]] to pair a count with the entries it 
describes.
+   */
+  def modificationCount: Long = mutations
+
+  /** A copy of the entries and the [[modificationCount]] they correspond to. 
*/
+  def snapshotWithModificationCount: (Long, util.Map[String, String]) = 
synchronized {
+    (mutations, new util.HashMap[String, String](underlying))
+  }
+
+  private def mutate[T](f: => T): T = synchronized {

Review Comment:
   **Nit (P3):** `getAllConfs` (`settings.synchronized { ... }`), 
`setConf(Properties)`, and the `RuntimeConfig` PythonWorkerEnvironment 
check-then-write all depend on these methods locking the map itself. 
`Collections.synchronizedMap` guaranteed that before; now it's implemented 
here, and none of the new tests would fail if `mutate` (or the reads) switched 
to a private lock. A small test would pin it down. For example, assert 
`Thread.holdsLock(map)` inside a `compute`/`merge` callback, or check that a 
writer thread blocks while the test holds `map.synchronized`.
   
   **Verification:**
   - **Regression:** The new test passes against the current implementation and 
fails when mutate is changed to use a different lock or no lock.



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