Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions WRAPPER_PLUGIN_CLASSLOADERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Wrapper Plugin Dedicated Class Loaders

Check notice on line 1 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L1

Expected: [None]; Actual: # Wrapper Plugin Dedicated Class Loaders

This feature implements dedicated class loaders for wrapper plugins to isolate them from the main application class loader.

Check notice on line 3 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L3

Expected: 80; Actual: 123

## Overview

The ArcadeDB server now loads wrapper plugins (MongoDB, Redis, PostgreSQL, and Gremlin protocol wrappers) using dedicated class loaders instead of the main application class loader. This provides better isolation and prevents potential class loading conflicts.

Check notice on line 7 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L7

Expected: 80; Actual: 260

## Implementation Details

### Wrapper Plugins Affected

Check notice on line 11 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L11

Expected: 1; Actual: 0; Below
- **MongoDB Protocol Plugin**: `com.arcadedb.mongo.MongoDBProtocolPlugin`

Check notice on line 12 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L12

Lists should be surrounded by blank lines
- **Redis Protocol Plugin**: `com.arcadedb.redis.RedisProtocolPlugin`
- **PostgreSQL Protocol Plugin**: `com.arcadedb.postgres.PostgresProtocolPlugin`
- **Gremlin Server Plugin**: `com.arcadedb.server.gremlin.GremlinServerPlugin`

### Key Components

#### WrapperPluginClassLoader

Check notice on line 19 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L19

Expected: 1; Actual: 0; Below
A specialized `URLClassLoader` that:
- Extends `URLClassLoader` for custom class loading behavior

Check notice on line 21 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L21

Lists should be surrounded by blank lines
- Maintains a registry of class loaders per plugin type
- Implements proper cleanup and resource management
- Uses singleton pattern to ensure one class loader per plugin type

#### Modified ArcadeDBServer.registerPlugins()

Check notice on line 26 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L26

Expected: 1; Actual: 0; Below
The plugin registration method now:
- Detects wrapper plugins by class name patterns

Check notice on line 28 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L28

Lists should be surrounded by blank lines
- Creates dedicated class loaders for wrapper plugins
- Loads regular plugins with the main class loader
- Logs when wrapper plugins are loaded with dedicated class loaders

#### Enhanced Server Cleanup

Check notice on line 33 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L33

Expected: 1; Actual: 0; Below
The server shutdown process now:
- Properly closes all wrapper plugin class loaders

Check notice on line 35 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L35

Lists should be surrounded by blank lines
- Prevents resource leaks during server shutdown

## Benefits

1. **Isolation**: Wrapper plugins are isolated from the main application class loader
2. **Conflict Prevention**: Reduces potential class loading conflicts between different protocols

Check notice on line 41 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L41

Expected: 80; Actual: 97
3. **Resource Management**: Proper cleanup of class loaders during shutdown
4. **Backward Compatibility**: Regular plugins continue to use the main class loader

## Usage

No configuration changes are required. The feature is automatically enabled for wrapper plugins based on their class names.

Check notice on line 47 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L47

Expected: 80; Actual: 123

When the server starts, you'll see log messages like:
```

Check notice on line 50 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L50

Fenced code blocks should be surrounded by blank lines

Check notice on line 50 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L50

Fenced code blocks should have a language specified
Loading wrapper plugin MongoDB with dedicated class loader
Loading wrapper plugin Redis with dedicated class loader
Loading wrapper plugin PostgreSQL with dedicated class loader
Loading wrapper plugin Gremlin with dedicated class loader
```

During shutdown:
```
- Closing wrapper plugin class loaders
```

## Technical Implementation

### Plugin Detection

Check notice on line 64 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L64

Expected: 1; Actual: 0; Below
```java

Check notice on line 65 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L65

Fenced code blocks should be surrounded by blank lines
public static boolean isWrapperPlugin(final String pluginClassName) {
return pluginClassName != null && (
pluginClassName.contains("mongo") ||
pluginClassName.contains("redis") ||
pluginClassName.contains("postgres") ||
pluginClassName.contains("gremlin")
);
}
```

### Class Loader Creation

Check notice on line 76 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L76

Expected: 1; Actual: 0; Below
```java
if (WrapperPluginClassLoader.isWrapperPlugin(pluginClass)) {
// Load wrapper plugins with dedicated class loader
final String wrapperName = WrapperPluginClassLoader.getWrapperPluginName(pluginClass);
final WrapperPluginClassLoader wrapperClassLoader = WrapperPluginClassLoader.getOrCreateClassLoader(
wrapperName,
new java.net.URL[0], // URLs will be resolved from classpath
Thread.currentThread().getContextClassLoader()
);
c = (Class<ServerPlugin>) Class.forName(pluginClass, true, wrapperClassLoader);
}
```

## Testing

The implementation includes comprehensive unit tests:
- `WrapperPluginClassLoaderTest`: Tests core class loader functionality

