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) {
+ }
+ }
}