You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spark.apache.org by gu...@apache.org on 2026/09/28 21:53:06 UTC

(spark) branch branch-4.x updated: [SPARK-59821][CORE] Make Maven utility tests safe for concurrent runs

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

HyukjinKwon 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 b1a57d5ec476 [SPARK-59821][CORE] Make Maven utility tests safe for concurrent runs
b1a57d5ec476 is described below

commit b1a57d5ec476535ac894ed1d800ece4ec008094b
Author: qianlan717 <qi...@databricks.com>
AuthorDate: Tue Sep 29 06:52:36 2026 +0900

    [SPARK-59821][CORE] Make Maven utility tests safe for concurrent runs
    
    ### What changes were proposed in this pull request?
    
    This follow-up to #58874 makes the Maven test utilities safe when the same suite runs concurrently.
    
    - Compile generated Java classes in an invocation-specific temporary directory instead of the process working directory.
    - Give each test JVM its own stable fake local Maven repository.
    - Add a regression test that compiles the same class name concurrently into separate destinations.
    
    ### Why are the changes needed?
    
    `SparkTestUtils.createCompiledClass` currently asks javac to emit a fixed class name, such as `MyLib.class`, in the process working directory before moving it to the requested destination. `MavenUtils.m2Path` also uses the fixed relative path `dummy/.m2/repository` during tests.
    
    When a test target is repeated concurrently, the JVMs can move or delete files needed by another JVM. This causes failures such as:
    
    ```
    java.nio.file.NoSuchFileException:
      MyLib.class -> dummy/.m2/repository/.../MyLib.class
    ```
    
    Using isolated temporary paths removes this cross-JVM interference without changing production Maven resolution.
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    Added a `SparkTestUtilsSuite` regression test that starts eight concurrent compilations of the same class name and verifies every output exists.
    
    `git diff --check` passes.
    
    The following focused command was attempted locally:
    
    ```
    /usr/bin/sbt \
      "common-utils/testOnly org.apache.spark.util.SparkTestUtilsSuite" \
      "common-utils/testOnly org.apache.spark.util.MavenUtilsSuite"
    ```
    
    The suites could not start because the local environment could not resolve Maven Central or the SBT plugin repositories (`UnknownHostException`). GitHub Actions will provide the executable test result.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: OpenAI Codex (GPT-5)
    
    Closes #59096 from qianlan717/SPARK-59553-concurrent-ivy-test-isolation.
    
    Authored-by: qianlan717 <qi...@databricks.com>
    Signed-off-by: Hyukjin Kwon <hy...@databricks.com>
    (cherry picked from commit aeda06559238be417f32a11bb5814af59b2bace7)
    Signed-off-by: Hyukjin Kwon <hy...@databricks.com>
---
 .../scala/org/apache/spark/util/MavenUtils.scala   |  8 ++++--
 .../org/apache/spark/util/SparkTestUtils.scala     | 31 +++++++++++++---------
 .../apache/spark/util/SparkTestUtilsSuite.scala    | 28 +++++++++++++++++++
 3 files changed, 52 insertions(+), 15 deletions(-)

