This is an automated email from the ASF dual-hosted git repository.
zhztheplayer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git
The following commit(s) were added to refs/heads/main by this push:
new 4d0ba0d201 [GLUTEN-12655][CORE] Reset the component graph after
ComponentSuite (#12658)
4d0ba0d201 is described below
commit 4d0ba0d2016de2f5ae7324bbae878e7e367cc382
Author: YangJie <[email protected]>
AuthorDate: Fri Jul 31 18:54:31 2026 +0800
[GLUTEN-12655][CORE] Reset the component graph after ComponentSuite (#12658)
* fix: stop ComponentSuite leaking its dummy components into later suites
The component graph and the discovery latch are JVM-global. ComponentSuite
registers dummy components into the graph, including a deliberate dependency
cycle, and never removes them, so any later suite in the same JVM that calls
Component#sorted fails with "Cycle detected in the component graph: B, D,
C".
No suite in gluten-core happens to call it after ComponentSuite today,
which is
why this has gone unnoticed, but eight call sites in main reach it,
including
GlutenDriverPlugin#init and GlutenSessionExtensions, so any future suite
that
boots a SparkContext or builds session extensions hits it.
Add a testing-only reset that empties the graph, clears each component's
registration flag so it can register again, and unlatches discovery. Call
it from
ComponentSuite#afterAll.
Add ComponentGraphResetSuite as the guard: it registers a cycle, asserts
sorting
reports it, resets, then asserts sorting succeeds and the dummy components
are
gone. Verified it fails when the reset body is emptied.
* fix: make the component-graph reset guard cover what it resets
The guard suite only caught an all-or-nothing regression. Deleting the flag
reset in Registry#clear, deleting the latch re-arm, or deleting
ComponentSuite's
afterAll override each left gluten-core fully green, because the suite
asserted
through sortedUnsafe() and its final assertion was vacuous once the graph
was
empty.
Rewrite the suite around two mutants that now fail:
- clear then re-register the same instance, which only works if
Registry#clear
resets the registration flag;
- run ComponentSuite in-process and assert the graph is empty afterwards,
which
covers the fix's own call site without depending on suite execution
order.
The cycle test now asserts isEmpty rather than absence-by-name, and cleanup
moved
to afterAll so a failed assertion cannot leak the cycle it registers.
Keep the flag and the graph in sync: ensureRegistered rolls the flag back
when
graph.add throws, since a component that never entered the graph is
invisible to
Registry#clear and could otherwise never register again. The rollback
covers only
graph.add; extending it over dependencies() would roll back a component
that is
already in the graph, making the next registration fail with a misleading
"UID already registered".
Make resetRegisteredForTesting final. Nine of the eleven in-repo Component
implementations sit in this package and could otherwise override it to a
no-op
and silently defeat the clear.
Narrow the clearAllForTesting scaladoc to what it actually guarantees:
values
derived from an earlier Component.sorted() are not reset, and rediscovery
installs fresh instances. Note on the latch line that it exists for the
backend
modules, whose classpath carries component files, so it does not read as
dead
code in gluten-core.
* fix: narrow the registration rollback to NonFatal
Catching Throwable also matches VirtualMachineError and friends, where
rolling
back a registration flag serves no purpose. graph.add only throws
IllegalArgumentException from its own require checks, so NonFatal covers
every
case the rollback is meant for.
---
.../org/apache/gluten/component/Component.scala | 34 ++++++-
.../org/apache/gluten/component/package.scala | 23 +++++
.../component/ComponentGraphResetSuite.scala | 113 +++++++++++++++++++++
.../apache/gluten/component/ComponentSuite.scala | 8 ++
4 files changed, 177 insertions(+), 1 deletion(-)
diff --git
a/gluten-core/src/main/scala/org/apache/gluten/component/Component.scala
b/gluten-core/src/main/scala/org/apache/gluten/component/Component.scala
index 7e688d6e70..e2256c01e0 100644
--- a/gluten-core/src/main/scala/org/apache/gluten/component/Component.scala
+++ b/gluten-core/src/main/scala/org/apache/gluten/component/Component.scala
@@ -28,6 +28,7 @@ import org.apache.spark.internal.Logging
import java.util.concurrent.atomic.{AtomicBoolean, AtomicInteger}
import scala.collection.mutable
+import scala.util.control.NonFatal
/**
* The base API to inject user-defined logic to Gluten. To register a
component, the implementation
@@ -47,10 +48,23 @@ trait Component {
if (!isRegistered.compareAndSet(false, true)) {
return
}
- graph.add(this)
+ try {
+ graph.add(this)
+ } catch {
+ // Nothing entered the graph, so Graph#clear will not see this component
and cannot reset
+ // its flag. Roll the flag back here to keep it in sync with the graph.
+ case NonFatal(t) =>
+ isRegistered.set(false)
+ throw t
+ }
dependencies().foreach(req => graph.declareDependency(this, req))
}
+ // Visible for testing. Paired with Graph#clear so a cleared component can
register again.
+ final private[component] def resetRegisteredForTesting(): Unit = {
+ isRegistered.set(false)
+ }
+
/**
* Determines whether a component should be registered based on runtime
conditions. For instance,
* if a component depends on a Spark extension's JAR, this method should be
overridden to check
@@ -124,6 +138,12 @@ object Component extends Logging {
graph.sorted()
}
+ // Visible for testing. Internal to component.clearAllForTesting; does not
touch the discovery
+ // latch.
+ private[component] def clearForTesting(): Unit = {
+ graph.clear()
+ }
+
private class Registry {
private val lookupByUid: mutable.Map[Int, Component] = mutable.Map()
private val lookupByClass: mutable.Map[Class[_ <: Component], Component] =
mutable.Map()
@@ -139,6 +159,12 @@ object Component extends Logging {
lookupByClass(clazz) = comp
}
+ def clear(): Unit = synchronized {
+ lookupByUid.values.foreach(_.resetRegisteredForTesting())
+ lookupByUid.clear()
+ lookupByClass.clear()
+ }
+
def isUidRegistered(uid: Int): Boolean = synchronized {
lookupByUid.contains(uid)
}
@@ -189,6 +215,12 @@ object Component extends Logging {
sortedComponents = None
}
+ def clear(): Unit = synchronized {
+ registry.clear()
+ uidAndDependencyPairs.clear()
+ sortedComponents = None
+ }
+
private def newLookup(): Map[Int, Node] = {
val uidToNodeLookup: mutable.Map[Int, Node] = mutable.Map()
diff --git
a/gluten-core/src/main/scala/org/apache/gluten/component/package.scala
b/gluten-core/src/main/scala/org/apache/gluten/component/package.scala
index cf0181c39c..9cad0c0406 100644
--- a/gluten-core/src/main/scala/org/apache/gluten/component/package.scala
+++ b/gluten-core/src/main/scala/org/apache/gluten/component/package.scala
@@ -45,4 +45,27 @@ package object component extends Logging {
)
logInfo(s"Components registered within order:
${components.map(_.name()).mkString(", ")}")
}
+
+ /**
+ * Empties the component graph and re-arms the discovery latch, so the next
call to
+ * [[Component.sorted]] runs classpath discovery again.
+ *
+ * Only the graph and the latch are reset. Values derived from an earlier
[[Component.sorted]] are
+ * not: `BackendsApiManager.backend`, `GlutenCostModel.costModelRegistry`
and the `graphCache`
+ * inside `Transition.factory` keep what they computed from the pre-clear
component set. Neither
+ * are the per-vertex `TransitionGraph.Vertex.initialized` flags, so a
re-registered component
+ * does not add its transition edges a second time. Rediscovery also
constructs fresh component
+ * instances, so such a value can end up holding an instance that is no
longer the one in the
+ * graph.
+ *
+ * Visible for testing. The graph and the latch are both JVM-global, so a
suite that registers
+ * components of its own leaks them into every later suite in the same JVM.
Such a suite must call
+ * this when it finishes.
+ */
+ private[gluten] def clearAllForTesting(): Unit = {
+ Component.clearForTesting()
+ // Re-arms discovery. Unobservable in gluten-core, whose test classpath
carries no
+ // 'META-INF/gluten-components' file; the backend modules are the ones
that need it.
+ allComponentsLoaded.set(false)
+ }
}
diff --git
a/gluten-core/src/test/scala/org/apache/gluten/component/ComponentGraphResetSuite.scala
b/gluten-core/src/test/scala/org/apache/gluten/component/ComponentGraphResetSuite.scala
new file mode 100644
index 0000000000..109f01906a
--- /dev/null
+++
b/gluten-core/src/test/scala/org/apache/gluten/component/ComponentGraphResetSuite.scala
@@ -0,0 +1,113 @@
+/*
+ * 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.component
+
+import org.apache.gluten.extension.injector.Injector
+
+import org.scalatest.{Args, BeforeAndAfterAll, Reporter}
+import org.scalatest.events.{Event, SuiteAborted, TestFailed}
+import org.scalatest.funsuite.AnyFunSuite
+
+import scala.collection.mutable
+
+class ComponentGraphResetSuite extends AnyFunSuite with BeforeAndAfterAll {
+ import ComponentGraphResetSuite._
+
+ override protected def afterAll(): Unit = {
+ // Every test here registers components of its own into the JVM-global
graph. Clearing from
+ // afterAll rather than from the test body makes sure a failed assertion
cannot leak them.
+ clearAllForTesting()
+ super.afterAll()
+ }
+
+ test("a cycle registered by an earlier suite does not outlive the reset") {
+ // Reproduces what ComponentSuite leaves behind: components wired into a
cycle. Without the
+ // reset these stay in the JVM-global graph and every later
Component#sorted call throws.
+ clearAllForTesting()
+ new CycleA().ensureRegistered()
+ new CycleB().ensureRegistered()
+ assertThrows[UnsupportedOperationException](Component.sortedUnsafe())
+
+ clearAllForTesting()
+
+ // The graph is empty again, so sorting no longer reports the cycle.
+ assert(Component.sortedUnsafe().isEmpty)
+ }
+
+ test("a component cleared from the graph can be registered again") {
+ // Covers the registration-flag reset the clear performs: without it this
very instance could
+ // never re-enter the graph, because ensureRegistered short-circuits on
its own flag.
+ clearAllForTesting()
+ val standalone = new Standalone()
+ standalone.ensureRegistered()
+ assert(Component.sortedUnsafe().exists(_.name() == StandaloneName))
+
+ clearAllForTesting()
+ assert(Component.sortedUnsafe().isEmpty)
+
+ standalone.ensureRegistered()
+ assert(Component.sortedUnsafe().exists(_.name() == StandaloneName))
+ }
+
+ test("ComponentSuite leaves the component graph empty") {
+ // Covers ComponentSuite's own cleanup without depending on suite
execution order: run that
+ // suite in-process, then assert it took its dummy components back out of
the graph.
+ clearAllForTesting()
+ val reporter = new FailureCollectingReporter()
+ val status = new ComponentSuite().run(None, Args(reporter))
+ assert(status.succeeds(), s"ComponentSuite itself failed:
${reporter.failures.mkString("; ")}")
+ assert(Component.sortedUnsafe().isEmpty)
+ }
+}
+
+object ComponentGraphResetSuite {
+ private val StandaloneName: String = "reset-standalone"
+
+ /**
+ * Keeps the nested suite's events out of the test log, while retaining
enough of them to say what
+ * went wrong if the nested suite itself fails.
+ */
+ private class FailureCollectingReporter extends Reporter {
+ private val buffer = mutable.Buffer[String]()
+
+ def failures: Seq[String] = buffer.toSeq
+
+ override def apply(event: Event): Unit = event match {
+ case e: TestFailed => buffer += s"${e.testName}: ${e.message}"
+ case e: SuiteAborted => buffer += s"${e.suiteName} aborted: ${e.message}"
+ case _ =>
+ }
+ }
+
+ private class Standalone extends Component {
+ override def name(): String = StandaloneName
+ override def dependencies(): Seq[Class[_ <: Component]] = Nil
+ override def injectRules(injector: Injector): Unit = {}
+ }
+
+ private class CycleA extends Component {
+ override def name(): String = "reset-A"
+ override def dependencies(): Seq[Class[_ <: Component]] =
Seq(classOf[CycleB])
+ override def injectRules(injector: Injector): Unit = {}
+ }
+
+ private class CycleB extends Component {
+ override def name(): String = "reset-B"
+ override def dependencies(): Seq[Class[_ <: Component]] =
Seq(classOf[CycleA])
+ override def injectRules(injector: Injector): Unit = {}
+ }
+}
diff --git
a/gluten-core/src/test/scala/org/apache/gluten/component/ComponentSuite.scala
b/gluten-core/src/test/scala/org/apache/gluten/component/ComponentSuite.scala
index b062c58b53..1e4335e942 100644
---
a/gluten-core/src/test/scala/org/apache/gluten/component/ComponentSuite.scala
+++
b/gluten-core/src/test/scala/org/apache/gluten/component/ComponentSuite.scala
@@ -27,6 +27,14 @@ import scala.collection.mutable
class ComponentSuite extends AnyFunSuite with BeforeAndAfterAll {
import ComponentSuite._
+ override protected def afterAll(): Unit = {
+ // The component graph is JVM-global, and these tests register dummy
components into it,
+ // including a deliberate dependency cycle. Leaving them behind makes any
later suite that
+ // calls Component#sorted fail.
+ clearAllForTesting()
+ super.afterAll()
+ }
+
test("Load order") {
val a = new DummyBackend("A") {}
val b = new DummyBackend("B") {}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]