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

zhouyuan pushed a commit to branch branch-1.8
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/branch-1.8 by this push:
     new e55a1fcaf3 [GLUTEN-13276][VL] Fix CelebornShuffleManager init fails 
with NoSuchMethodException: FailedShuffleCleaner.<init>() (#13283)
e55a1fcaf3 is described below

commit e55a1fcaf35937da0b2e3bdb30d0ccb40ca03cdd
Author: maoyuwei1 <[email protected]>
AuthorDate: Tue Oct 27 17:13:26 2026 +0800

    [GLUTEN-13276][VL] Fix CelebornShuffleManager init fails with 
NoSuchMethodException: FailedShuffleCleaner.<init>() (#13283)
---
 gluten-celeborn/pom.xml                            |  17 ++
 .../gluten/celeborn/CelebornShuffleManager.java    |   4 +-
 .../shuffle/gluten/celeborn/CelebornUtils.java     |  52 ++++--
 .../celeborn/CelebornShuffleManagerTest.java       | 191 ++++++++++++++++++++
 .../shuffle/gluten/celeborn/CelebornUtilsTest.java | 200 +++++++++++++++++++++
 .../celeborn/FailedShuffleCleanerTestSupport.java  |  57 ++++++
 ...TestCelebornColumnarBatchSerializerFactory.java |  32 ++++
 .../celeborn/TestCelebornShuffleWriterFactory.java |  43 +++++
 ...celeborn.CelebornColumnarBatchSerializerFactory |   1 +
 ...le.gluten.celeborn.CelebornShuffleWriterFactory |   1 +
 10 files changed, 586 insertions(+), 12 deletions(-)

diff --git a/gluten-celeborn/pom.xml b/gluten-celeborn/pom.xml
index 997de509bf..4cd255b69b 100755
--- a/gluten-celeborn/pom.xml
+++ b/gluten-celeborn/pom.xml
@@ -67,6 +67,23 @@
       <version>${guava.version}</version>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.hadoop</groupId>
+      <artifactId>hadoop-client</artifactId>
+      <version>${hadoop.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-inline</artifactId>
+      <version>4.11.0</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
diff --git 
a/gluten-celeborn/src/main/java/org/apache/spark/shuffle/gluten/celeborn/CelebornShuffleManager.java
 
b/gluten-celeborn/src/main/java/org/apache/spark/shuffle/gluten/celeborn/CelebornShuffleManager.java
index 0a82e32726..a3ba5d6dd0 100644
--- 
a/gluten-celeborn/src/main/java/org/apache/spark/shuffle/gluten/celeborn/CelebornShuffleManager.java
+++ 
b/gluten-celeborn/src/main/java/org/apache/spark/shuffle/gluten/celeborn/CelebornShuffleManager.java
@@ -179,8 +179,8 @@ public class CelebornShuffleManager
           // for Celeborn 0.6.0
           CelebornUtils.incrementApplicationCount(lifecycleManager);
           CelebornUtils.registerCancelShuffleCallback(lifecycleManager);
-          CelebornUtils.stageRerun(
-              stageRerunEnabled, lifecycleManager, celebornConf, 
failedShuffleCleaner);
+          failedShuffleCleaner =
+              CelebornUtils.stageRerun(stageRerunEnabled, lifecycleManager, 
celebornConf);
 
           shuffleClient =
               CelebornUtils.getShuffleClient(
diff --git 
a/gluten-celeborn/src/main/java/org/apache/spark/shuffle/gluten/celeborn/CelebornUtils.java
 
b/gluten-celeborn/src/main/java/org/apache/spark/shuffle/gluten/celeborn/CelebornUtils.java
index 292ed9cbcd..98a1701d08 100644
--- 
a/gluten-celeborn/src/main/java/org/apache/spark/shuffle/gluten/celeborn/CelebornUtils.java
+++ 
b/gluten-celeborn/src/main/java/org/apache/spark/shuffle/gluten/celeborn/CelebornUtils.java
@@ -200,9 +200,20 @@ public class CelebornUtils {
       try {
         Class<?> clazz = Class.forName(className);
 
-        Constructor<?> constructor = clazz.getConstructor();
+        Constructor<?> matched = null;
+        for (Constructor<?> constructor : clazz.getConstructors()) {
+          if (isConstructorMatched(constructor, args)) {
+            matched = constructor;
+            break;
+          }
+        }
+        if (matched == null) {
+          throw new NoSuchMethodException(
+              className + ".<init> with " + args.length + " matched 
argument(s)");
+        }
+        matched.setAccessible(true);
 
-        return constructor.newInstance(args);
+        return matched.newInstance(args);
 
       } catch (ClassNotFoundException e) {
         return null;
@@ -212,6 +223,23 @@ public class CelebornUtils {
     }
   }
 
+  private static boolean isConstructorMatched(Constructor<?> constructor, 
Object... args) {
+    Class<?>[] parameterTypes = constructor.getParameterTypes();
+    if (parameterTypes.length != args.length) {
+      return false;
+    }
+    for (int i = 0; i < parameterTypes.length; i++) {
+      if (args[i] == null) {
+        if (parameterTypes[i].isPrimitive()) {
+          return false;
+        }
+      } else if (!parameterTypes[i].isAssignableFrom(args[i].getClass())) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   public static CelebornShuffleHandle getCelebornShuffleHandle(
       String appUniqueId,
       String lifecycleManagerHost,
@@ -359,11 +387,9 @@ public class CelebornUtils {
     }
   }
 
-  public static void stageRerun(
-      boolean stageRerunEnabled,
-      LifecycleManager lifecycleManager,
-      CelebornConf celebornConf,
-      Object failedShuffleCleaner) {
+  public static Object stageRerun(
+      boolean stageRerunEnabled, LifecycleManager lifecycleManager, 
CelebornConf celebornConf) {
+    Object failedShuffleCleaner = null;
     if (stageRerunEnabled) {
       MapOutputTrackerMaster mapOutputTracker =
           (MapOutputTrackerMaster) SparkEnv.get().mapOutputTracker();
@@ -375,9 +401,10 @@ public class CelebornUtils {
 
       // for Celeborn 0.6.0
       registerCelebornSkewShuffleCheckCallback(lifecycleManager, celebornConf);
-      fetchCleanFailedShuffle(lifecycleManager, celebornConf, 
failedShuffleCleaner);
+      failedShuffleCleaner = fetchCleanFailedShuffle(lifecycleManager, 
celebornConf);
       reducerFileGroupBroadcast(lifecycleManager, celebornConf);
     }
+    return failedShuffleCleaner;
   }
 
   public static void registerShuffleTrackerCallback(
@@ -538,14 +565,18 @@ public class CelebornUtils {
     }
   }
 
-  public static void fetchCleanFailedShuffle(
-      LifecycleManager lifecycleManager, CelebornConf celebornConf, Object 
failedShuffleCleaner) {
+  public static Object fetchCleanFailedShuffle(
+      LifecycleManager lifecycleManager, CelebornConf celebornConf) {
+    Object failedShuffleCleaner = null;
     try {
       // for Celeborn 0.6.0
       Method clientFetchCleanFailedShuffleMethod =
           
celebornConf.getClass().getDeclaredMethod("clientFetchCleanFailedShuffle");
       if ((Boolean) clientFetchCleanFailedShuffleMethod.invoke(celebornConf)) {
         failedShuffleCleaner = createInstance(FAILED_SHUFFLE_CLEANER_NAME, 
lifecycleManager);
+        if (failedShuffleCleaner == null) {
+          return null;
+        }
         Method registerValidateCelebornShuffleIdForCleanCallbackMethod =
             lifecycleManager
                 .getClass()
@@ -589,6 +620,7 @@ public class CelebornUtils {
     } catch (Exception e) {
       throw new RuntimeException(e);
     }
+    return failedShuffleCleaner;
   }
 
   public static void reducerFileGroupBroadcast(
diff --git 
a/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/CelebornShuffleManagerTest.java
 
b/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/CelebornShuffleManagerTest.java
new file mode 100644
index 0000000000..007d761363
--- /dev/null
+++ 
b/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/CelebornShuffleManagerTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.shuffle.gluten.celeborn;
+
+import org.apache.celeborn.client.LifecycleManager;
+import org.apache.celeborn.client.ShuffleClient;
+import org.apache.celeborn.common.CelebornConf;
+import org.apache.spark.MapOutputTrackerMaster;
+import org.apache.spark.SparkConf;
+import org.apache.spark.SparkEnv;
+import org.apache.spark.shuffle.celeborn.SparkUtils;
+import org.junit.Test;
+import org.mockito.Answers;
+import org.mockito.MockedConstruction;
+import org.mockito.MockedStatic;
+import org.mockito.stubbing.Answer;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.LongAdder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.withSettings;
+
+/**
+ * Tests for the {@code FailedShuffleCleaner} ownership of {@link 
CelebornShuffleManager}.
+ *
+ * <p>The test is hermetic: {@link LifecycleManager} construction and the 
{@link ShuffleClient}
+ * factory are mocked, so no {@code RpcEnv}, no {@code ApplicationHeartbeater} 
and no RPC against a
+ * Celeborn master is involved. Only the cleaner itself is a real Celeborn 
object, because its
+ * executor is the resource whose lifecycle is under test.
+ */
+public class CelebornShuffleManagerTest {
+
+  private static final String STAGE_RERUN_KEY = 
"spark.celeborn.client.spark.stageRerun.enabled";
+  private static final String CLEAN_FAILED_SHUFFLE_KEY =
+      "spark.celeborn.client.spark.fetch.cleanFailedShuffle";
+  private static final String CLEAN_FAILED_SHUFFLE_INTERVAL_KEY =
+      "spark.celeborn.client.spark.fetch.cleanFailedShuffleInterval";
+
+  @Test
+  public void 
testManagerRetainsFailedShuffleCleanerAndStopShutsDownItsExecutor() throws 
Exception {
+    FailedShuffleCleanerTestSupport.assumeSupported();
+
+    // SparkConf(false) ignores ambient spark.* system properties, so the 
manager only sees the
+    // settings below.
+    SparkConf sparkConf =
+        new SparkConf(false)
+            .set(STAGE_RERUN_KEY, "true")
+            .set(CLEAN_FAILED_SHUFFLE_KEY, "true")
+            // Long enough that the periodic cleanup never fires while the 
test runs.
+            .set(CLEAN_FAILED_SHUFFLE_INTERVAL_KEY, "1h")
+            .set("spark.sql.adaptive.localShuffleReader.enabled", "false");
+
+    SparkEnv previousEnv = SparkEnv.get();
+    SparkEnv env = mock(SparkEnv.class);
+    when(env.executorId()).thenReturn("driver");
+    
when(env.mapOutputTracker()).thenReturn(mock(MapOutputTrackerMaster.class));
+    SparkEnv.set(env);
+    try (MockedStatic<ShuffleClient> shuffleClients = 
mockStatic(ShuffleClient.class);
+        MockedStatic<SparkUtils> sparkUtils = mockStatic(SparkUtils.class, 
sparkUtilsAnswer());
+        MockedConstruction<LifecycleManager> lifecycleManagers =
+            mockConstruction(
+                LifecycleManager.class,
+                context -> 
withSettings().defaultAnswer(lifecycleManagerAnswer(context)),
+                (lifecycleManager, context) -> {})) {
+      CelebornShuffleManager manager = new CelebornShuffleManager(sparkConf);
+      ExecutorService cleanerExecutor;
+      try {
+        initializeLifecycleManager(manager, "gluten-celeborn-test-app");
+        assertEquals(
+            "The driver side manager should create exactly one 
LifecycleManager",
+            1,
+            lifecycleManagers.constructed().size());
+
+        Object cleaner = failedShuffleCleanerOf(manager);
+        assertNotNull(
+            "CelebornShuffleManager should retain the FailedShuffleCleaner 
created during"
+                + " LifecycleManager initialization",
+            cleaner);
+        assertEquals(CelebornUtils.FAILED_SHUFFLE_CLEANER_NAME, 
cleaner.getClass().getName());
+
+        cleanerExecutor = FailedShuffleCleanerTestSupport.executorOf(cleaner);
+        assertNotNull("FailedShuffleCleaner should own a scheduled executor", 
cleanerExecutor);
+        assertFalse(
+            "FailedShuffleCleaner executor should be running before the 
manager is stopped",
+            cleanerExecutor.isShutdown());
+      } finally {
+        manager.stop();
+      }
+
+      assertTrue(
+          "CelebornShuffleManager.stop() should shut down the cleaner 
executor",
+          cleanerExecutor.isShutdown());
+      assertTrue(
+          "cleaner executor should terminate once the manager is stopped",
+          cleanerExecutor.awaitTermination(10, TimeUnit.SECONDS));
+      assertNull(
+          "CelebornShuffleManager.stop() should release the cleaner reference",
+          failedShuffleCleanerOf(manager));
+    } finally {
+      SparkEnv.set(previousEnv);
+    }
+  }
+
+  /**
+   * Keeps the real Celeborn {@code SparkUtils} behavior, except for {@code 
addSparkListener}, which
+   * dereferences {@code SparkContext.getActive()} without a null check and 
would therefore require
+   * a live SparkContext. Registering the listener is unrelated to the cleaner 
ownership under test.
+   */
+  private static Answer<Object> sparkUtilsAnswer() {
+    return invocation -> {
+      if ("addSparkListener".equals(invocation.getMethod().getName())) {
+        return null;
+      }
+      return invocation.callRealMethod();
+    };
+  }
+
+  /**
+   * Drives the production initialization path, which is where the cleaner has 
to be retained. It is
+   * reached from {@code registerShuffle} in production, which additionally 
needs a live
+   * SparkContext, so the private method is invoked directly instead.
+   */
+  private static void initializeLifecycleManager(CelebornShuffleManager 
manager, String appId)
+      throws ReflectiveOperationException {
+    Method method =
+        
CelebornShuffleManager.class.getDeclaredMethod("initializeLifecycleManager", 
String.class);
+    method.setAccessible(true);
+    try {
+      method.invoke(manager, appId);
+    } catch (InvocationTargetException e) {
+      if (e.getCause() instanceof RuntimeException) {
+        throw (RuntimeException) e.getCause();
+      }
+      throw e;
+    }
+  }
+
+  private static Object failedShuffleCleanerOf(CelebornShuffleManager manager)
+      throws ReflectiveOperationException {
+    Field field = 
CelebornShuffleManager.class.getDeclaredField("failedShuffleCleaner");
+    field.setAccessible(true);
+    return field.get(manager);
+  }
+
+  /**
+   * Answers only what the production initialization path reads from the 
LifecycleManager: the conf
+   * the manager passed to the constructor, and the counters it increments. 
Everything else keeps
+   * the Mockito defaults, so no real LifecycleManager behavior is involved. 
Return types are
+   * matched instead of method names to stay tolerant to Celeborn version 
differences.
+   */
+  private static Answer<Object> 
lifecycleManagerAnswer(MockedConstruction.Context context) {
+    CelebornConf celebornConf = (CelebornConf) context.arguments().get(1);
+    return invocation -> {
+      Class<?> returnType = invocation.getMethod().getReturnType();
+      if (CelebornConf.class.equals(returnType)) {
+        return celebornConf;
+      }
+      if (LongAdder.class.equals(returnType)) {
+        return new LongAdder();
+      }
+      return Answers.RETURNS_DEFAULTS.answer(invocation);
+    };
+  }
+}
diff --git 
a/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/CelebornUtilsTest.java
 
b/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/CelebornUtilsTest.java
new file mode 100644
index 0000000000..915ba979da
--- /dev/null
+++ 
b/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/CelebornUtilsTest.java
@@ -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.spark.shuffle.gluten.celeborn;
+
+import org.apache.celeborn.client.LifecycleManager;
+import org.apache.celeborn.common.CelebornConf;
+import org.junit.Test;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for the Celeborn reflection compatibility layer in {@link 
CelebornUtils}. The manager side
+ * ownership of the cleaner is covered by {@link CelebornShuffleManagerTest}.
+ */
+public class CelebornUtilsTest {
+
+  /** Mirrors classes like {@code FailedShuffleCleaner} that only expose a 
parameterized ctor. */
+  public static class ParameterizedOnly {
+    public final String value;
+
+    public ParameterizedOnly(String value) {
+      this.value = value;
+    }
+  }
+
+  /** Mirrors classes usable through both the no-arg and the parameterized 
ctor. */
+  public static class NoArgAndParameterized {
+    public String value;
+
+    public NoArgAndParameterized() {
+      this.value = "no-arg";
+    }
+
+    public NoArgAndParameterized(String value) {
+      this.value = value;
+    }
+  }
+
+  /** Only accepts a primitive parameter, which can never be fed with a {@code 
null} arg. */
+  public static class PrimitiveParameter {
+    public PrimitiveParameter(int value) {}
+  }
+
+  /** Accepts any reference type, so a {@code String} arg must be assignable. 
*/
+  public static class SupertypeParameter {
+    public final Object value;
+
+    public SupertypeParameter(Object value) {
+      this.value = value;
+    }
+  }
+
+  @Test
+  public void testCreateInstanceWithParameterizedConstructor() {
+    Object instance = 
CelebornUtils.createInstance(ParameterizedOnly.class.getName(), "arg");
+    assertTrue(instance instanceof ParameterizedOnly);
+    assertEquals("arg", ((ParameterizedOnly) instance).value);
+  }
+
+  @Test
+  public void testCreateInstancePicksConstructorMatchingArguments() {
+    Object noArg = 
CelebornUtils.createInstance(NoArgAndParameterized.class.getName());
+    assertEquals("no-arg", ((NoArgAndParameterized) noArg).value);
+
+    Object withArg = 
CelebornUtils.createInstance(NoArgAndParameterized.class.getName(), "arg");
+    assertEquals("arg", ((NoArgAndParameterized) withArg).value);
+  }
+
+  @Test
+  public void testCreateInstanceAcceptsSubtypeArgument() {
+    Object instance = 
CelebornUtils.createInstance(SupertypeParameter.class.getName(), "arg");
+    assertTrue(instance instanceof SupertypeParameter);
+    assertEquals("arg", ((SupertypeParameter) instance).value);
+  }
+
+  @Test
+  public void testCreateInstanceAcceptsNullForReferenceParameter() {
+    Object instance =
+        CelebornUtils.createInstance(SupertypeParameter.class.getName(), 
(Object) null);
+    assertTrue(instance instanceof SupertypeParameter);
+    assertNull(((SupertypeParameter) instance).value);
+  }
+
+  @Test
+  public void testCreateInstanceRejectsNullForPrimitiveParameter() {
+    try {
+      CelebornUtils.createInstance(PrimitiveParameter.class.getName(), 
(Object) null);
+      fail("Expected RuntimeException since no constructor accepts null for a 
primitive");
+    } catch (RuntimeException e) {
+      assertTrue(e.getCause() instanceof NoSuchMethodException);
+    }
+  }
+
+  @Test
+  public void testCreateInstanceThrowsWhenNoConstructorMatches() {
+    try {
+      CelebornUtils.createInstance(PrimitiveParameter.class.getName(), 
"not-an-int");
+      fail("Expected RuntimeException since no constructor matches the 
argument type");
+    } catch (RuntimeException e) {
+      assertTrue(e.getCause() instanceof NoSuchMethodException);
+    }
+  }
+
+  @Test
+  public void testCreateInstanceReturnsNullWhenClassIsAbsent() {
+    
assertNull(CelebornUtils.createInstance("org.apache.celeborn.NonexistentClass"));
+  }
+
+  @Test
+  public void 
testFetchCleanFailedShuffleReturnsCleanerAndStopShutsDownItsExecutor()
+      throws Exception {
+    FailedShuffleCleanerTestSupport.assumeSupported();
+
+    CelebornConf conf = hermeticConf();
+    conf.set("celeborn.client.spark.fetch.cleanFailedShuffle", "true");
+    // A mocked LifecycleManager keeps the test hermetic: no RpcEnv, no 
ApplicationHeartbeater and
+    // therefore no RPC against an ambient Celeborn master.
+    LifecycleManager lifecycleManager = mock(LifecycleManager.class);
+    when(lifecycleManager.conf()).thenReturn(conf);
+
+    Object cleaner = CelebornUtils.fetchCleanFailedShuffle(lifecycleManager, 
conf);
+    ExecutorService cleanerExecutor = null;
+    try {
+      assertNotNull("FailedShuffleCleaner instance should be returned to the 
caller", cleaner);
+      assertEquals(CelebornUtils.FAILED_SHUFFLE_CLEANER_NAME, 
cleaner.getClass().getName());
+      // The returned instance is the very one wired into the LifecycleManager 
callbacks.
+      
verify(lifecycleManager).registerValidateCelebornShuffleIdForCleanCallback(any());
+      verify(lifecycleManager).registerUnregisterShuffleCallback(any());
+
+      cleanerExecutor = FailedShuffleCleanerTestSupport.executorOf(cleaner);
+      assertNotNull("FailedShuffleCleaner should own a scheduled executor", 
cleanerExecutor);
+      assertFalse(
+          "FailedShuffleCleaner executor should be running before stop",
+          cleanerExecutor.isShutdown());
+    } finally {
+      if (cleaner != null) {
+        CelebornUtils.stopFailedShuffleCleaner(cleaner);
+      }
+    }
+
+    assertTrue(
+        "stopFailedShuffleCleaner should shut down the cleaner executor",
+        cleanerExecutor.isShutdown());
+    assertTrue(
+        "cleaner executor should terminate once the cleaner is stopped",
+        cleanerExecutor.awaitTermination(10, TimeUnit.SECONDS));
+  }
+
+  @Test
+  public void testFetchCleanFailedShuffleDisabledCreatesNoCleaner() {
+    FailedShuffleCleanerTestSupport.assumeSupported();
+
+    CelebornConf conf = hermeticConf();
+    conf.set("celeborn.client.spark.fetch.cleanFailedShuffle", "false");
+    LifecycleManager lifecycleManager = mock(LifecycleManager.class);
+
+    assertNull(CelebornUtils.fetchCleanFailedShuffle(lifecycleManager, conf));
+    verifyNoInteractions(lifecycleManager);
+  }
+
+  @Test
+  public void testStageRerunDisabledReturnsNull() {
+    assertNull(CelebornUtils.stageRerun(false, null, hermeticConf()));
+  }
+
+  /**
+   * Builds a {@link CelebornConf} that ignores ambient {@code celeborn.*} 
settings coming from
+   * system properties, so the assertions below cannot be influenced by the 
local environment.
+   */
+  private static CelebornConf hermeticConf() {
+    return new CelebornConf(false);
+  }
+}
diff --git 
a/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/FailedShuffleCleanerTestSupport.java
 
b/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/FailedShuffleCleanerTestSupport.java
new file mode 100644
index 0000000000..f8eb956985
--- /dev/null
+++ 
b/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/FailedShuffleCleanerTestSupport.java
@@ -0,0 +1,57 @@
+/*
+ * 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.shuffle.gluten.celeborn;
+
+import org.apache.celeborn.common.CelebornConf;
+import org.junit.Assume;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.ExecutorService;
+
+/** Shared helpers for the {@code FailedShuffleCleaner} lifecycle assertions. 
*/
+final class FailedShuffleCleanerTestSupport {
+
+  private FailedShuffleCleanerTestSupport() {}
+
+  /**
+   * Keeps the cleaner assertions strict on Celeborn clients that ship {@code 
FailedShuffleCleaner},
+   * and skips them on older clients where {@link 
CelebornUtils#fetchCleanFailedShuffle} is a no-op
+   * by design.
+   */
+  static void assumeSupported() {
+    try {
+      CelebornConf.class.getDeclaredMethod("clientFetchCleanFailedShuffle");
+      Class.forName(CelebornUtils.FAILED_SHUFFLE_CLEANER_NAME);
+    } catch (NoSuchMethodException | ClassNotFoundException e) {
+      Assume.assumeNoException("Celeborn client in use does not support 
FailedShuffleCleaner", e);
+    }
+  }
+
+  /**
+   * Returns the executor owned by the cleaner, which is the resource that 
leaked before the fix.
+   * The field is looked up by type instead of by name to stay tolerant to 
Celeborn renames.
+   */
+  static ExecutorService executorOf(Object cleaner) throws 
ReflectiveOperationException {
+    for (Field field : cleaner.getClass().getDeclaredFields()) {
+      if (ExecutorService.class.isAssignableFrom(field.getType())) {
+        field.setAccessible(true);
+        return (ExecutorService) field.get(cleaner);
+      }
+    }
+    throw new AssertionError("No ExecutorService field found on " + 
cleaner.getClass().getName());
+  }
+}
diff --git 
a/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/TestCelebornColumnarBatchSerializerFactory.java
 
b/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/TestCelebornColumnarBatchSerializerFactory.java
new file mode 100644
index 0000000000..ba65f70043
--- /dev/null
+++ 
b/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/TestCelebornColumnarBatchSerializerFactory.java
@@ -0,0 +1,32 @@
+/*
+ * 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.shuffle.gluten.celeborn;
+
+/**
+ * Satisfies the {@link CelebornShuffleManager} static initializer, which 
requires exactly one
+ * factory on the classpath. The real implementations live in the backend 
modules, which this module
+ * does not depend on.
+ */
+public class TestCelebornColumnarBatchSerializerFactory
+    implements CelebornColumnarBatchSerializerFactory {
+
+  @Override
+  public String columnarBatchSerializerClass() {
+    throw new UnsupportedOperationException(
+        "Columnar serializers are out of scope for these tests");
+  }
+}
diff --git 
a/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/TestCelebornShuffleWriterFactory.java
 
b/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/TestCelebornShuffleWriterFactory.java
new file mode 100644
index 0000000000..8e19880589
--- /dev/null
+++ 
b/gluten-celeborn/src/test/java/org/apache/spark/shuffle/gluten/celeborn/TestCelebornShuffleWriterFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.shuffle.gluten.celeborn;
+
+import org.apache.celeborn.client.ShuffleClient;
+import org.apache.celeborn.common.CelebornConf;
+import org.apache.spark.TaskContext;
+import org.apache.spark.shuffle.ShuffleWriteMetricsReporter;
+import org.apache.spark.shuffle.ShuffleWriter;
+import org.apache.spark.shuffle.celeborn.CelebornShuffleHandle;
+
+/**
+ * Satisfies the {@link CelebornShuffleManager} static initializer, which 
requires exactly one
+ * factory on the classpath. The real implementations live in the backend 
modules, which this module
+ * does not depend on.
+ */
+public class TestCelebornShuffleWriterFactory implements 
CelebornShuffleWriterFactory {
+
+  @Override
+  public <K, V> ShuffleWriter<K, V> createShuffleWriterInstance(
+      int shuffleId,
+      CelebornShuffleHandle<K, V, V> handle,
+      TaskContext context,
+      CelebornConf celebornConf,
+      ShuffleClient client,
+      ShuffleWriteMetricsReporter writeMetrics) {
+    throw new UnsupportedOperationException("Shuffle writers are out of scope 
for these tests");
+  }
+}
diff --git 
a/gluten-celeborn/src/test/resources/META-INF/services/org.apache.spark.shuffle.gluten.celeborn.CelebornColumnarBatchSerializerFactory
 
b/gluten-celeborn/src/test/resources/META-INF/services/org.apache.spark.shuffle.gluten.celeborn.CelebornColumnarBatchSerializerFactory
new file mode 100644
index 0000000000..6babefbaaa
--- /dev/null
+++ 
b/gluten-celeborn/src/test/resources/META-INF/services/org.apache.spark.shuffle.gluten.celeborn.CelebornColumnarBatchSerializerFactory
@@ -0,0 +1 @@
+org.apache.spark.shuffle.gluten.celeborn.TestCelebornColumnarBatchSerializerFactory
diff --git 
a/gluten-celeborn/src/test/resources/META-INF/services/org.apache.spark.shuffle.gluten.celeborn.CelebornShuffleWriterFactory
 
b/gluten-celeborn/src/test/resources/META-INF/services/org.apache.spark.shuffle.gluten.celeborn.CelebornShuffleWriterFactory
new file mode 100644
index 0000000000..aec860240c
--- /dev/null
+++ 
b/gluten-celeborn/src/test/resources/META-INF/services/org.apache.spark.shuffle.gluten.celeborn.CelebornShuffleWriterFactory
@@ -0,0 +1 @@
+org.apache.spark.shuffle.gluten.celeborn.TestCelebornShuffleWriterFactory


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to