diff --git a/common/utils/src/main/scala/org/apache/spark/util/MavenUtils.scala b/common/utils/src/main/scala/org/apache/spark/util/MavenUtils.scala
index 8ca1f9efe649..0f5b94fe5f87 100644
--- a/common/utils/src/main/scala/org/apache/spark/util/MavenUtils.scala
+++ b/common/utils/src/main/scala/org/apache/spark/util/MavenUtils.scala
@@ -46,6 +46,10 @@ import org.apache.spark.util.ArrayImplicits._
 private[spark] object MavenUtils extends Logging {
   val JAR_IVY_SETTING_PATH_KEY: String = "spark.jars.ivySettings"
 
+  private lazy val testM2Path = new File(
+    SparkFileUtils.createTempDir(namePrefix = "spark-test-m2"),
+    "repository")
+
   private val ivyLock = new ReentrantLock()
   private val ivyLockPollIntervalMs = 100L
 
@@ -152,8 +156,8 @@ private[spark] object MavenUtils extends Logging {
   /** Path of the local Maven cache. */
   private[util] def m2Path: File = {
     if (SparkEnvUtils.isTesting) {
-      // test builds delete the maven cache, and this can cause flakiness
-      new File("dummy", ".m2" + File.separator + "repository")
+      // Keep concurrent test JVMs from deleting each other's local Maven repository.
+      testM2Path
     } else {
       new File(System.getProperty("user.home"), ".m2" + File.separator + "repository")
     }
diff --git a/common/utils/src/main/scala/org/apache/spark/util/SparkTestUtils.scala b/common/utils/src/main/scala/org/apache/spark/util/SparkTestUtils.scala
index 0af0e0f6de45..c5832706e977 100644
--- a/common/utils/src/main/scala/org/apache/spark/util/SparkTestUtils.scala
+++ b/common/utils/src/main/scala/org/apache/spark/util/SparkTestUtils.scala
@@ -46,10 +46,8 @@ private[spark] trait SparkTestUtils {
       sourceFile: JavaSourceFromString,
       classpathUrls: Seq[URL]): File = {
     val compiler = ToolProvider.getSystemJavaCompiler
-
-    // Calling this outputs a class file in pwd. It's easier to just rename the files than
-    // build a custom FileManager that controls the output location.
-    val options = if (classpathUrls.nonEmpty) {
+    val compileDir = SparkFileUtils.createTempDir(namePrefix = "spark-javac")
+    val classpathOptions = if (classpathUrls.nonEmpty) {
       Seq(
         "-classpath",
         classpathUrls
@@ -60,17 +58,24 @@ private[spark] trait SparkTestUtils {
     } else {
       Seq.empty
     }
-    compiler.getTask(null, null, null, options.asJava, null, Arrays.asList(sourceFile)).call()
-
-    val fileName = className + ".class"
-    val result = new File(fileName)
-    assert(result.exists(), "Compiled file not found: " + result.getAbsolutePath())
-    val out = new File(destDir, fileName)
+    val options = Seq("-d", compileDir.getAbsolutePath) ++ classpathOptions
 
-    Files.move(result.toPath, out.toPath)
+    try {
+      val compiled = compiler
+        .getTask(null, null, null, options.asJava, null, Arrays.asList(sourceFile))
+        .call()
+      assert(compiled, s"Failed to compile $className")
 
-    assert(out.exists(), "Destination file not moved: " + out.getAbsolutePath())
-    out
+      val fileName = className + ".class"
+      val result = SparkFileUtils.recursiveList(compileDir)
+        .find(file => file.isFile && file.getName == fileName)
+        .getOrElse(throw new IllegalStateException(s"Compiled file not found: $fileName"))
+      val out = new File(destDir, fileName)
+      Files.move(result.toPath, out.toPath)
+      out
+    } finally {
+      SparkFileUtils.deleteRecursively(compileDir)
+    }
   }
 
   /** Creates a compiled class with the given name. Class file will be placed in destDir. */
diff --git a/common/utils/src/test/scala/org/apache/spark/util/SparkTestUtilsSuite.scala b/common/utils/src/test/scala/org/apache/spark/util/SparkTestUtilsSuite.scala
index 10a599739f6c..2b099fdc11a9 100644
--- a/common/utils/src/test/scala/org/apache/spark/util/SparkTestUtilsSuite.scala
+++ b/common/utils/src/test/scala/org/apache/spark/util/SparkTestUtilsSuite.scala
@@ -17,6 +17,11 @@
 
 package org.apache.spark.util
 
+import java.util.concurrent.{CountDownLatch, Executors}
+
+import scala.concurrent.{ExecutionContext, Future}
+import scala.concurrent.duration._
+
 import org.scalatest.funsuite.AnyFunSuite // scalastyle:ignore funsuite
 
 class SparkTestUtilsSuite extends AnyFunSuite with SparkTestUtils { // scalastyle:ignore funsuite
@@ -27,4 +32,27 @@ class SparkTestUtilsSuite extends AnyFunSuite with SparkTestUtils { // scalastyl
     val result = createCompiledClass("Hello", dir, sourceFile, Seq(dir.toURI.toURL))
     assert(result.exists(), s"Compiled class file should exist at ${result.getPath}")
   }
+
+  test("createCompiledClass supports concurrent compilation of the same class name") {
+    val executor = Executors.newFixedThreadPool(8)
+    implicit val executionContext: ExecutionContext =
+      ExecutionContext.fromExecutorService(executor)
+    val start = new CountDownLatch(1)
+    try {
+      val compiledClasses = (1 to 8).map { _ =>
+        Future {
+          val dir = SparkFileUtils.createTempDir()
+          val sourceFile = new JavaSourceFromString("Hello", "public class Hello {}")
+          start.await()
+          createCompiledClass("Hello", dir, sourceFile, Seq.empty)
+        }
+      }
+      start.countDown()
+
+      val results = SparkThreadUtils.awaitResult(Future.sequence(compiledClasses), 30.seconds)
+      assert(results.forall(_.exists()))
+    } finally {
+      executor.shutdownNow()
+    }
+  }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@spark.apache.org
For additional commands, e-mail: commits-help@spark.apache.org