Skip to content

Commit b799a71

Browse files
Fix duplicate VFS provider registration when multiple CARs bundle the same connector dependency
When multiple Carbon Applications (CARs) each bundle the same File Connector dependency (e.g. commons-vfs2-sandbox), LibDeployerUtils.addDependencyToSynapseLibrary() and createSynapseLibrary() added the same-named JAR (at different temp-dir paths) to the shared LibClassLoader multiple times. StandardFileSystemManager.configurePlugins() then loaded META-INF/vfs-providers.xml twice, causing DefaultFileSystemManager.addProvider() to throw "Multiple providers registered for URL scheme 'smb2'", opening the circuit breaker and permanently breaking all File Connector operations. Fix: add a filename-based isDependencyAlreadyLoaded() guard in both methods that skips adding a JAR to the classloader if a URL with the same filename is already present. First-time additions are unaffected; duplicate same-named jars from subsequent CARs are silently skipped (logged at DEBUG level). Also adds LibDeployerUtilsTest (8 unit tests) covering the dedup logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2150d87 commit b799a71

3 files changed

Lines changed: 345 additions & 3 deletions

File tree

modules/core/src/main/java/org/apache/synapse/libraries/LibClassLoader.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ public void addToClassPath(String path) throws MalformedURLException {
4848

4949
File file = new File(path);
5050
ArrayList urls = new ArrayList();
51-
urls.add(file.toURL());
51+
if (file.exists()) {
52+
urls.add(file.toURL());
53+
}
5254
File libfiles = new File(file, "lib");
5355
if (!addFiles(urls, libfiles)) {
5456
libfiles = new File(file, "Lib");

modules/core/src/main/java/org/apache/synapse/libraries/util/LibDeployerUtils.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,14 @@ public static Library createSynapseLibrary(String libPath, DeploymentFileData de
9191
} else {
9292
if (deployedLibClassLoader instanceof LibClassLoader) {
9393
try {
94-
((LibClassLoader) deployedLibClassLoader).addToClassPath(extractPath);
94+
if (!isDependencyAlreadyLoaded((LibClassLoader) deployedLibClassLoader, extractPath)) {
95+
((LibClassLoader) deployedLibClassLoader).addToClassPath(extractPath);
96+
} else {
97+
if (log.isDebugEnabled()) {
98+
log.debug("Synapse Library '" + libArtifactName +
99+
"' is already loaded in the class loader. Skipping duplicate addition of: " + extractPath);
100+
}
101+
}
95102
} catch (MalformedURLException e) {
96103
throw new SynapseArtifactDeploymentException("Error setting up lib classpath for Synapse" +
97104
" Library : " + libFile.getAbsolutePath(), e);
@@ -133,7 +140,15 @@ public static void addDependencyToSynapseLibrary(String libraryName, String depe
133140
if (classLoader instanceof LibClassLoader) {
134141
LibClassLoader libClassLoader = (LibClassLoader) classLoader;
135142
try {
136-
libClassLoader.addToClassPath(dependencyPath);
143+
if (!isDependencyAlreadyLoaded(libClassLoader, dependencyPath)) {
144+
libClassLoader.addToClassPath(dependencyPath);
145+
} else {
146+
if (log.isDebugEnabled()) {
147+
log.debug("Dependency '" + new File(dependencyPath).getName() +
148+
"' is already loaded in the class loader for Synapse Library '" +
149+
libraryName + "'. Skipping duplicate addition.");
150+
}
151+
}
137152
} catch (MalformedURLException e) {
138153
throw new SynapseArtifactDeploymentException("Error while adding dependency to the Synapse Library : " +
139154
libraryName, e);
@@ -593,6 +608,25 @@ private static void copyInputStream(InputStream in, OutputStream out)
593608

594609
/////////////////// End Of Common Utility Methods
595610

611+
/**
612+
* Checks whether a dependency (identified by its file name) is already loaded
613+
* in the given LibClassLoader. This guards against duplicate class-path entries
614+
* when multiple CARs bundle the same connector dependency jar.
615+
*
616+
* @param libClassLoader the class loader to inspect
617+
* @param dependencyPath absolute path to the dependency jar or directory
618+
* @return true if a URL with the same file name is already present
619+
*/
620+
private static boolean isDependencyAlreadyLoaded(LibClassLoader libClassLoader, String dependencyPath) {
621+
String dependencyName = new File(dependencyPath).getName();
622+
for (URL url : libClassLoader.getURLs()) {
623+
File urlFile = new File(url.getPath());
624+
if (dependencyName.equals(urlFile.getName())) {
625+
return true;
626+
}
627+
}
628+
return false;
629+
}
596630

597631
public static void main(String[] args) {
598632
new SynapseLibrary(null, null).resolveDependencies(null);
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/*
2+
* Copyright (c) 2026, WSO2 LLC. (http://www.wso2.org) All Rights Reserved.
3+
*
4+
* WSO2 LLC. licenses this file to you under the Apache License,
5+
* Version 2.0 (the "License"); you may not use this file except
6+
* in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing,
12+
* software distributed under the License is distributed on an
13+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
* KIND, either express or implied. See the License for the
15+
* specific language governing permissions and limitations
16+
* under the License.
17+
*/
18+
package org.apache.synapse.libraries.util;
19+
20+
import org.apache.axis2.deployment.DeploymentException;
21+
import org.apache.synapse.config.SynapseConfiguration;
22+
import org.apache.synapse.libraries.LibClassLoader;
23+
import org.junit.After;
24+
import org.junit.Before;
25+
import org.junit.Test;
26+
27+
import java.io.File;
28+
import java.io.IOException;
29+
import java.net.URL;
30+
import java.nio.file.Files;
31+
import java.nio.file.Path;
32+
33+
import static org.junit.Assert.*;
34+
35+
/**
36+
* Unit tests for {@link LibDeployerUtils#addDependencyToSynapseLibrary(String, String)}.
37+
* <p>
38+
* Covers the deduplication fix for issue #4965: when multiple CARs each bundle the same
39+
* connector dependency JAR (e.g., {@code commons-vfs2-sandbox-2.10.0-wso2v4.jar}), the
40+
* second CAR's deployment must <em>not</em> add a duplicate URL to the shared
41+
* {@link LibClassLoader}. Without this dedup, {@code StandardFileSystemManager.configurePlugins()}
42+
* encounters two copies of {@code META-INF/vfs-providers.xml} and throws
43+
* {@code FileSystemException: Multiple providers registered for URL scheme "smb2"},
44+
* which opens the circuit breaker and permanently breaks File Connector operations.
45+
*/
46+
public class LibDeployerUtilsTest {
47+
48+
private static final String LIBRARY_NAME = "{org.wso2.carbon.connector}file";
49+
50+
private Path car1Dir;
51+
private Path car2Dir;
52+
private Path car3Dir;
53+
54+
@Before
55+
public void setUp() throws IOException {
56+
// Ensure the static SynapseConfiguration classloader registry is clean before each test.
57+
SynapseConfiguration.getLibraryClassLoaders().clear();
58+
car1Dir = Files.createTempDirectory("issue4965_car1_");
59+
car2Dir = Files.createTempDirectory("issue4965_car2_");
60+
car3Dir = Files.createTempDirectory("issue4965_car3_");
61+
}
62+
63+
@After
64+
public void tearDown() {
65+
SynapseConfiguration.getLibraryClassLoaders().clear();
66+
deleteQuietly(car1Dir.toFile());
67+
deleteQuietly(car2Dir.toFile());
68+
deleteQuietly(car3Dir.toFile());
69+
}
70+
71+
// -------------------------------------------------------------------------
72+
// Basic classloader lifecycle
73+
// -------------------------------------------------------------------------
74+
75+
/**
76+
* Verifies that the first call to {@code addDependencyToSynapseLibrary} creates a
77+
* {@link LibClassLoader} and registers it in {@link SynapseConfiguration}.
78+
*/
79+
@Test
80+
public void testFirstCallCreatesLibClassLoader() throws DeploymentException, IOException {
81+
String libName = "{org.wso2.carbon.connector}newlib";
82+
File jar = createTempFile(car1Dir, "newlib-1.0.0.jar");
83+
84+
assertNull("No classloader should exist before the first call",
85+
SynapseConfiguration.getClassLoader(libName));
86+
87+
LibDeployerUtils.addDependencyToSynapseLibrary(libName, jar.getAbsolutePath());
88+
89+
ClassLoader loader = SynapseConfiguration.getClassLoader(libName);
90+
assertNotNull("ClassLoader must be created on the first call", loader);
91+
assertTrue("ClassLoader must be a LibClassLoader instance",
92+
loader instanceof LibClassLoader);
93+
}
94+
95+
/**
96+
* Verifies that the first call adds the dependency JAR to the classloader URL list.
97+
*/
98+
@Test
99+
public void testFirstCallAddsJarUrlToClassLoader() throws DeploymentException, IOException {
100+
File jar = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
101+
102+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar.getAbsolutePath());
103+
104+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
105+
assertEquals("First call must add exactly one URL to the classloader", 1,
106+
classLoader.getURLs().length);
107+
}
108+
109+
// -------------------------------------------------------------------------
110+
// Deduplication — issue #4965 regression tests
111+
// -------------------------------------------------------------------------
112+
113+
/**
114+
* Core regression test for issue #4965.
115+
* <p>
116+
* Simulates two CARs each bundling {@code commons-vfs2-sandbox-2.10.0-wso2v4.jar}: the JARs
117+
* have the same filename but live under different CAR temp-extraction paths. After both
118+
* deployments the {@link LibClassLoader} must contain that filename exactly <em>once</em>.
119+
* <p>
120+
* Before the fix the second call added a duplicate URL, causing
121+
* {@code StandardFileSystemManager} to read {@code vfs-providers.xml} twice and throw
122+
* {@code "Multiple providers registered for URL scheme 'smb2'"}.
123+
*/
124+
@Test
125+
public void testDuplicateFilenameIsNotAddedToClassLoader() throws DeploymentException, IOException {
126+
// Two CARs, same dependency filename, different extraction paths
127+
File jar1 = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
128+
File jar2 = createTempFile(car2Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
129+
130+
// CAR 1 deploys — no classloader exists yet
131+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar1.getAbsolutePath());
132+
// CAR 2 deploys — classloader already exists; duplicate filename must be skipped
133+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar2.getAbsolutePath());
134+
135+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
136+
assertNotNull(classLoader);
137+
138+
long matchCount = countUrlsWithFilename(
139+
classLoader.getURLs(), "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
140+
assertEquals(
141+
"Duplicate JAR filename must appear exactly once in the classloader URLs"
142+
+ " — regression guard for issue #4965",
143+
1, matchCount);
144+
}
145+
146+
/**
147+
* Verifies that the deduplication holds across three CARs bundling the same dependency.
148+
*/
149+
@Test
150+
public void testTripleDeploymentDeduplicates() throws DeploymentException, IOException {
151+
File jar1 = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
152+
File jar2 = createTempFile(car2Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
153+
File jar3 = createTempFile(car3Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
154+
155+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar1.getAbsolutePath());
156+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar2.getAbsolutePath());
157+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar3.getAbsolutePath());
158+
159+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
160+
long matchCount = countUrlsWithFilename(
161+
classLoader.getURLs(), "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
162+
assertEquals(
163+
"Same JAR bundled in three CARs should appear exactly once in classloader URLs",
164+
1, matchCount);
165+
}
166+
167+
// -------------------------------------------------------------------------
168+
// Deduplication is filename-based: distinct JARs must still be added
169+
// -------------------------------------------------------------------------
170+
171+
/**
172+
* Verifies that a dependency with a <em>different</em> filename is correctly added as a
173+
* separate classloader entry — i.e., the dedup must not suppress genuinely distinct JARs.
174+
*/
175+
@Test
176+
public void testDifferentFilenameIsAdded() throws DeploymentException, IOException {
177+
File jar1 = createTempFile(car1Dir, "commons-vfs2-2.10.0-wso2v4.jar");
178+
File jar2 = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
179+
180+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar1.getAbsolutePath());
181+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, jar2.getAbsolutePath());
182+
183+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
184+
URL[] urls = classLoader.getURLs();
185+
assertEquals("Two distinct dependency JARs must each appear once in the classloader", 2,
186+
urls.length);
187+
}
188+
189+
/**
190+
* Full two-CAR scenario with all File Connector 6 dependencies:
191+
* verifies that both {@code commons-vfs2} and {@code commons-vfs2-sandbox} are each
192+
* deduplicated independently when two CARs are deployed.
193+
*/
194+
@Test
195+
public void testAllFileConnectorDependenciesDeduplicatedAcrossTwoCars()
196+
throws DeploymentException, IOException {
197+
// CAR 1 dependencies
198+
File vfsJar1 = createTempFile(car1Dir, "commons-vfs2-2.10.0-wso2v4.jar");
199+
File sandboxJar1 = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
200+
// CAR 2 — same filenames, different temp extraction path
201+
File vfsJar2 = createTempFile(car2Dir, "commons-vfs2-2.10.0-wso2v4.jar");
202+
File sandboxJar2 = createTempFile(car2Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
203+
204+
// CAR 1 deploys
205+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, vfsJar1.getAbsolutePath());
206+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, sandboxJar1.getAbsolutePath());
207+
// CAR 2 deploys the same deps again
208+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, vfsJar2.getAbsolutePath());
209+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, sandboxJar2.getAbsolutePath());
210+
211+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
212+
URL[] urls = classLoader.getURLs();
213+
214+
assertEquals("Total unique dependency JARs should be 2 — no duplicates allowed", 2,
215+
urls.length);
216+
assertEquals("commons-vfs2 JAR must appear exactly once",
217+
1, countUrlsWithFilename(urls, "commons-vfs2-2.10.0-wso2v4.jar"));
218+
assertEquals("commons-vfs2-sandbox JAR must appear exactly once",
219+
1, countUrlsWithFilename(urls, "commons-vfs2-sandbox-2.10.0-wso2v4.jar"));
220+
}
221+
222+
// -------------------------------------------------------------------------
223+
// Edge cases
224+
// -------------------------------------------------------------------------
225+
226+
/**
227+
* Verifies that calling {@code addDependencyToSynapseLibrary} with the exact same path
228+
* twice (not just same filename) also deduplicates correctly.
229+
*/
230+
@Test
231+
public void testExactSamePathDeduplicates() throws DeploymentException, IOException {
232+
File jar = createTempFile(car1Dir, "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
233+
String path = jar.getAbsolutePath();
234+
235+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, path);
236+
LibDeployerUtils.addDependencyToSynapseLibrary(LIBRARY_NAME, path);
237+
238+
LibClassLoader classLoader = (LibClassLoader) SynapseConfiguration.getClassLoader(LIBRARY_NAME);
239+
long matchCount = countUrlsWithFilename(
240+
classLoader.getURLs(), "commons-vfs2-sandbox-2.10.0-wso2v4.jar");
241+
assertEquals("Same path added twice must still produce only one URL", 1, matchCount);
242+
}
243+
244+
/**
245+
* Verifies that separate library names maintain independent classloaders, so dedup for
246+
* one library does not interfere with another.
247+
*/
248+
@Test
249+
public void testSeparateLibrariesHaveIndependentClassLoaders()
250+
throws DeploymentException, IOException {
251+
String libA = "{org.wso2.carbon.connector}fileA";
252+
String libB = "{org.wso2.carbon.connector}fileB";
253+
254+
File jarA = createTempFile(car1Dir, "lib-a-1.0.0.jar");
255+
File jarB = createTempFile(car2Dir, "lib-b-1.0.0.jar");
256+
257+
LibDeployerUtils.addDependencyToSynapseLibrary(libA, jarA.getAbsolutePath());
258+
LibDeployerUtils.addDependencyToSynapseLibrary(libB, jarB.getAbsolutePath());
259+
260+
assertNotSame("Each library must have its own independent classloader",
261+
SynapseConfiguration.getClassLoader(libA),
262+
SynapseConfiguration.getClassLoader(libB));
263+
assertEquals("libA classloader must contain exactly 1 URL", 1,
264+
((LibClassLoader) SynapseConfiguration.getClassLoader(libA)).getURLs().length);
265+
assertEquals("libB classloader must contain exactly 1 URL", 1,
266+
((LibClassLoader) SynapseConfiguration.getClassLoader(libB)).getURLs().length);
267+
}
268+
269+
// -------------------------------------------------------------------------
270+
// Helpers
271+
// -------------------------------------------------------------------------
272+
273+
private static File createTempFile(Path directory, String filename) throws IOException {
274+
File file = directory.resolve(filename).toFile();
275+
file.createNewFile();
276+
return file;
277+
}
278+
279+
/**
280+
* Counts how many URLs in the given array end with the specified filename.
281+
*/
282+
private static long countUrlsWithFilename(URL[] urls, String filename) {
283+
long count = 0;
284+
for (URL url : urls) {
285+
String path = url.getPath();
286+
String urlFilename = path.substring(path.lastIndexOf('/') + 1);
287+
if (filename.equals(urlFilename)) {
288+
count++;
289+
}
290+
}
291+
return count;
292+
}
293+
294+
private static void deleteQuietly(File file) {
295+
if (file == null || !file.exists()) {
296+
return;
297+
}
298+
File[] children = file.listFiles();
299+
if (children != null) {
300+
for (File child : children) {
301+
deleteQuietly(child);
302+
}
303+
}
304+
file.delete();
305+
}
306+
}

0 commit comments

Comments
 (0)