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

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new d64aff9ea9 fix(web): replace plugin classloaders atomically (#7089)
d64aff9ea9 is described below

commit d64aff9ea9bf298a4c43ef3d3e23191395efecaf
Author: Liming Deng <[email protected]>
AuthorDate: Thu Oct 1 07:05:32 2026 +0800

    fix(web): replace plugin classloaders atomically (#7089)
---
 .../shenyu/web/loader/ShenyuLoaderService.java     | 11 +--
 .../web/loader/ShenyuPluginClassLoaderHolder.java  | 47 ++++++++----
 .../loader/ShenyuPluginClassLoaderHolderTest.java  | 89 +++++++++++++++++++++-
 3 files changed, 124 insertions(+), 23 deletions(-)

diff --git 
a/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuLoaderService.java
 
b/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuLoaderService.java
index bf0af766d8..40cb162a80 100644
--- 
a/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuLoaderService.java
+++ 
b/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuLoaderService.java
@@ -31,7 +31,6 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
-import java.util.ArrayList;
 import java.util.Base64;
 import java.util.List;
 import java.util.Objects;
@@ -80,23 +79,21 @@ public class ShenyuLoaderService {
      */
     public void loadExtOrUploadPlugins(final PluginData uploadedJarResource) {
         try {
-            List<ShenyuLoaderResult> plugins = new ArrayList<>();
             ShenyuPluginClassLoaderHolder singleton = 
ShenyuPluginClassLoaderHolder.getSingleton();
             if (Objects.isNull(uploadedJarResource)) {
                 ExtPluginLoadResult loadResult = 
ShenyuExtPathPluginJarLoader.loadExtendPlugins(shenyuConfig.getExtPlugin().getPath());
                 
webHandler.removeExtPlugins(loadResult.getRemovedPluginNames());
                 for (PluginJarParser.PluginJar extPath : 
loadResult.getPluginJars()) {
                     LOG.info("shenyu extPlugin find new {} to load", 
extPath.getAbsolutePath());
-                    ShenyuPluginClassLoader extPathClassLoader = 
singleton.createPluginClassLoader(extPath);
-                    
plugins.addAll(extPathClassLoader.loadUploadedJarPlugins());
+                    singleton.replacePluginClassLoader(extPath,
+                            classLoader -> 
loaderPlugins(classLoader.loadUploadedJarPlugins()));
                 }
             } else {
                 PluginJarParser.PluginJar pluginJar = 
PluginJarParser.parseJar(Base64.getDecoder().decode(uploadedJarResource.getPluginJar()));
                 LOG.info("shenyu upload plugin jar find new {} to load", 
pluginJar.getJarKey());
-                ShenyuPluginClassLoader uploadPluginClassLoader = 
singleton.createPluginClassLoader(pluginJar);
-                
plugins.addAll(uploadPluginClassLoader.loadUploadedJarPlugins());
+                singleton.replacePluginClassLoader(pluginJar,
+                        classLoader -> 
loaderPlugins(classLoader.loadUploadedJarPlugins()));
             }
-            loaderPlugins(plugins);
         } catch (Exception e) {
             LOG.error("shenyu plugins load has error ", e);
         }
diff --git 
a/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolder.java
 
b/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolder.java
index d5af90eafb..bce851b768 100644
--- 
a/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolder.java
+++ 
b/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolder.java
@@ -23,6 +23,8 @@ import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Consumer;
 
 /**
  * ShenyuPluginClassLoaderHolder.
@@ -33,6 +35,8 @@ public final class ShenyuPluginClassLoaderHolder {
 
     private final Map<String, ShenyuPluginClassLoader> pluginCache = new 
ConcurrentHashMap<>();
 
+    private final Map<String, ReentrantLock> pluginLocks = new 
ConcurrentHashMap<>();
+
     private ShenyuPluginClassLoaderHolder() {
     }
 
@@ -46,19 +50,30 @@ public final class ShenyuPluginClassLoaderHolder {
     }
 
     /**
-     * createPluginClassLoader.
+     * Load and activate a plugin before replacing its previous class loader.
      *
      * @param pluginJar pluginJar
-     * @return ShenyuPluginClassLoader
+     * @param activation plugin loading and activation callback
      */
-    public ShenyuPluginClassLoader createPluginClassLoader(final 
PluginJarParser.PluginJar pluginJar) {
-        ShenyuPluginClassLoader shenyuPluginClassLoader = new 
ShenyuPluginClassLoader(pluginJar);
+    public void replacePluginClassLoader(final PluginJarParser.PluginJar 
pluginJar,
+                                         final 
Consumer<ShenyuPluginClassLoader> activation) {
         String jarKey = 
Optional.ofNullable(pluginJar.getAbsolutePath()).orElse(pluginJar.getJarKey());
-        if (pluginCache.containsKey(jarKey)) {
-            pluginCache.remove(jarKey).close();
+        ReentrantLock lock = pluginLocks.computeIfAbsent(jarKey, key -> new 
ReentrantLock());
+        lock.lock();
+        ShenyuPluginClassLoader candidate = new 
ShenyuPluginClassLoader(pluginJar);
+        try {
+            activation.accept(candidate);
+            ShenyuPluginClassLoader previous = pluginCache.get(jarKey);
+            if (Objects.nonNull(previous)) {
+                previous.close();
+            }
+            pluginCache.put(jarKey, candidate);
+        } catch (RuntimeException ex) {
+            candidate.close();
+            throw ex;
+        } finally {
+            lock.unlock();
         }
-        pluginCache.put(jarKey, shenyuPluginClassLoader);
-        return shenyuPluginClassLoader;
     }
 
     /**
@@ -68,11 +83,17 @@ public final class ShenyuPluginClassLoaderHolder {
      * @return removed plugin names
      */
     public Set<String> removePluginClassLoader(final String jarKey) {
-        ShenyuPluginClassLoader classLoader = pluginCache.remove(jarKey);
-        if (Objects.nonNull(classLoader)) {
-            Set<String> pluginNames = classLoader.getLoadedPluginNames();
-            classLoader.close();
-            return pluginNames;
+        ReentrantLock lock = pluginLocks.computeIfAbsent(jarKey, key -> new 
ReentrantLock());
+        lock.lock();
+        try {
+            ShenyuPluginClassLoader classLoader = pluginCache.remove(jarKey);
+            if (Objects.nonNull(classLoader)) {
+                Set<String> pluginNames = classLoader.getLoadedPluginNames();
+                classLoader.close();
+                return pluginNames;
+            }
+        } finally {
+            lock.unlock();
         }
         return Collections.emptySet();
     }
diff --git 
a/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolderTest.java
 
b/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolderTest.java
index 1ecec6d2ac..f0d7226923 100644
--- 
a/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolderTest.java
+++ 
b/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolderTest.java
@@ -19,8 +19,23 @@ package org.apache.shenyu.web.loader;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.apache.shenyu.plugin.api.utils.SpringBeanUtils;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ConfigurableApplicationContext;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -43,10 +58,9 @@ public final class ShenyuPluginClassLoaderHolderTest {
     }
 
     @Test
-    public void createPluginClassLoader() {
+    public void replacePluginClassLoader() {
         ShenyuPluginClassLoaderHolder singleton = 
ShenyuPluginClassLoaderHolder.getSingleton();
-        ShenyuPluginClassLoader pluginClassLoader = 
singleton.createPluginClassLoader(pluginJar);
-        assertNotNull(pluginClassLoader);
+        singleton.replacePluginClassLoader(pluginJar, classLoader -> 
assertNotNull(classLoader));
     }
 
     @Test
@@ -54,4 +68,73 @@ public final class ShenyuPluginClassLoaderHolderTest {
         ShenyuPluginClassLoaderHolder singleton = 
ShenyuPluginClassLoaderHolder.getSingleton();
         singleton.removePluginClassLoader("testKey");
     }
+
+    @Test
+    public void 
replacePluginClassLoaderSerializesLoadingAndClosesEveryDisplacedLoader() throws 
Exception {
+        int threadCount = 32;
+        String jarKey = "concurrent-test-key";
+        CountingBeanFactory beanFactory = new CountingBeanFactory();
+        ConfigurableApplicationContext context = 
mock(ConfigurableApplicationContext.class);
+        when(context.getBeanFactory()).thenReturn(beanFactory);
+        SpringBeanUtils.getInstance().setApplicationContext(context);
+        PluginJarParser.PluginJar concurrentPluginJar = 
mock(PluginJarParser.PluginJar.class);
+        when(concurrentPluginJar.getAbsolutePath()).thenReturn(jarKey);
+        
when(concurrentPluginJar.getClazzMap()).thenReturn(Collections.singletonMap("sample.Plugin",
 new byte[0]));
+        ExecutorService executor = Executors.newFixedThreadPool(threadCount);
+        CountDownLatch ready = new CountDownLatch(threadCount);
+        CountDownLatch start = new CountDownLatch(1);
+        AtomicInteger activeLoads = new AtomicInteger();
+        AtomicInteger maximumActiveLoads = new AtomicInteger();
+        List<Future<?>> futures = new ArrayList<>();
+
+        try {
+            List<Runnable> tasks = Collections.nCopies(threadCount, () -> {
+                ready.countDown();
+                try {
+                    start.await();
+                    
ShenyuPluginClassLoaderHolder.getSingleton().replacePluginClassLoader(concurrentPluginJar,
 classLoader -> {
+                        int current = activeLoads.incrementAndGet();
+                        maximumActiveLoads.accumulateAndGet(current, 
Math::max);
+                        activeLoads.decrementAndGet();
+                    });
+                } catch (InterruptedException ex) {
+                    Thread.currentThread().interrupt();
+                    throw new IllegalStateException(ex);
+                }
+            });
+            tasks.forEach(task -> futures.add(executor.submit(task)));
+            assertTrue(ready.await(5, TimeUnit.SECONDS));
+            start.countDown();
+            for (Future<?> future : futures) {
+                future.get(5, TimeUnit.SECONDS);
+            }
+
+            assertEquals(1, maximumActiveLoads.get());
+            assertEquals(threadCount - 1, beanFactory.destroyCount.get());
+        } finally {
+            start.countDown();
+            executor.shutdownNow();
+            assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
+            
ShenyuPluginClassLoaderHolder.getSingleton().removePluginClassLoader(jarKey);
+        }
+    }
+
+    private static final class CountingBeanFactory extends 
DefaultListableBeanFactory {
+
+        private final AtomicInteger destroyCount = new AtomicInteger();
+
+        @Override
+        public boolean containsBean(final String name) {
+            return true;
+        }
+
+        @Override
+        public void destroySingleton(final String beanName) {
+            destroyCount.incrementAndGet();
+        }
+
+        @Override
+        public void removeBeanDefinition(final String beanName) {
+        }
+    }
 }

Reply via email to