Check notice on line 93 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L93

Lists should be surrounded by blank lines
- `WrapperPluginIntegrationTest`: Tests integration with the plugin system

All wrapper plugin detection logic has been validated to correctly identify wrapper plugins and exclude non-wrapper plugins.

Check notice on line 96 in WRAPPER_PLUGIN_CLASSLOADERS.md

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

WRAPPER_PLUGIN_CLASSLOADERS.md#L96

Expected: 80; Actual: 124
23 changes: 22 additions & 1 deletion server/src/main/java/com/arcadedb/server/ArcadeDBServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,22 @@ private void registerPlugins(final ServerPlugin.INSTALLATION_PRIORITY installati
final String pluginName = pluginPair[0];
final String pluginClass = pluginPair.length > 1 ? pluginPair[1] : pluginPair[0];

final Class<ServerPlugin> c = (Class<ServerPlugin>) Class.forName(pluginClass);
final Class<ServerPlugin> c;
if (WrapperPluginClassLoader.isWrapperPlugin(pluginClass)) {
// Load wrapper plugins with dedicated class loader
final String wrapperName = WrapperPluginClassLoader.getWrapperPluginName(pluginClass);
final WrapperPluginClassLoader wrapperClassLoader = WrapperPluginClassLoader.getOrCreateClassLoader(
wrapperName,
new java.net.URL[0], // URLs will be resolved from classpath
Thread.currentThread().getContextClassLoader()
);
c = (Class<ServerPlugin>) Class.forName(pluginClass, true, wrapperClassLoader);
LogManager.instance().log(this, Level.INFO, "Loading wrapper plugin %s with dedicated class loader", pluginName);
} else {
// Load regular plugins with main class loader
c = (Class<ServerPlugin>) Class.forName(pluginClass);
}

final ServerPlugin pluginInstance = c.getConstructor().newInstance();

if (pluginInstance.getInstallationPriority() != installationPriority)
Expand Down Expand Up @@ -326,6 +341,12 @@ public synchronized void stop() {
"Error on halting '" + pEntry.getKey() + "' plugin", false);
}

// Clean up wrapper plugin class loaders
CodeUtils.executeIgnoringExceptions(() -> {
LogManager.instance().log(this, Level.INFO, "- Closing wrapper plugin class loaders");
WrapperPluginClassLoader.closeAllClassLoaders();
}, "Error on closing wrapper plugin class loaders", false);

if (haServer != null)
CodeUtils.executeIgnoringExceptions(haServer::stopService, "Error on stopping HA service", false);

Expand Down
120 changes: 120 additions & 0 deletions server/src/main/java/com/arcadedb/server/WrapperPluginClassLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
*
* Licensed 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.
*
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com)
* SPDX-License-Identifier: Apache-2.0
*/
package com.arcadedb.server;

import com.arcadedb.log.LogManager;

import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;

