Copilot commented on code in PR #12549:
URL: https://github.com/apache/gluten/pull/12549#discussion_r3951052394
##########
gluten-core/src/main/scala/org/apache/gluten/config/GlutenCoreConfig.scala:
##########
@@ -39,7 +39,7 @@ class GlutenCoreConfig(conf: SQLConf) extends Logging {
def offHeapMemorySize: Long = getConf(COLUMNAR_OFFHEAP_SIZE_IN_BYTES)
- def taskOffHeapMemorySize: Long =
getConf(COLUMNAR_TASK_OFFHEAP_SIZE_IN_BYTES)
+ def taskOffHeapMemorySize: Long =
getConf(COLUMNAR_TASK_OFFHEAP_SIZE_IN_BYTES).getOrElse(0L)
Review Comment:
`COLUMNAR_TASK_OFFHEAP_SIZE_IN_BYTES` is now modeled as an optional entry
where absence has meaningful semantics (per surrounding changes/docs).
Converting `None` to `0L` in the typed accessor loses that distinction and can
cause callers to treat an 'unset/unbounded' config as an explicit zero limit. A
safer approach is to expose this accessor as `Option[Long]` (or add a second
`taskOffHeapMemorySizeOpt: Option[Long]` accessor) and update call sites that
need a concrete value to decide their own fallback.
##########
gluten-core/src/main/scala/org/apache/gluten/config/NativeConfRegistry.scala:
##########
@@ -0,0 +1,200 @@
+/*
+ * 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.gluten.config
+
+import org.apache.spark.internal.Logging
+
+import scala.collection.JavaConverters._
+
+/**
+ * A registration of one conf key that should be passed to native side.
+ *
+ * @param key
+ * the conf key.
+ * @param convert
+ * normalizes a value for native side through the conf's own converter, i.e.
the one chosen by
+ * `stringConf` / `bytesConf(unit)` / `intConf` / ... plus any `transform`.
Both a user-set value
+ * and a resolved default flow through it, so a size conf reaches native as
a number rather than
+ * as "64k", and a `timeParserPolicy` conf reaches native upper-cased even
when the user wrote it
+ * in lower case.
+ * @param declaredDefault
+ * the default to deliver when the user did not set the key, in its
already-converted native form,
+ * or `None` to deliver nothing (native's own fallback takes over).
Evaluated per delivery rather
+ * than snapshotted here, since a default may be dynamic -
`spark.sql.session.timeZone` follows
+ * the JVM default time zone, and `spark.sql.ansi.enabled` follows Spark's
own default which
+ * differs between 3.x and 4.x. Which path applies is determined at the
declaration site:
+ *
+ * - `createOptional`: `None` - nothing is delivered when unset.
+ * - `createWithDefault(value)`: the stated value in converted form.
+ * - `createWithDefaultFunction(f)`: `f`'s current result in converted form.
+ */
+case class NativeConfEntry(
+ key: String,
+ convert: String => String = identity,
+ declaredDefault: () => Option[String] = () => None)
+
+/**
+ * A registry for conf keys that should be passed to native side.
+ *
+ * This registry is the single conf-passing mechanism on JVM side, but it is
not the API to use it.
+ * Rather than maintaining hard-coded key lists in common conf-passing code,
each module (core,
+ * backend, connector, etc.) declares its own native confs at their
definition, through
+ * `ConfigRegistry`'s four builders plus `ConfigBuilder.passToNative`:
+ *
+ * - `buildConf` / `buildStaticConf` for Gluten's own configurations;
+ * - `registerConf` / `registerStaticConf` for Spark / Hadoop keys that have
no Gluten
+ * `ConfigEntry`, which declare the native delivery only.
+ *
+ * Registration happens automatically on entry creation, hence [[register]] is
meant for
+ * `ConfigBuilder` rather than for conf objects.
+ *
+ * Registrations are naturally modular: a backend's or connector's
registrations only take effect
+ * when its conf object is loaded, so e.g. Velox-only keys never leak into a
ClickHouse deployment.
+ *
+ * There are two delivery channels, matching the two lifecycle stages of a
native backend, and which
+ * ones a conf lands on follows its mutability rather than any argument:
+ *
+ * - backend: delivered once during native backend initialization. See
+ * `GlutenConfig.getNativeBackendConf`. A static conf (`buildStaticConf` /
`registerStaticConf`)
+ * goes here only, since a snapshot taken at init is its value forever.
+ * - runtime: delivered each time a native runtime instance is created, e.g.
per task pipeline /
+ * native memory manager. See `GlutenConfig.getNativeSessionConf`. A
modifiable conf
+ * (`buildConf` / `registerConf`) goes here *and* to the backend channel,
so native observes the
+ * current value wherever it reads the key.
+ *
+ * When the user did not set a registered key, what is delivered is stated at
the declaration site
+ * (see `ConfigBuilder.passToNative`): `createOptional` delivers nothing and
leaves native's own
+ * fallback in charge; `createWithDefault(value)` delivers the stated value;
and
+ * `createWithDefaultFunction(f)` delivers `f`'s result, re-evaluated on every
delivery. Delivery is
+ * always normalized through the conf's own value converter, so all per-key
parsing lives at the
+ * declaration site rather than in per-key transforms at delivery.
+ */
+
+object NativeConfRegistry extends Logging {
+
+ private val runtimeEntries =
+ new java.util.concurrent.ConcurrentHashMap[String,
NativeConfEntry]().asScala
+ private val backendEntries =
+ new java.util.concurrent.ConcurrentHashMap[String,
NativeConfEntry]().asScala
+
+ // The backend channel is delivered once, during native backend
initialization, so a registration
+ // arriving afterwards can never reach it: the conf would show up on the
runtime channel only, and
+ // native would keep using its own fallback wherever it reads the key at
init. Latched on first
+ // delivery so a late declaration is reported rather than silently
half-applied.
+ @volatile private var backendConfDelivered = false
+
+ /**
+ * Register a conf key to be passed to native side. Called by
`ConfigBuilder` when an entry
+ * declaring `passToNative` is created; conf objects declare their native
confs through the
+ * builders instead of calling this directly.
+ *
+ * @param isStatic
+ * whether the conf is static to the native backend, i.e. declared by
`buildStaticConf` /
+ * `registerStaticConf`. A static conf is delivered on the backend channel
only; a modifiable
+ * one on both.
+ * @param convert
+ * normalizes a value for native through the conf's own value converter,
applied whether the
+ * value comes from the user or from a resolved default.
+ * @param declaredDefault
+ * the default declared by the conf itself, in its already-converted
native form, evaluated per
+ * delivery so a dynamic default stays up to date. `None` for a
`createOptional` conf, which
+ * then falls back to the foreign declaration for a Spark-owned key, or is
delivered only when
+ * set.
+ */
+ private[config] def register(
+ key: String,
+ isStatic: Boolean,
+ convert: String => String = identity,
+ declaredDefault: => Option[String] = None): Unit = {
+ val entry = NativeConfEntry(key, convert, () => declaredDefault)
+ if (!isStatic) {
+ doRegister(runtimeEntries, entry)
+ }
+ doRegisterToBackend(entry)
+ }
+
+ private def doRegisterToBackend(entry: NativeConfEntry): Unit = {
+ if (backendConfDelivered) {
+ // Not fatal: the conf still works on the runtime channel, and failing
here would take down a
+ // query for a conf object that merely loaded late. But native backend
init has already
+ // happened, so declare the gap loudly - the usual cause is a conf
object that is not declared
+ // through `Component.confs()`.
+ logWarning(
+ s"Native conf ${entry.key} was declared after native backend conf had
already been " +
+ s"delivered, so it will not reach the backend channel. Declare its
conf object through " +
+ s"Component.confs() so that it is initialized before native backend
initialization.")
+ }
+ doRegister(backendEntries, entry)
+ }
+
+ private def doRegister(
+ entries: scala.collection.concurrent.Map[String, NativeConfEntry],
+ entry: NativeConfEntry): Unit = {
+ val existing = entries.putIfAbsent(entry.key, entry)
+ require(existing.isEmpty, s"Native conf ${entry.key} already registered!")
+ }
+
+ /**
+ * Visible for testing. Production code never asks which channel a key is on
- it asks for the
+ * channel's contents through `selectRuntimeConf` / `selectBackendConf`.
`private[gluten]` rather
+ * than `private[config]` because `ComponentSuite` lives in
`org.apache.gluten.component`.
+ */
+ private[gluten] def isRuntimeKey(key: String): Boolean =
runtimeEntries.contains(key)
+
+ /** Visible for testing. See [[isRuntimeKey]]. */
+ private[gluten] def isBackendKey(key: String): Boolean =
backendEntries.contains(key)
+
+ /**
+ * Select runtime-scoped native confs from the given conf map. A key absent
from `conf` is
+ * delivered with its declared default, if it has one.
+ */
+ def selectRuntimeConf(conf: scala.collection.Map[String, String]):
Map[String, String] = {
+ select(runtimeEntries, conf)
+ }
+
+ /**
+ * Select backend(static)-scoped native confs from the given conf map. A key
absent from `conf` is
+ * delivered with its declared default, if it has one.
+ *
+ * Marks the backend channel as delivered, so that a declaration arriving
afterwards - which can
+ * no longer reach native backend initialization - is reported rather than
silently applied to the
+ * runtime channel alone.
+ */
+ def selectBackendConf(conf: scala.collection.Map[String, String]):
Map[String, String] = {
+ backendConfDelivered = true
+ select(backendEntries, conf)
+ }
Review Comment:
`backendConfDelivered` is a process-wide latch with no reset path. That’s
fine for production, but it makes unit tests and any long-lived tooling that
reinitializes components in-process order-dependent and prone to log spam.
Suggestion: add a `private[config]` reset method (only used in tests) that
resets `backendConfDelivered` (and optionally clears maps), and use it from
test suites that exercise registration/delivery sequencing.
##########
gluten-core/src/test/scala/org/apache/gluten/config/NativeConfRegistrySuite.scala:
##########
@@ -0,0 +1,292 @@
+/*
+ * 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.gluten.config
+
+import org.apache.spark.network.util.ByteUnit
+import org.apache.spark.sql.internal.MapProvider
+
+import org.scalatest.funsuite.AnyFunSuite
+
+import java.util.Locale
+
+class NativeConfRegistrySuite extends AnyFunSuite {
+
+ private def withRegisteredKeys(keys: String*)(f: => Unit): Unit = {
+ try f
+ finally keys.foreach(NativeConfRegistry.unregister)
+ }
+
+ // Other suites in the same JVM may have loaded conf objects (e.g.
GlutenCoreConfig) whose
+ // initializers register real native confs. Restrict select results to the
keys under test so
+ // assertions are not affected by those global registrations.
+ private def selectRuntime(conf: Map[String, String], keys: String*):
Map[String, String] = {
+ NativeConfRegistry.selectRuntimeConf(conf).filter { case (k, _) =>
keys.contains(k) }
+ }
+
+ private def selectBackend(conf: Map[String, String], keys: String*):
Map[String, String] = {
+ NativeConfRegistry.selectBackendConf(conf).filter { case (k, _) =>
keys.contains(k) }
+ }
Review Comment:
`NativeConfRegistry.selectBackendConf` latches global state
(`backendConfDelivered = true`), which persists across tests in the same JVM.
After the first test that calls `selectBackendConf`, later declarations in this
suite will start emitting late-registration warnings (and potentially mask
regressions or make test output noisy / order-dependent). Consider adding a
test-only reset (e.g., `NativeConfRegistry.resetForTesting()` clearing
`backendConfDelivered` and maps) and calling it in `beforeEach/afterEach`, or
refactor this suite to avoid invoking `selectBackendConf` until all
declarations for the entire suite are complete.
--
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]