/**
* Dedicated class loader for wrapper plugins to isolate them from the main application class loader.
* This class loader is used for loading MongoDB, Redis, PostgreSQL, and Gremlin protocol wrappers.
*/
public class WrapperPluginClassLoader extends URLClassLoader {
private static final ConcurrentMap<String, WrapperPluginClassLoader> classLoaders = new ConcurrentHashMap<>();

private final String pluginName;

private WrapperPluginClassLoader(final String pluginName, final URL[] urls, final ClassLoader parent) {
super(urls, parent);
this.pluginName = pluginName;
LogManager.instance().log(this, Level.FINE, "Created dedicated class loader for wrapper plugin: %s", pluginName);
}

/**
* Creates or returns a dedicated class loader for a wrapper plugin.
*
* @param pluginName the name of the plugin
* @param urls the URLs from which to load classes and resources
* @param parent the parent class loader
* @return the dedicated class loader for the plugin
*/
public static synchronized WrapperPluginClassLoader getOrCreateClassLoader(
final String pluginName,
final URL[] urls,
final ClassLoader parent) {
return classLoaders.computeIfAbsent(pluginName, name -> new WrapperPluginClassLoader(name, urls, parent));
}

/**
* Checks if the given plugin class name represents a wrapper plugin that should be loaded
* with a dedicated class loader.
*
* @param pluginClassName the class name of the plugin
* @return true if this is a wrapper plugin, false otherwise
*/
public static boolean isWrapperPlugin(final String pluginClassName) {
return pluginClassName != null && (
pluginClassName.contains("mongo") ||
pluginClassName.contains("redis") ||
pluginClassName.contains("postgres") ||
pluginClassName.contains("gremlin")
);
}

/**
* Extracts the wrapper plugin name from the plugin class name.
*
* @param pluginClassName the class name of the plugin
* @return the wrapper plugin name or null if not a wrapper plugin
*/
public static String getWrapperPluginName(final String pluginClassName) {
if (pluginClassName == null) return null;

final String lowerClassName = pluginClassName.toLowerCase();
if (lowerClassName.contains("mongo")) return "MongoDB";
if (lowerClassName.contains("redis")) return "Redis";
if (lowerClassName.contains("postgres")) return "PostgreSQL";
if (lowerClassName.contains("gremlin")) return "Gremlin";

return null;
}

@Override
public void close() throws IOException {
LogManager.instance().log(this, Level.FINE, "Closing dedicated class loader for wrapper plugin: %s", pluginName);
classLoaders.remove(pluginName);
super.close();
}

/**
* Closes all wrapper plugin class loaders.
*/
public static void closeAllClassLoaders() {
for (final WrapperPluginClassLoader classLoader : classLoaders.values()) {
try {
classLoader.close();
} catch (final IOException e) {
LogManager.instance().log(WrapperPluginClassLoader.class, Level.WARNING,
"Error closing class loader for plugin: %s", e, classLoader.pluginName);
}
}
classLoaders.clear();
}

@Override
public String toString() {
return "WrapperPluginClassLoader{pluginName='" + pluginName + "'}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
*
* Licensed 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.
*
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com)
* SPDX-License-Identifier: Apache-2.0
*/
package com.arcadedb.server;

import org.junit.jupiter.api.Test;

import java.net.URL;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;

/**
* Test for the WrapperPluginClassLoader.
*/
public class WrapperPluginClassLoaderTest {

@Test
public void testIsWrapperPlugin() {
assertThat(WrapperPluginClassLoader.isWrapperPlugin("com.arcadedb.mongo.MongoDBProtocolPlugin")).isTrue();
assertThat(WrapperPluginClassLoader.isWrapperPlugin("com.arcadedb.redis.RedisProtocolPlugin")).isTrue();
assertThat(WrapperPluginClassLoader.isWrapperPlugin("com.arcadedb.postgres.PostgresProtocolPlugin")).isTrue();
assertThat(WrapperPluginClassLoader.isWrapperPlugin("com.arcadedb.server.gremlin.GremlinServerPlugin")).isTrue();

assertThat(WrapperPluginClassLoader.isWrapperPlugin("com.arcadedb.server.http.HttpServerPlugin")).isFalse();
assertThat(WrapperPluginClassLoader.isWrapperPlugin("com.example.SomeOtherPlugin")).isFalse();
assertThat(WrapperPluginClassLoader.isWrapperPlugin(null)).isFalse();
}

@Test
public void testGetWrapperPluginName() {
assertThat(WrapperPluginClassLoader.getWrapperPluginName("com.arcadedb.mongo.MongoDBProtocolPlugin")).isEqualTo("MongoDB");
assertThat(WrapperPluginClassLoader.getWrapperPluginName("com.arcadedb.redis.RedisProtocolPlugin")).isEqualTo("Redis");
assertThat(WrapperPluginClassLoader.getWrapperPluginName("com.arcadedb.postgres.PostgresProtocolPlugin")).isEqualTo("PostgreSQL");
assertThat(WrapperPluginClassLoader.getWrapperPluginName("com.arcadedb.server.gremlin.GremlinServerPlugin")).isEqualTo("Gremlin");

assertThat(WrapperPluginClassLoader.getWrapperPluginName("com.arcadedb.server.http.HttpServerPlugin")).isNull();
assertThat(WrapperPluginClassLoader.getWrapperPluginName("com.example.SomeOtherPlugin")).isNull();
assertThat(WrapperPluginClassLoader.getWrapperPluginName(null)).isNull();
}

@Test
public void testCreateClassLoader() {
final URL[] urls = new URL[0];
final ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();

final WrapperPluginClassLoader classLoader1 = WrapperPluginClassLoader.getOrCreateClassLoader("TestPlugin", urls, parentClassLoader);
assertThat(classLoader1).isNotNull();

// Should return the same instance for the same plugin name
final WrapperPluginClassLoader classLoader2 = WrapperPluginClassLoader.getOrCreateClassLoader("TestPlugin", urls, parentClassLoader);
assertThat(classLoader2).isSameAs(classLoader1);

// Should create a different instance for a different plugin name
final WrapperPluginClassLoader classLoader3 = WrapperPluginClassLoader.getOrCreateClassLoader("AnotherTestPlugin", urls, parentClassLoader);
assertThat(classLoader3).isNotSameAs(classLoader1);
}

@Test
public void testCloseAllClassLoaders() {
final URL[] urls = new URL[0];
final ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();

WrapperPluginClassLoader.getOrCreateClassLoader("TestPlugin1", urls, parentClassLoader);
WrapperPluginClassLoader.getOrCreateClassLoader("TestPlugin2", urls, parentClassLoader);

// This should not throw an exception
assertThatCode(() -> WrapperPluginClassLoader.closeAllClassLoaders()).doesNotThrowAnyException();
}
}
Loading
Loading