From 2a1414a8492b17a33908f40d31747e709393c85c Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 14:44:44 +0000 Subject: [PATCH 01/14] feat: implement plugin architecture with isolated class loaders - Add PluginClassLoader for isolated plugin class loading - Add PluginManager for ServiceLoader-based plugin discovery - Add PluginDescriptor for plugin metadata and lifecycle management - Update ArcadeDBServer to use PluginManager alongside legacy loading - Create META-INF/services files for all plugins (gremlin, postgresw, mongodbw, redisw, grpcw) - Update Maven assembly to deploy plugins to lib/plugins/ directory - Add comprehensive documentation (PLUGINS.md and package-info.java) - Add unit tests for PluginManager Each plugin now runs in its own class loader, allowing: - Different versions of dependencies per plugin - Shared server API classes for consistency - Standard Java ServiceLoader pattern for discovery - Clean plugin lifecycle management Resolves #157 Co-authored-by: Roberto Franchini --- PLUGINS.md | 299 ++++++++++++++++++ .../services/com.arcadedb.server.ServerPlugin | 1 + .../services/com.arcadedb.server.ServerPlugin | 1 + .../services/com.arcadedb.server.ServerPlugin | 1 + package/src/main/assembly/full.xml | 22 +- .../services/com.arcadedb.server.ServerPlugin | 1 + .../services/com.arcadedb.server.ServerPlugin | 1 + .../com/arcadedb/server/ArcadeDBServer.java | 31 +- .../server/plugin/PluginClassLoader.java | 74 +++++ .../server/plugin/PluginDescriptor.java | 82 +++++ .../arcadedb/server/plugin/PluginManager.java | 235 ++++++++++++++ .../arcadedb/server/plugin/package-info.java | 106 +++++++ .../server/plugin/PluginManagerTest.java | 92 ++++++ 13 files changed, 943 insertions(+), 3 deletions(-) create mode 100644 PLUGINS.md create mode 100644 gremlin/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin create mode 100644 grpcw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin create mode 100644 mongodbw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin create mode 100644 postgresw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin create mode 100644 redisw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin create mode 100644 server/src/main/java/com/arcadedb/server/plugin/PluginClassLoader.java create mode 100644 server/src/main/java/com/arcadedb/server/plugin/PluginDescriptor.java create mode 100644 server/src/main/java/com/arcadedb/server/plugin/PluginManager.java create mode 100644 server/src/main/java/com/arcadedb/server/plugin/package-info.java create mode 100644 server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java diff --git a/PLUGINS.md b/PLUGINS.md new file mode 100644 index 0000000000..d1cd3fdf4f --- /dev/null +++ b/PLUGINS.md @@ -0,0 +1,299 @@ +# ArcadeDB Plugin Architecture + +## Overview + +ArcadeDB supports a plugin architecture that allows extending the server functionality through isolated plugins. Each plugin runs in its own class loader, enabling plugins to have different versions of dependencies without conflicts. + +## Plugin Types + +ArcadeDB includes the following built-in plugins: + +- **Gremlin** - Apache TinkerPop Gremlin graph traversal language support +- **PostgreSQL Wire Protocol** - PostgreSQL protocol compatibility +- **MongoDB Wire Protocol** - MongoDB query language compatibility +- **Redis Wire Protocol** - Redis command compatibility +- **gRPC** - gRPC protocol support + +## Architecture + +### Class Loading + +The plugin system uses isolated class loaders with the following strategy: + +1. **Server API Classes** (`com.arcadedb.*`) - Loaded from parent class loader (shared across all plugins) +2. **Plugin Classes** - Loaded from plugin's own JAR first (isolated) +3. **Other Classes** - Fall back to parent class loader if not found in plugin JAR + +This approach ensures: +- Plugins can use different versions of third-party libraries +- Server APIs are shared for consistency and communication +- Memory efficiency through shared core classes + +### Components + +#### PluginManager +- Discovers plugins from `lib/plugins/` directory +- Manages plugin lifecycle (start, stop) +- Coordinates plugin loading with server initialization + +#### PluginClassLoader +- Custom class loader that isolates plugin dependencies +- Parent-first delegation for server API classes +- Child-first delegation for plugin-specific classes + +#### PluginDescriptor +- Metadata container for each plugin +- Tracks plugin state and lifecycle +- Associates plugin with its class loader + +## Plugin Lifecycle + +1. **Discovery** - Scan `lib/plugins/` directory for JAR files +2. **Class Loading** - Create isolated class loader for each plugin JAR +3. **Service Discovery** - Use Java ServiceLoader to find `ServerPlugin` implementations +4. **Configuration** - Call `configure()` with server instance and configuration +5. **Starting** - Call `startService()` based on installation priority +6. **Running** - Plugin provides functionality +7. **Stopping** - Call `stopService()` in reverse order +8. **Cleanup** - Close class loaders and release resources + +### Installation Priorities + +Plugins are started in phases based on their installation priority: + +1. `BEFORE_HTTP_ON` - Before HTTP server starts (default) +2. `AFTER_HTTP_ON` - After HTTP server starts +3. `AFTER_DATABASES_OPEN` - After databases are loaded + +## Creating a Plugin + +### 1. Implement ServerPlugin Interface + +```java +package com.example.myplugin; + +import com.arcadedb.ContextConfiguration; +import com.arcadedb.server.ArcadeDBServer; +import com.arcadedb.server.ServerPlugin; + +public class MyPlugin implements ServerPlugin { + private ArcadeDBServer server; + private ContextConfiguration configuration; + + @Override + public void configure(ArcadeDBServer arcadeDBServer, ContextConfiguration configuration) { + this.server = arcadeDBServer; + this.configuration = configuration; + // Initialize your plugin configuration + } + + @Override + public void startService() { + // Start your plugin services + System.out.println("MyPlugin started!"); + } + + @Override + public void stopService() { + // Stop your plugin services and clean up resources + System.out.println("MyPlugin stopped!"); + } + + @Override + public INSTALLATION_PRIORITY getInstallationPriority() { + return INSTALLATION_PRIORITY.AFTER_HTTP_ON; + } +} +``` + +### 2. Create Service Provider Configuration + +Create file: `src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin` + +Content: +``` +com.example.myplugin.MyPlugin +``` + +### 3. Build Plugin JAR + +```bash +mvn clean package +``` + +### 4. Deploy Plugin + +Copy the plugin JAR to the `lib/plugins/` directory in your ArcadeDB installation: + +```bash +cp target/myplugin-1.0.0.jar $ARCADEDB_HOME/lib/plugins/ +``` + +### 5. Start ArcadeDB + +The plugin will be automatically discovered and loaded when ArcadeDB starts: + +```bash +cd $ARCADEDB_HOME +bin/server.sh +``` + +Check the logs for: +``` +[INFO] Discovered plugin: myplugin from myplugin-1.0.0.jar +[INFO] - myplugin plugin started +``` + +## Plugin Dependencies + +### Server API Dependencies + +Plugin POMs should include server dependencies with `provided` scope: + +```xml + + com.arcadedb + arcadedb-server + ${arcadedb.version} + provided + +``` + +### Plugin-Specific Dependencies + +Plugin-specific dependencies use normal `compile` scope and will be packaged with the plugin: + +```xml + + com.example + my-library + 1.0.0 + compile + +``` + +## Building Distributions with Plugins + +### Maven Assembly + +The Maven assembly descriptor automatically places plugin JARs in `lib/plugins/`: + +```xml + + lib/plugins + + com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-postgresw + com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw + + false + +``` + +## Advanced Topics + +### Accessing Server Resources + +Plugins have full access to the ArcadeDB server instance: + +```java +@Override +public void configure(ArcadeDBServer arcadeDBServer, ContextConfiguration configuration) { + this.server = arcadeDBServer; + + // Access databases + ServerDatabase db = server.getDatabase("mydb"); + + // Access HTTP server for custom endpoints + HttpServer httpServer = server.getHttpServer(); + + // Access security + ServerSecurity security = server.getSecurity(); +} +``` + +### Thread Context Class Loader + +The PluginManager automatically sets the thread context class loader during plugin operations: + +- During `configure()` - Set to plugin's class loader +- During `startService()` - Set to plugin's class loader +- During `stopService()` - Set to plugin's class loader + +This ensures proper class loading for frameworks that use the thread context class loader. + +### HTTP Endpoint Registration + +Plugins can register custom HTTP endpoints: + +```java +@Override +public void registerAPI(HttpServer httpServer, PathHandler routes) { + routes.addExactPath("/api/myplugin", exchange -> { + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/json"); + exchange.getResponseSender().send("{\"status\":\"ok\"}"); + }); +} +``` + +## Troubleshooting + +### Plugin Not Discovered + +Check that: +1. Plugin JAR is in `lib/plugins/` directory +2. `META-INF/services/com.arcadedb.server.ServerPlugin` file exists +3. Service file contains correct plugin class name +4. Plugin class implements `ServerPlugin` interface + +### ClassNotFoundException + +If you see `ClassNotFoundException` for server classes: +- Ensure server dependencies use `provided` scope +- Check that class is in `com.arcadedb.*` package + +If you see `ClassNotFoundException` for plugin classes: +- Ensure dependency is included with `compile` scope +- Check that JAR contains the required class + +### Plugin Conflicts + +If two plugins have conflicting dependencies: +- This is the main benefit of isolated class loaders +- Each plugin can use its own version +- Ensure server API classes match across all plugins + +## Migration from Legacy Plugin Loading + +The new plugin system is backward compatible with the legacy configuration-based loading. Both systems can coexist: + +### Legacy Method (still supported) +```properties +arcadedb.server.plugins=gremlin:com.arcadedb.server.gremlin.GremlinServerPlugin +``` + +### New Method (recommended) +1. Place plugin JAR in `lib/plugins/` +2. Include `META-INF/services` file +3. No configuration needed + +## Best Practices + +1. **Use Provided Scope** - Server dependencies should always use `provided` scope +2. **Clean Shutdown** - Implement proper cleanup in `stopService()` +3. **Thread Safety** - Make plugin implementations thread-safe +4. **Logging** - Use `LogManager.instance().log()` for consistent logging +5. **Error Handling** - Handle exceptions gracefully, don't crash the server +6. **Resource Management** - Close all resources in `stopService()` +7. **Configuration** - Use `ContextConfiguration` for plugin settings + +## Examples + +See the built-in plugins for complete examples: +- `gremlin/` - Complex plugin with custom graph manager +- `postgresw/` - Network protocol plugin +- `mongodbw/` - Query language compatibility plugin +- `redisw/` - Simple protocol plugin +- `grpcw/` - gRPC service plugin diff --git a/gremlin/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/gremlin/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..e825fbc860 --- /dev/null +++ b/gremlin/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.server.gremlin.GremlinServerPlugin diff --git a/grpcw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/grpcw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..eae577bba6 --- /dev/null +++ b/grpcw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.server.grpc.GrpcServerPlugin diff --git a/mongodbw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/mongodbw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..edb7f7cc6a --- /dev/null +++ b/mongodbw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.mongo.MongoDBProtocolPlugin diff --git a/package/src/main/assembly/full.xml b/package/src/main/assembly/full.xml index ba3b73a778..7d41f5c5e3 100644 --- a/package/src/main/assembly/full.xml +++ b/package/src/main/assembly/full.xml @@ -94,14 +94,34 @@ + lib *:jar:* + + com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-postgresw + com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw + + + + + lib/plugins + + com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-postgresw + com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw + + false diff --git a/postgresw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/postgresw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..46abd8bbcb --- /dev/null +++ b/postgresw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.postgres.PostgresProtocolPlugin diff --git a/redisw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/redisw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..62063d53a7 --- /dev/null +++ b/redisw/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.redis.RedisProtocolPlugin diff --git a/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java b/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java index 258551e120..936a07f2e8 100644 --- a/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java +++ b/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java @@ -43,6 +43,7 @@ import com.arcadedb.server.security.ServerSecurity; import com.arcadedb.server.security.ServerSecurityException; import com.arcadedb.server.security.ServerSecurityUser; +import com.arcadedb.server.plugin.PluginManager; import com.arcadedb.utility.CodeUtils; import com.arcadedb.utility.FileUtils; import com.arcadedb.utility.ServerPathUtils; @@ -79,6 +80,7 @@ public enum STATUS {OFFLINE, STARTING, ONLINE, SHUTTING_DOWN} private FileServerEventLog eventLog; private final Map plugins = new LinkedHashMap<>(); + private PluginManager pluginManager; private String serverRootPath; private HAServer haServer; private ServerSecurity security; @@ -139,8 +141,11 @@ public synchronized void start() { throw new ServerException("Error on starting the server '" + serverName + "'"); } + // Discover plugins from lib/plugins directory + pluginManager.discoverPlugins(); + LogManager.instance().log(this, Level.INFO, "Starting ArcadeDB Server in %s mode with plugins %s ...", - GlobalConfiguration.SERVER_MODE.getValueAsString(), getPluginNames()); + GlobalConfiguration.SERVER_MODE.getValueAsString(), getAllPluginNames()); // START METRICS & CONNECTED JMX REPORTER if (configuration.getValueAsBoolean(GlobalConfiguration.SERVER_METRICS)) { @@ -172,6 +177,7 @@ public synchronized void start() { httpServer = new HttpServer(this); registerPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON); + pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON); httpServer.startService(); @@ -181,6 +187,7 @@ public synchronized void start() { } registerPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_HTTP_ON); + pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_HTTP_ON); loadDefaultDatabases(); @@ -188,6 +195,7 @@ public synchronized void start() { loadDatabases(); registerPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_DATABASES_OPEN); + pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_DATABASES_OPEN); status = STATUS.ONLINE; @@ -276,6 +284,16 @@ private Set getPluginNames() { return result; } + private Set getAllPluginNames() { + final Set result = new LinkedHashSet<>(); + // Add legacy plugins + result.addAll(getPluginNames()); + // Add PluginManager plugins + if (pluginManager != null) + result.addAll(pluginManager.getPluginNames()); + return result; + } + private void registerPlugins(final ServerPlugin.INSTALLATION_PRIORITY installationPriority) { final String registeredPlugins = configuration.getValueAsString(GlobalConfiguration.SERVER_PLUGINS); if (registeredPlugins != null && !registeredPlugins.isEmpty()) { @@ -354,6 +372,11 @@ public synchronized void stop() { status = STATUS.SHUTTING_DOWN; + // Stop plugins managed by PluginManager first + if (pluginManager != null) + pluginManager.stopPlugins(); + + // Stop legacy plugins for (final Map.Entry pEntry : plugins.entrySet()) { LogManager.instance().log(this, Level.INFO, "- Stop %s plugin", pEntry.getKey()); CodeUtils.executeIgnoringExceptions(() -> pEntry.getValue().stopService(), @@ -395,7 +418,10 @@ public synchronized void stop() { } public Collection getPlugins() { - return Collections.unmodifiableCollection(plugins.values()); + final List allPlugins = new ArrayList<>(plugins.values()); + if (pluginManager != null) + allPlugins.addAll(pluginManager.getPlugins()); + return Collections.unmodifiableCollection(allPlugins); } public ServerDatabase getDatabase(final String databaseName) { @@ -745,6 +771,7 @@ private void loadConfiguration() { private void init() { eventLog = new FileServerEventLog(this); + pluginManager = new PluginManager(this, configuration); Runtime.getRuntime().addShutdownHook(new Thread(() -> { // Mark logger as shutting down to prevent NPE when handlers are closed (issue #2813) diff --git a/server/src/main/java/com/arcadedb/server/plugin/PluginClassLoader.java b/server/src/main/java/com/arcadedb/server/plugin/PluginClassLoader.java new file mode 100644 index 0000000000..f218557469 --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/plugin/PluginClassLoader.java @@ -0,0 +1,74 @@ +/* + * 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.plugin; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; + +/** + * Custom class loader for plugins that provides isolation while allowing access to server APIs. + *

+ * This class loader follows a parent-first delegation model for server classes (com.arcadedb.server.*) + * and a child-first model for plugin-specific classes, allowing each plugin to have its own + * version of dependencies. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + */ +public class PluginClassLoader extends URLClassLoader { + private static final String SERVER_PACKAGE_PREFIX = "com.arcadedb."; + + public PluginClassLoader(final String pluginName, final File pluginJarFile, final ClassLoader parent) + throws MalformedURLException { + super(new URL[]{pluginJarFile.toURI().toURL()}, parent); + } + + @Override + protected Class loadClass(final String name, final boolean resolve) throws ClassNotFoundException { + // Always delegate server API classes to parent to ensure shared instances + if (name.startsWith(SERVER_PACKAGE_PREFIX)) { + return super.loadClass(name, resolve); + } + + // For plugin classes, try to load from this class loader first + synchronized (getClassLoadingLock(name)) { + // Check if the class has already been loaded by this class loader + Class c = findLoadedClass(name); + if (c == null) { + try { + // Try to load from this class loader's JAR first + c = findClass(name); + } catch (final ClassNotFoundException e) { + // If not found, delegate to parent + c = super.loadClass(name, resolve); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } + } + + @Override + public String toString() { + return "PluginClassLoader{urls=" + java.util.Arrays.toString(getURLs()) + "}"; + } +} diff --git a/server/src/main/java/com/arcadedb/server/plugin/PluginDescriptor.java b/server/src/main/java/com/arcadedb/server/plugin/PluginDescriptor.java new file mode 100644 index 0000000000..4f051c45ae --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/plugin/PluginDescriptor.java @@ -0,0 +1,82 @@ +/* + * 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.plugin; + +import com.arcadedb.server.ServerPlugin; + +import java.io.File; +import java.util.Objects; + +/** + * Descriptor for a plugin that provides metadata and lifecycle management. + * Each plugin is loaded in its own class loader for isolation. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + */ +public class PluginDescriptor { + private final String pluginName; + private final File pluginJarFile; + private final ClassLoader classLoader; + private ServerPlugin pluginInstance; + private boolean started; + + public PluginDescriptor(final String pluginName, final File pluginJarFile, final ClassLoader classLoader) { + this.pluginName = Objects.requireNonNull(pluginName, "Plugin name cannot be null"); + this.pluginJarFile = Objects.requireNonNull(pluginJarFile, "Plugin JAR file cannot be null"); + this.classLoader = Objects.requireNonNull(classLoader, "Class loader cannot be null"); + this.started = false; + } + + public String getPluginName() { + return pluginName; + } + + public File getPluginJarFile() { + return pluginJarFile; + } + + public ClassLoader getClassLoader() { + return classLoader; + } + + public ServerPlugin getPluginInstance() { + return pluginInstance; + } + + public void setPluginInstance(final ServerPlugin pluginInstance) { + this.pluginInstance = pluginInstance; + } + + public boolean isStarted() { + return started; + } + + public void setStarted(final boolean started) { + this.started = started; + } + + @Override + public String toString() { + return "PluginDescriptor{" + + "pluginName='" + pluginName + '\'' + + ", pluginJarFile=" + pluginJarFile + + ", started=" + started + + '}'; + } +} diff --git a/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java b/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java new file mode 100644 index 0000000000..d10baf8508 --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java @@ -0,0 +1,235 @@ +/* + * 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.plugin; + +import com.arcadedb.ContextConfiguration; +import com.arcadedb.log.LogManager; +import com.arcadedb.server.ArcadeDBServer; +import com.arcadedb.server.ServerException; +import com.arcadedb.server.ServerPlugin; +import com.arcadedb.utility.CodeUtils; + +import java.io.File; +import java.io.IOException; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; + +/** + * Manager for loading and managing plugins using isolated class loaders. + * Plugins are discovered from the lib/plugins directory using the ServiceLoader pattern. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + */ +public class PluginManager { + private final ArcadeDBServer server; + private final ContextConfiguration configuration; + private final String pluginsDirectory; + private final Map plugins = new LinkedHashMap<>(); + private final Map classLoaderMap = new ConcurrentHashMap<>(); + + public PluginManager(final ArcadeDBServer server, final ContextConfiguration configuration) { + this.server = server; + this.configuration = configuration; + this.pluginsDirectory = server.getRootPath() + File.separator + "lib" + File.separator + "plugins"; + } + + /** + * Discover and load plugins from the plugins directory. + * Each plugin JAR is loaded in its own isolated class loader. + */ + public void discoverPlugins() { + final File pluginsDir = new File(pluginsDirectory); + if (!pluginsDir.exists() || !pluginsDir.isDirectory()) { + LogManager.instance().log(this, Level.INFO, "Plugins directory not found: %s", pluginsDirectory); + return; + } + + final File[] pluginJars = pluginsDir.listFiles((dir, name) -> name.endsWith(".jar")); + if (pluginJars == null || pluginJars.length == 0) { + LogManager.instance().log(this, Level.INFO, "No plugin JARs found in: %s", pluginsDirectory); + return; + } + + for (final File pluginJar : pluginJars) { + try { + loadPlugin(pluginJar); + } catch (final Exception e) { + LogManager.instance().log(this, Level.SEVERE, "Failed to load plugin from: %s", e, pluginJar.getAbsolutePath()); + } + } + } + + /** + * Load a plugin from a JAR file using an isolated class loader. + */ + private void loadPlugin(final File pluginJar) throws Exception { + final String jarName = pluginJar.getName(); + final String pluginName = jarName.substring(0, jarName.lastIndexOf('.')); + + LogManager.instance().log(this, Level.FINE, "Loading plugin from: %s", pluginJar.getAbsolutePath()); + + // Create isolated class loader for this plugin + final PluginClassLoader classLoader = new PluginClassLoader(pluginName, pluginJar, getClass().getClassLoader()); + + // Create plugin descriptor + final PluginDescriptor descriptor = new PluginDescriptor(pluginName, pluginJar, classLoader); + + // Use ServiceLoader to discover plugin implementations + final ServiceLoader serviceLoader = ServiceLoader.load(ServerPlugin.class, classLoader); + final Iterator iterator = serviceLoader.iterator(); + + if (!iterator.hasNext()) { + LogManager.instance().log(this, Level.WARNING, + "No ServerPlugin implementation found in: %s (missing META-INF/services entry?)", pluginJar.getAbsolutePath()); + return; + } + + // Load the first plugin implementation (typically only one per JAR) + final ServerPlugin pluginInstance = iterator.next(); + descriptor.setPluginInstance(pluginInstance); + + // Register the plugin + plugins.put(pluginName, descriptor); + classLoaderMap.put(classLoader, descriptor); + + LogManager.instance().log(this, Level.INFO, "Discovered plugin: %s from %s", pluginName, pluginJar.getName()); + } + + /** + * Start plugins based on their installation priority. + */ + public void startPlugins(final ServerPlugin.INSTALLATION_PRIORITY priority) { + for (final Map.Entry entry : plugins.entrySet()) { + final String pluginName = entry.getKey(); + final PluginDescriptor descriptor = entry.getValue(); + final ServerPlugin plugin = descriptor.getPluginInstance(); + + if (plugin == null || descriptor.isStarted()) { + continue; + } + + if (plugin.getInstallationPriority() != priority) { + continue; + } + + try { + // Set the context class loader to the plugin's class loader + final Thread currentThread = Thread.currentThread(); + final ClassLoader originalClassLoader = currentThread.getContextClassLoader(); + try { + currentThread.setContextClassLoader(descriptor.getClassLoader()); + + // Configure and start the plugin + plugin.configure(server, configuration); + plugin.startService(); + + descriptor.setStarted(true); + LogManager.instance().log(this, Level.INFO, "- %s plugin started", pluginName); + + } finally { + currentThread.setContextClassLoader(originalClassLoader); + } + } catch (final Exception e) { + throw new ServerException("Error starting plugin: " + pluginName, e); + } + } + } + + /** + * Stop all plugins in reverse order of registration. + */ + public void stopPlugins() { + final List> pluginList = new ArrayList<>(plugins.entrySet()); + Collections.reverse(pluginList); + + for (final Map.Entry entry : pluginList) { + final String pluginName = entry.getKey(); + final PluginDescriptor descriptor = entry.getValue(); + final ServerPlugin plugin = descriptor.getPluginInstance(); + + if (plugin == null || !descriptor.isStarted()) { + continue; + } + + LogManager.instance().log(this, Level.INFO, "- Stop %s plugin", pluginName); + + final Thread currentThread = Thread.currentThread(); + final ClassLoader originalClassLoader = currentThread.getContextClassLoader(); + try { + currentThread.setContextClassLoader(descriptor.getClassLoader()); + CodeUtils.executeIgnoringExceptions(plugin::stopService, + "Error stopping plugin: " + pluginName, false); + descriptor.setStarted(false); + } finally { + currentThread.setContextClassLoader(originalClassLoader); + } + } + + // Close class loaders + for (final PluginDescriptor descriptor : plugins.values()) { + final ClassLoader classLoader = descriptor.getClassLoader(); + if (classLoader instanceof PluginClassLoader) { + try { + ((PluginClassLoader) classLoader).close(); + } catch (final IOException e) { + LogManager.instance().log(this, Level.WARNING, "Error closing class loader for plugin: %s", + e, descriptor.getPluginName()); + } + } + } + + plugins.clear(); + classLoaderMap.clear(); + } + + /** + * Get all loaded plugins. + */ + public Collection getPlugins() { + final List result = new ArrayList<>(); + for (final PluginDescriptor descriptor : plugins.values()) { + if (descriptor.getPluginInstance() != null) { + result.add(descriptor.getPluginInstance()); + } + } + return Collections.unmodifiableCollection(result); + } + + /** + * Get the number of loaded plugins. + */ + public int getPluginCount() { + return plugins.size(); + } + + /** + * Get plugin names. + */ + public Set getPluginNames() { + return Collections.unmodifiableSet(plugins.keySet()); + } + + /** + * Get plugin descriptor by name. + */ + public PluginDescriptor getPluginDescriptor(final String pluginName) { + return plugins.get(pluginName); + } +} diff --git a/server/src/main/java/com/arcadedb/server/plugin/package-info.java b/server/src/main/java/com/arcadedb/server/plugin/package-info.java new file mode 100644 index 0000000000..2611a801ab --- /dev/null +++ b/server/src/main/java/com/arcadedb/server/plugin/package-info.java @@ -0,0 +1,106 @@ +/* + * 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 + */ + +/** + * Plugin architecture for ArcadeDB with isolated class loaders. + *

+ * This package provides infrastructure for loading and managing plugins in isolated class loaders, + * allowing each plugin to have its own set of dependencies without conflicts. + * + *

Architecture

+ *

+ * The plugin system consists of three main components: + *

    + *
  • {@link com.arcadedb.server.plugin.PluginManager} - Discovers and manages plugin lifecycle
  • + *
  • {@link com.arcadedb.server.plugin.PluginClassLoader} - Provides isolated class loading
  • + *
  • {@link com.arcadedb.server.plugin.PluginDescriptor} - Holds plugin metadata and state
  • + *
+ * + *

Plugin Discovery

+ *

+ * Plugins are discovered using the Java ServiceLoader pattern. Each plugin JAR must: + *

    + *
  1. Implement {@link com.arcadedb.server.ServerPlugin} interface
  2. + *
  3. Provide a META-INF/services/com.arcadedb.server.ServerPlugin file with the implementation class name
  4. + *
  5. Be placed in the {@code lib/plugins/} directory
  6. + *
+ * + *

Class Loading Strategy

+ *

+ * The {@link com.arcadedb.server.plugin.PluginClassLoader} uses a hybrid delegation model: + *

    + *
  • Server API classes (com.arcadedb.*) are loaded from the parent class loader (shared)
  • + *
  • Plugin-specific classes are loaded from the plugin's JAR first (isolated)
  • + *
  • Other classes fall back to parent class loader if not found in plugin JAR
  • + *
+ * + *

Plugin Lifecycle

+ *

+ * Plugins follow this lifecycle: + *

    + *
  1. Discovery - PluginManager scans lib/plugins/ directory
  2. + *
  3. Loading - Each plugin JAR gets its own PluginClassLoader
  4. + *
  5. Instantiation - ServiceLoader creates plugin instances
  6. + *
  7. Configuration - {@link com.arcadedb.server.ServerPlugin#configure} is called
  8. + *
  9. Starting - {@link com.arcadedb.server.ServerPlugin#startService} is called
  10. + *
  11. Running - Plugin provides its functionality
  12. + *
  13. Stopping - {@link com.arcadedb.server.ServerPlugin#stopService} is called
  14. + *
  15. Cleanup - ClassLoaders are closed
  16. + *
+ * + *

Creating a Plugin

+ *

+ * To create a new plugin: + *

{@code
+ * public class MyPlugin implements ServerPlugin {
+ *   @Override
+ *   public void configure(ArcadeDBServer server, ContextConfiguration config) {
+ *     // Initialize configuration
+ *   }
+ *
+ *   @Override
+ *   public void startService() {
+ *     // Start plugin services
+ *   }
+ *
+ *   @Override
+ *   public void stopService() {
+ *     // Stop plugin services
+ *   }
+ * }
+ * }
+ * + *

+ * Create {@code src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin}: + *

+ * com.example.MyPlugin
+ * 
+ * + *

Thread Safety

+ *

+ * The plugin system manages the thread context class loader during plugin operations to ensure + * proper class loading context. Plugin implementations should be thread-safe if they handle + * concurrent requests. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + * @see com.arcadedb.server.ServerPlugin + * @see com.arcadedb.server.plugin.PluginManager + * @see com.arcadedb.server.plugin.PluginClassLoader + */ +package com.arcadedb.server.plugin; diff --git a/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java b/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java new file mode 100644 index 0000000000..8145384041 --- /dev/null +++ b/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java @@ -0,0 +1,92 @@ +/* + * 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.plugin; + +import com.arcadedb.ContextConfiguration; +import com.arcadedb.GlobalConfiguration; +import com.arcadedb.server.ArcadeDBServer; +import com.arcadedb.server.ServerPlugin; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.util.Collection; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test for PluginManager to verify plugin discovery and loading with isolated class loaders. + * + * @author Luca Garulli (l.garulli@arcadedata.com) + */ +public class PluginManagerTest { + private ArcadeDBServer server; + private PluginManager pluginManager; + + @BeforeEach + public void setup() { + final ContextConfiguration configuration = new ContextConfiguration(); + configuration.setValue(GlobalConfiguration.SERVER_ROOT_PATH, "./target/test-server"); + configuration.setValue(GlobalConfiguration.SERVER_DATABASE_DIRECTORY, "./target/test-server/databases"); + + server = new ArcadeDBServer(configuration); + pluginManager = new PluginManager(server, configuration); + } + + @AfterEach + public void teardown() { + if (server != null && server.isStarted()) { + server.stop(); + } + } + + @Test + public void testPluginManagerCreation() { + assertNotNull(pluginManager); + assertEquals(0, pluginManager.getPluginCount()); + } + + @Test + public void testDiscoverPluginsWithNoDirectory() { + // Should handle missing plugins directory gracefully + pluginManager.discoverPlugins(); + assertEquals(0, pluginManager.getPluginCount()); + } + + @Test + public void testGetPluginNames() { + final Collection names = pluginManager.getPluginNames(); + assertNotNull(names); + assertTrue(names.isEmpty()); + } + + @Test + public void testGetPlugins() { + final Collection plugins = pluginManager.getPlugins(); + assertNotNull(plugins); + assertTrue(plugins.isEmpty()); + } + + @Test + public void testStopPluginsWhenEmpty() { + // Should handle stopping with no plugins loaded + assertDoesNotThrow(() -> pluginManager.stopPlugins()); + } +} From 566cad9b320c8ea4ee556e171bda338498c9dbb8 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:30:01 +0000 Subject: [PATCH 02/14] test: expand PluginManagerTest with comprehensive coverage - Add test plugin implementations for various scenarios - Add tests for successful plugin loading and lifecycle - Add tests for error scenarios (missing META-INF/services, startup failures) - Add tests for class loader isolation verification - Add tests for plugin start/stop ordering by priority - Add tests for multiple plugin loading - Use @TempDir for proper test isolation Coverage now includes: - Plugin discovery with empty/missing directories - Plugin loading with proper META-INF/services - Full lifecycle (configure -> start -> stop) - Priority-based initialization (BEFORE_HTTP_ON, AFTER_HTTP_ON) - Reverse-order shutdown - Class loader isolation between plugins - Error handling for invalid plugins Co-authored-by: Roberto Franchini --- .../server/plugin/PluginManagerTest.java | 360 +++++++++++++++++- 1 file changed, 357 insertions(+), 3 deletions(-) diff --git a/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java b/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java index 8145384041..4b79bc90ab 100644 --- a/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java +++ b/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java @@ -21,13 +21,24 @@ import com.arcadedb.ContextConfiguration; import com.arcadedb.GlobalConfiguration; import com.arcadedb.server.ArcadeDBServer; +import com.arcadedb.server.ServerException; import com.arcadedb.server.ServerPlugin; +import com.arcadedb.server.http.HttpServer; +import io.undertow.server.handlers.PathHandler; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; -import java.io.File; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Collection; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; import static org.junit.jupiter.api.Assertions.*; @@ -40,11 +51,14 @@ public class PluginManagerTest { private ArcadeDBServer server; private PluginManager pluginManager; + @TempDir + Path tempDir; + @BeforeEach public void setup() { final ContextConfiguration configuration = new ContextConfiguration(); - configuration.setValue(GlobalConfiguration.SERVER_ROOT_PATH, "./target/test-server"); - configuration.setValue(GlobalConfiguration.SERVER_DATABASE_DIRECTORY, "./target/test-server/databases"); + configuration.setValue(GlobalConfiguration.SERVER_ROOT_PATH, tempDir.toString()); + configuration.setValue(GlobalConfiguration.SERVER_DATABASE_DIRECTORY, tempDir.resolve("databases").toString()); server = new ArcadeDBServer(configuration); pluginManager = new PluginManager(server, configuration); @@ -52,6 +66,9 @@ public void setup() { @AfterEach public void teardown() { + if (pluginManager != null) { + pluginManager.stopPlugins(); + } if (server != null && server.isStarted()) { server.stop(); } @@ -89,4 +106,341 @@ public void testStopPluginsWhenEmpty() { // Should handle stopping with no plugins loaded assertDoesNotThrow(() -> pluginManager.stopPlugins()); } + + @Test + public void testDiscoverPluginsWithEmptyDirectory() throws IOException { + // Create empty plugins directory + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + pluginManager.discoverPlugins(); + assertEquals(0, pluginManager.getPluginCount()); + } + + @Test + public void testLoadPluginWithMetaInfServices() throws Exception { + // Create a test plugin JAR with proper META-INF/services + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + final File pluginJar = createTestPluginJar(pluginsDir, "test-plugin", TestPlugin1.class); + + pluginManager.discoverPlugins(); + + assertEquals(1, pluginManager.getPluginCount()); + assertTrue(pluginManager.getPluginNames().contains("test-plugin")); + + final Collection plugins = pluginManager.getPlugins(); + assertEquals(1, plugins.size()); + } + + @Test + public void testLoadMultiplePlugins() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "plugin1", TestPlugin1.class); + createTestPluginJar(pluginsDir, "plugin2", TestPlugin2.class); + + pluginManager.discoverPlugins(); + + assertEquals(2, pluginManager.getPluginCount()); + final Set names = pluginManager.getPluginNames(); + assertTrue(names.contains("plugin1")); + assertTrue(names.contains("plugin2")); + } + + @Test + public void testPluginLifecycle() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "lifecycle-plugin", LifecycleTestPlugin.class); + + pluginManager.discoverPlugins(); + assertEquals(1, pluginManager.getPluginCount()); + + // Start the plugin + pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON); + + // Verify plugin was configured and started + final PluginDescriptor descriptor = pluginManager.getPluginDescriptor("lifecycle-plugin"); + assertNotNull(descriptor); + assertTrue(descriptor.isStarted()); + assertTrue(descriptor.getPluginInstance() instanceof LifecycleTestPlugin); + + final LifecycleTestPlugin plugin = (LifecycleTestPlugin) descriptor.getPluginInstance(); + assertTrue(plugin.configured.get()); + assertTrue(plugin.started.get()); + assertFalse(plugin.stopped.get()); + + // Stop the plugin + pluginManager.stopPlugins(); + assertTrue(plugin.stopped.get()); + assertFalse(descriptor.isStarted()); + } + + @Test + public void testPluginStartOrderByPriority() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "before-plugin", BeforeHttpPlugin.class); + createTestPluginJar(pluginsDir, "after-plugin", AfterHttpPlugin.class); + + pluginManager.discoverPlugins(); + assertEquals(2, pluginManager.getPluginCount()); + + // Start BEFORE_HTTP_ON plugins + pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON); + + PluginDescriptor beforeDesc = pluginManager.getPluginDescriptor("before-plugin"); + PluginDescriptor afterDesc = pluginManager.getPluginDescriptor("after-plugin"); + + assertTrue(beforeDesc.isStarted()); + assertFalse(afterDesc.isStarted()); + + // Start AFTER_HTTP_ON plugins + pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_HTTP_ON); + + assertTrue(beforeDesc.isStarted()); + assertTrue(afterDesc.isStarted()); + } + + @Test + public void testPluginWithoutMetaInfServices() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + // Create JAR without META-INF/services + final File pluginJar = pluginsDir.resolve("invalid-plugin.jar").toFile(); + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(pluginJar))) { + // Just create an empty JAR + jos.putNextEntry(new JarEntry("dummy.txt")); + jos.write("test".getBytes()); + jos.closeEntry(); + } + + pluginManager.discoverPlugins(); + + // Plugin should not be loaded due to missing META-INF/services + assertEquals(0, pluginManager.getPluginCount()); + } + + @Test + public void testPluginStartException() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "failing-plugin", FailingPlugin.class); + + pluginManager.discoverPlugins(); + assertEquals(1, pluginManager.getPluginCount()); + + // Starting the plugin should throw exception + assertThrows(ServerException.class, () -> + pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON)); + } + + @Test + public void testGetPluginDescriptor() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "test-plugin", TestPlugin1.class); + + pluginManager.discoverPlugins(); + + final PluginDescriptor descriptor = pluginManager.getPluginDescriptor("test-plugin"); + assertNotNull(descriptor); + assertEquals("test-plugin", descriptor.getPluginName()); + assertNotNull(descriptor.getPluginJarFile()); + assertNotNull(descriptor.getClassLoader()); + assertNotNull(descriptor.getPluginInstance()); + assertFalse(descriptor.isStarted()); + } + + @Test + public void testClassLoaderIsolation() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "plugin1", TestPlugin1.class); + createTestPluginJar(pluginsDir, "plugin2", TestPlugin2.class); + + pluginManager.discoverPlugins(); + + final PluginDescriptor desc1 = pluginManager.getPluginDescriptor("plugin1"); + final PluginDescriptor desc2 = pluginManager.getPluginDescriptor("plugin2"); + + // Each plugin should have its own class loader + assertNotNull(desc1.getClassLoader()); + assertNotNull(desc2.getClassLoader()); + assertNotSame(desc1.getClassLoader(), desc2.getClassLoader()); + + // Both should be PluginClassLoader instances + assertTrue(desc1.getClassLoader() instanceof PluginClassLoader); + assertTrue(desc2.getClassLoader() instanceof PluginClassLoader); + } + + @Test + public void testStopPluginsReverseOrder() throws Exception { + final Path pluginsDir = tempDir.resolve("lib/plugins"); + Files.createDirectories(pluginsDir); + + createTestPluginJar(pluginsDir, "plugin1", OrderTestPlugin1.class); + createTestPluginJar(pluginsDir, "plugin2", OrderTestPlugin2.class); + createTestPluginJar(pluginsDir, "plugin3", OrderTestPlugin3.class); + + OrderTestPlugin1.stopOrder.set(0); + OrderTestPlugin2.stopOrder.set(0); + OrderTestPlugin3.stopOrder.set(0); + OrderTestPlugin1.stopCounter.set(0); + OrderTestPlugin2.stopCounter.set(0); + OrderTestPlugin3.stopCounter.set(0); + + pluginManager.discoverPlugins(); + pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON); + + // Stop plugins - should be in reverse order of discovery + pluginManager.stopPlugins(); + + // Verify plugins were stopped in reverse order + assertTrue(OrderTestPlugin3.stopOrder.get() < OrderTestPlugin2.stopOrder.get()); + assertTrue(OrderTestPlugin2.stopOrder.get() < OrderTestPlugin1.stopOrder.get()); + } + + /** + * Helper method to create a test plugin JAR with proper META-INF/services + */ + private File createTestPluginJar(final Path pluginsDir, final String pluginName, final Class pluginClass) + throws Exception { + final File jarFile = pluginsDir.resolve(pluginName + ".jar").toFile(); + + try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile))) { + // Add the plugin class + final String classFileName = pluginClass.getName().replace('.', '/') + ".class"; + jos.putNextEntry(new JarEntry(classFileName)); + + // Load class bytes from current classloader + try (InputStream is = getClass().getClassLoader().getResourceAsStream(classFileName)) { + if (is != null) { + is.transferTo(jos); + } + } + jos.closeEntry(); + + // Add META-INF/services/com.arcadedb.server.ServerPlugin + jos.putNextEntry(new JarEntry("META-INF/services/com.arcadedb.server.ServerPlugin")); + jos.write(pluginClass.getName().getBytes()); + jos.closeEntry(); + } + + return jarFile; + } + + // Test plugin implementations + public static class TestPlugin1 implements ServerPlugin { + @Override + public void startService() { + } + } + + public static class TestPlugin2 implements ServerPlugin { + @Override + public void startService() { + } + } + + public static class LifecycleTestPlugin implements ServerPlugin { + public final AtomicBoolean configured = new AtomicBoolean(false); + public final AtomicBoolean started = new AtomicBoolean(false); + public final AtomicBoolean stopped = new AtomicBoolean(false); + + @Override + public void configure(ArcadeDBServer arcadeDBServer, ContextConfiguration configuration) { + configured.set(true); + } + + @Override + public void startService() { + started.set(true); + } + + @Override + public void stopService() { + stopped.set(true); + } + } + + public static class BeforeHttpPlugin implements ServerPlugin { + @Override + public void startService() { + } + + @Override + public INSTALLATION_PRIORITY getInstallationPriority() { + return INSTALLATION_PRIORITY.BEFORE_HTTP_ON; + } + } + + public static class AfterHttpPlugin implements ServerPlugin { + @Override + public void startService() { + } + + @Override + public INSTALLATION_PRIORITY getInstallationPriority() { + return INSTALLATION_PRIORITY.AFTER_HTTP_ON; + } + } + + public static class FailingPlugin implements ServerPlugin { + @Override + public void startService() { + throw new RuntimeException("Plugin failed to start"); + } + } + + public static class OrderTestPlugin1 implements ServerPlugin { + public static final AtomicInteger stopCounter = new AtomicInteger(0); + public static final AtomicInteger stopOrder = new AtomicInteger(0); + + @Override + public void startService() { + } + + @Override + public void stopService() { + stopOrder.set(stopCounter.incrementAndGet()); + } + } + + public static class OrderTestPlugin2 implements ServerPlugin { + public static final AtomicInteger stopCounter = new AtomicInteger(0); + public static final AtomicInteger stopOrder = new AtomicInteger(0); + + @Override + public void startService() { + } + + @Override + public void stopService() { + stopOrder.set(stopCounter.incrementAndGet()); + } + } + + public static class OrderTestPlugin3 implements ServerPlugin { + public static final AtomicInteger stopCounter = new AtomicInteger(0); + public static final AtomicInteger stopOrder = new AtomicInteger(0); + + @Override + public void startService() { + } + + @Override + public void stopService() { + stopOrder.set(stopCounter.incrementAndGet()); + } + } } From 09aedafe4ad1605acd0876f1d624a85c2460c91e Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:30:50 +0000 Subject: [PATCH 03/14] build: update minimal, headless, and base assembly files for plugin folder structure Apply the same lib/plugins/ structure from full.xml to all assembly configurations: - minimal.xml: Move postgresw to lib/plugins/ with useTransitiveDependencies=false - headless.xml: Move postgresw to lib/plugins/ with useTransitiveDependencies=false - base.xml: Update comment to match new convention (base excludes all plugins) This ensures consistent plugin deployment across all distribution types. Co-authored-by: Roberto Franchini --- package/src/main/assembly/base.xml | 10 ++++++---- package/src/main/assembly/headless.xml | 15 +++++++++++++-- package/src/main/assembly/minimal.xml | 15 +++++++++++++-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/package/src/main/assembly/base.xml b/package/src/main/assembly/base.xml index d7622c0b72..b2b88a2839 100644 --- a/package/src/main/assembly/base.xml +++ b/package/src/main/assembly/base.xml @@ -95,9 +95,10 @@ + lib @@ -106,15 +107,16 @@ com.arcadedb:arcadedb-console com.arcadedb:arcadedb-gremlin - com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-postgresw com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-graphql com.arcadedb:arcadedb-studio - com.arcadedb:arcadedb-postgresw - com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-metrics + diff --git a/package/src/main/assembly/headless.xml b/package/src/main/assembly/headless.xml index 7226e0b93f..abef33dbe1 100644 --- a/package/src/main/assembly/headless.xml +++ b/package/src/main/assembly/headless.xml @@ -93,9 +93,10 @@ + lib @@ -103,12 +104,22 @@ com.arcadedb:arcadedb-gremlin - com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-postgresw com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-graphql com.arcadedb:arcadedb-studio + + + lib/plugins + + com.arcadedb:arcadedb-postgresw + + false + diff --git a/package/src/main/assembly/minimal.xml b/package/src/main/assembly/minimal.xml index 019ad17ebf..560ec8e88f 100644 --- a/package/src/main/assembly/minimal.xml +++ b/package/src/main/assembly/minimal.xml @@ -93,9 +93,10 @@ + lib @@ -103,11 +104,21 @@ com.arcadedb:arcadedb-gremlin - com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-postgresw com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-graphql + + + lib/plugins + + com.arcadedb:arcadedb-postgresw + + false + From 5743fa72d121b5e5f175c30a54b17449274c93b3 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 20:09:06 +0000 Subject: [PATCH 04/14] feat: update arcadedb-builder.sh to deploy plugins to lib/plugins/ - Add creation of lib/plugins/ directory during build - Route shaded modules (plugins) to lib/plugins/ instead of lib/ - Regular modules continue to deploy to lib/ - Aligns builder with new plugin architecture using isolated class loaders Co-authored-by: Roberto Franchini --- package/arcadedb-builder.sh | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/package/arcadedb-builder.sh b/package/arcadedb-builder.sh index 22bc270026..7aa5d53e26 100755 --- a/package/arcadedb-builder.sh +++ b/package/arcadedb-builder.sh @@ -804,6 +804,17 @@ download_optional_modules() { local extracted_dir="$TEMP_DIR/arcadedb-${ARCADEDB_VERSION}" local lib_dir="${extracted_dir}/lib" + local plugins_dir="${extracted_dir}/lib/plugins" + + # Create plugins directory if it doesn't exist + if [[ "$DRY_RUN" != true ]]; then + if [[ ! -d "$plugins_dir" ]]; then + mkdir -p "$plugins_dir" + log_verbose "Created plugins directory: $plugins_dir" + fi + else + log_info "[DRY RUN] Would create plugins directory" + fi # Split modules by comma IFS=',' read -ra modules <<<"$SELECTED_MODULES" @@ -813,13 +824,16 @@ download_optional_modules() { # Determine if shaded or regular JAR local classifier="" + local dest_dir="$lib_dir" if [[ " $SHADED_MODULES " =~ " $module " ]]; then classifier="-shaded" + # Shaded modules are plugins and go to lib/plugins/ + dest_dir="$plugins_dir" fi local artifact_id="arcadedb-${module}" local jar_filename="${artifact_id}-${ARCADEDB_VERSION}${classifier}.jar" - local jar_file="${lib_dir}/${jar_filename}" + local jar_file="${dest_dir}/${jar_filename}" if [[ -n "$LOCAL_REPO" ]]; then # Local repository mode From d6c34101b998f233d3406cd61c90a1b720576a43 Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Thu, 29 Jan 2026 17:01:27 +0100 Subject: [PATCH 05/14] fix and improve plugin managements --- engine/pom.xml | 5 - .../services/com.arcadedb.server.ServerPlugin | 1 + ...heusMetricsPluginNotAuthenticatedTest.java | 2 +- package/src/main/assembly/full.xml | 16 ++- package/src/main/assembly/headless.xml | 1 + package/src/main/assembly/minimal.xml | 1 + pom.xml | 5 + .../com/arcadedb/server/ArcadeDBServer.java | 108 +++++++++--------- .../com/arcadedb/server/ServerPlugin.java | 10 +- .../server/plugin/PluginDescriptor.java | 18 +-- .../arcadedb/server/plugin/PluginManager.java | 101 ++++++++++++---- .../server/plugin/PluginManagerTest.java | 103 ++++++++--------- 12 files changed, 214 insertions(+), 157 deletions(-) create mode 100644 metrics/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin diff --git a/engine/pom.xml b/engine/pom.xml index 36b861cbf6..32b733cace 100644 --- a/engine/pom.xml +++ b/engine/pom.xml @@ -157,11 +157,6 @@ antlr4-runtime ${antlr4.version} - - org.slf4j - slf4j-jdk14 - ${slf4j.version} - org.graalvm.sdk graal-sdk diff --git a/metrics/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin b/metrics/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..56d49460a5 --- /dev/null +++ b/metrics/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin diff --git a/metrics/src/test/java/com/arcadedb/metrics/prometheus/PrometheusMetricsPluginNotAuthenticatedTest.java b/metrics/src/test/java/com/arcadedb/metrics/prometheus/PrometheusMetricsPluginNotAuthenticatedTest.java index 26bfab9f68..4627be1f36 100644 --- a/metrics/src/test/java/com/arcadedb/metrics/prometheus/PrometheusMetricsPluginNotAuthenticatedTest.java +++ b/metrics/src/test/java/com/arcadedb/metrics/prometheus/PrometheusMetricsPluginNotAuthenticatedTest.java @@ -37,7 +37,7 @@ class PrometheusMetricsPluginNotAuthenticatedTest extends BaseGraphServerTest { public void setTestConfiguration() { super.setTestConfiguration(); System.setProperty("arcadedb.serverMetrics.prometheus.requireAuthentication", "false"); - GlobalConfiguration.SERVER_PLUGINS.setValue("Prometheus:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin"); + GlobalConfiguration.SERVER_PLUGINS.setValue("PrometheusMetricsPlugin"); } @AfterEach diff --git a/package/src/main/assembly/full.xml b/package/src/main/assembly/full.xml index 7d41f5c5e3..ce98666ac3 100644 --- a/package/src/main/assembly/full.xml +++ b/package/src/main/assembly/full.xml @@ -109,17 +109,23 @@ com.arcadedb:arcadedb-mongodbw com.arcadedb:arcadedb-redisw com.arcadedb:arcadedb-grpcw + com.arcadedb:arcadedb-graphql + com.arcadedb:arcadedb-studio + com.arcadedb:arcadedb-metrics lib/plugins - com.arcadedb:arcadedb-gremlin - com.arcadedb:arcadedb-postgresw - com.arcadedb:arcadedb-mongodbw - com.arcadedb:arcadedb-redisw - com.arcadedb:arcadedb-grpcw + com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-postgresw + com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw + com.arcadedb:arcadedb-graphql + com.arcadedb:arcadedb-studio + com.arcadedb:arcadedb-metrics false diff --git a/package/src/main/assembly/headless.xml b/package/src/main/assembly/headless.xml index abef33dbe1..d15a499b8c 100644 --- a/package/src/main/assembly/headless.xml +++ b/package/src/main/assembly/headless.xml @@ -110,6 +110,7 @@ com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-graphql com.arcadedb:arcadedb-studio + com.arcadedb:arcadedb-metrics diff --git a/package/src/main/assembly/minimal.xml b/package/src/main/assembly/minimal.xml index 560ec8e88f..18ace7c7a7 100644 --- a/package/src/main/assembly/minimal.xml +++ b/package/src/main/assembly/minimal.xml @@ -109,6 +109,7 @@ com.arcadedb:arcadedb-redisw com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-graphql + com.arcadedb:arcadedb-metrics diff --git a/pom.xml b/pom.xml index 411119c2e4..7ee9389ac0 100644 --- a/pom.xml +++ b/pom.xml @@ -412,6 +412,11 @@ + + org.slf4j + slf4j-jdk14 + ${slf4j.version} + org.junit.jupiter junit-jupiter diff --git a/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java b/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java index 936a07f2e8..c9637deab9 100644 --- a/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java +++ b/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java @@ -40,10 +40,10 @@ import com.arcadedb.server.ha.HAServer; import com.arcadedb.server.ha.ReplicatedDatabase; import com.arcadedb.server.http.HttpServer; +import com.arcadedb.server.plugin.PluginManager; import com.arcadedb.server.security.ServerSecurity; import com.arcadedb.server.security.ServerSecurityException; import com.arcadedb.server.security.ServerSecurityUser; -import com.arcadedb.server.plugin.PluginManager; import com.arcadedb.utility.CodeUtils; import com.arcadedb.utility.FileUtils; import com.arcadedb.utility.ServerPathUtils; @@ -60,7 +60,15 @@ import java.io.IOException; import java.io.InputStream; import java.lang.reflect.InvocationTargetException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.logging.Level; @@ -78,15 +86,13 @@ public enum STATUS {OFFLINE, STARTING, ONLINE, SHUTTING_DOWN} private String hostAddress; private final boolean replicationLifecycleEventsEnabled; private FileServerEventLog eventLog; - private final Map plugins = - new LinkedHashMap<>(); + private final Map plugins = new LinkedHashMap<>(); private PluginManager pluginManager; private String serverRootPath; private HAServer haServer; private ServerSecurity security; private HttpServer httpServer; - private final ConcurrentMap databases = - new ConcurrentHashMap<>(); + private final ConcurrentMap databases = new ConcurrentHashMap<>(); private final List testEventListeners = new ArrayList<>(); private volatile STATUS status = STATUS.OFFLINE; // private ServerMonitor serverMonitor; @@ -176,8 +182,8 @@ public synchronized void start() { // START HTTP SERVER IMMEDIATELY. THE HTTP ADDRESS WILL BE USED BY HA httpServer = new HttpServer(this); - registerPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON); - pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON); +// registerPlugins(ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON); + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON); httpServer.startService(); @@ -186,16 +192,16 @@ public synchronized void start() { haServer.startService(); } - registerPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_HTTP_ON); - pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_HTTP_ON); +// registerPlugins(ServerPlugin.PluginInstallationPriority.AFTER_HTTP_ON); + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.AFTER_HTTP_ON); loadDefaultDatabases(); // RELOAD DATABASE IF A PLUGIN REGISTERED A NEW DATABASE (LIKE THE GREMLIN SERVER) loadDatabases(); - registerPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_DATABASES_OPEN); - pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_DATABASES_OPEN); +// registerPlugins(ServerPlugin.PluginInstallationPriority.AFTER_DATABASES_OPEN); + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.AFTER_DATABASES_OPEN); status = STATUS.ONLINE; @@ -294,7 +300,7 @@ private Set getAllPluginNames() { return result; } - private void registerPlugins(final ServerPlugin.INSTALLATION_PRIORITY installationPriority) { + private void registerPlugins(final ServerPlugin.PluginInstallationPriority installationPriority) { final String registeredPlugins = configuration.getValueAsString(GlobalConfiguration.SERVER_PLUGINS); if (registeredPlugins != null && !registeredPlugins.isEmpty()) { final String[] pluginEntries = registeredPlugins.split(","); @@ -528,7 +534,7 @@ public String toString() { } public ServerDatabase getDatabase(final String databaseName, final boolean createIfNotExists, - final boolean allowLoad) { + final boolean allowLoad) { if (databaseName == null || databaseName.trim().isEmpty()) throw new IllegalArgumentException("Invalid database name " + databaseName); @@ -549,7 +555,7 @@ public ServerDatabase getDatabase(final String databaseName, final boolean creat ComponentFile.MODE defaultDbMode = configuration.getValueAsEnum(GlobalConfiguration.SERVER_DEFAULT_DATABASE_MODE, - ComponentFile.MODE.class); + ComponentFile.MODE.class); if (defaultDbMode == null) defaultDbMode = READ_WRITE; @@ -646,46 +652,46 @@ private void loadDefaultDatabases() { final String commandParams = command.substring(commandSeparator + 1); switch (commandType) { - case "restore": - // DROP THE DATABASE BECAUSE THE RESTORE OPERATION WILL TAKE CARE OF CREATING A NEW DATABASE - if (database != null) { - ((DatabaseInternal) database).getEmbedded().drop(); - databases.remove(dbName); - } - final String dbPath = - configuration.getValueAsString(GlobalConfiguration.SERVER_DATABASE_DIRECTORY) + File.separator + dbName; + case "restore": + // DROP THE DATABASE BECAUSE THE RESTORE OPERATION WILL TAKE CARE OF CREATING A NEW DATABASE + if (database != null) { + ((DatabaseInternal) database).getEmbedded().drop(); + databases.remove(dbName); + } + final String dbPath = + configuration.getValueAsString(GlobalConfiguration.SERVER_DATABASE_DIRECTORY) + File.separator + dbName; // new Restore(commandParams, dbPath).restoreDatabase(); - try { - final Class clazz = Class.forName("com.arcadedb.integration.restore.Restore"); - final Object restorer = clazz.getConstructor(String.class, String.class).newInstance(commandParams, - dbPath); + try { + final Class clazz = Class.forName("com.arcadedb.integration.restore.Restore"); + final Object restorer = clazz.getConstructor(String.class, String.class).newInstance(commandParams, + dbPath); - clazz.getMethod("restoreDatabase").invoke(restorer); + clazz.getMethod("restoreDatabase").invoke(restorer); - } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException | - InstantiationException e) { - throw new CommandExecutionException("Error on restoring database, restore libs not found in " + - "classpath", e); - } catch (final InvocationTargetException e) { - throw new CommandExecutionException("Error on restoring database", e.getTargetException()); - } + } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException | + InstantiationException e) { + throw new CommandExecutionException("Error on restoring database, restore libs not found in " + + "classpath", e); + } catch (final InvocationTargetException e) { + throw new CommandExecutionException("Error on restoring database", e.getTargetException()); + } - getDatabase(dbName); - break; + getDatabase(dbName); + break; - case "import": - if (database == null) { - // CREATE THE DATABASE - LogManager.instance().log(this, Level.INFO, "Creating default database '%s'...", null, dbName); - database = createDatabase(dbName, defaultDbMode); - } - database.command("sql", "import database " + commandParams); - break; + case "import": + if (database == null) { + // CREATE THE DATABASE + LogManager.instance().log(this, Level.INFO, "Creating default database '%s'...", null, dbName); + database = createDatabase(dbName, defaultDbMode); + } + database.command("sql", "import database " + commandParams); + break; - default: - LogManager.instance().log(this, Level.SEVERE, "Unsupported command %s in startup command: '%s'", null - , commandType); + default: + LogManager.instance().log(this, Level.SEVERE, "Unsupported command %s in startup command: '%s'", null + , commandType); } } } else { @@ -726,7 +732,7 @@ private void parseCredentials(final String dbName, final String credentials) { user = security.authenticate(userName, userPassword, dbName); // UPDATE DB LIST + GROUP - user.addDatabase(dbName, new String[]{userGroup}); + user.addDatabase(dbName, new String[] { userGroup }); security.saveUsers(); } catch (final ServerSecurityException e) { @@ -737,7 +743,7 @@ private void parseCredentials(final String dbName, final String credentials) { } } else { // UPDATE DB LIST - user.addDatabase(dbName, new String[]{userGroup}); + user.addDatabase(dbName, new String[] { userGroup }); security.saveUsers(); } } else { @@ -748,7 +754,7 @@ private void parseCredentials(final String dbName, final String credentials) { // UPDATE DB LIST + GROUP ServerSecurityUser user = security.getUser(userName); - user.addDatabase(dbName, new String[]{userGroup}); + user.addDatabase(dbName, new String[] { userGroup }); security.saveUsers(); } } diff --git a/server/src/main/java/com/arcadedb/server/ServerPlugin.java b/server/src/main/java/com/arcadedb/server/ServerPlugin.java index 746993ed7a..9e31a180dc 100644 --- a/server/src/main/java/com/arcadedb/server/ServerPlugin.java +++ b/server/src/main/java/com/arcadedb/server/ServerPlugin.java @@ -22,10 +22,14 @@ import com.arcadedb.server.http.HttpServer; import io.undertow.server.handlers.PathHandler; -import static com.arcadedb.server.ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON; +import static com.arcadedb.server.ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON; public interface ServerPlugin { - enum INSTALLATION_PRIORITY {BEFORE_HTTP_ON, AFTER_HTTP_ON, AFTER_DATABASES_OPEN} + enum PluginInstallationPriority {BEFORE_HTTP_ON, AFTER_HTTP_ON, AFTER_DATABASES_OPEN} + + default String getName() { + return this.getClass().getSimpleName(); + } default void configure(ArcadeDBServer arcadeDBServer, ContextConfiguration configuration) { // DEFAULT IMPLEMENTATION @@ -41,7 +45,7 @@ default void registerAPI(final HttpServer httpServer, final PathHandler routes) // DEFAULT IMPLEMENTATION } - default INSTALLATION_PRIORITY getInstallationPriority() { + default PluginInstallationPriority getInstallationPriority() { return BEFORE_HTTP_ON; } } diff --git a/server/src/main/java/com/arcadedb/server/plugin/PluginDescriptor.java b/server/src/main/java/com/arcadedb/server/plugin/PluginDescriptor.java index 4f051c45ae..75fbe747fa 100644 --- a/server/src/main/java/com/arcadedb/server/plugin/PluginDescriptor.java +++ b/server/src/main/java/com/arcadedb/server/plugin/PluginDescriptor.java @@ -20,7 +20,6 @@ import com.arcadedb.server.ServerPlugin; -import java.io.File; import java.util.Objects; /** @@ -30,15 +29,13 @@ * @author Luca Garulli (l.garulli@arcadedata.com) */ public class PluginDescriptor { - private final String pluginName; - private final File pluginJarFile; - private final ClassLoader classLoader; - private ServerPlugin pluginInstance; - private boolean started; + private final String pluginName; + private final ClassLoader classLoader; + private ServerPlugin pluginInstance; + private boolean started; - public PluginDescriptor(final String pluginName, final File pluginJarFile, final ClassLoader classLoader) { + public PluginDescriptor(final String pluginName, final ClassLoader classLoader) { this.pluginName = Objects.requireNonNull(pluginName, "Plugin name cannot be null"); - this.pluginJarFile = Objects.requireNonNull(pluginJarFile, "Plugin JAR file cannot be null"); this.classLoader = Objects.requireNonNull(classLoader, "Class loader cannot be null"); this.started = false; } @@ -47,10 +44,6 @@ public String getPluginName() { return pluginName; } - public File getPluginJarFile() { - return pluginJarFile; - } - public ClassLoader getClassLoader() { return classLoader; } @@ -75,7 +68,6 @@ public void setStarted(final boolean started) { public String toString() { return "PluginDescriptor{" + "pluginName='" + pluginName + '\'' + - ", pluginJarFile=" + pluginJarFile + ", started=" + started + '}'; } diff --git a/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java b/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java index d10baf8508..1fa9358c67 100644 --- a/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java +++ b/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java @@ -19,6 +19,7 @@ package com.arcadedb.server.plugin; import com.arcadedb.ContextConfiguration; +import com.arcadedb.GlobalConfiguration; import com.arcadedb.log.LogManager; import com.arcadedb.server.ArcadeDBServer; import com.arcadedb.server.ServerException; @@ -27,7 +28,15 @@ import java.io.File; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; @@ -38,16 +47,52 @@ * @author Luca Garulli (l.garulli@arcadedata.com) */ public class PluginManager { - private final ArcadeDBServer server; - private final ContextConfiguration configuration; - private final String pluginsDirectory; - private final Map plugins = new LinkedHashMap<>(); - private final Map classLoaderMap = new ConcurrentHashMap<>(); + private final ArcadeDBServer server; + private final ContextConfiguration configuration; + private final String pluginsDirectory; + private final Map plugins = new LinkedHashMap<>(); + private final Map classLoaderMap = new ConcurrentHashMap<>(); + private final Set configuredPlugins; public PluginManager(final ArcadeDBServer server, final ContextConfiguration configuration) { this.server = server; this.configuration = configuration; this.pluginsDirectory = server.getRootPath() + File.separator + "lib" + File.separator + "plugins"; + configuredPlugins = getConfiguredPlugins(); + + } + + private Set getConfiguredPlugins() { + final String configuration = this.configuration.getValueAsString(GlobalConfiguration.SERVER_PLUGINS); + Set configuredPlugins = new HashSet<>(); + if (!configuration.isEmpty()) { + final String[] pluginEntries = configuration.split(","); + for (final String p : pluginEntries) { + final String[] pluginPair = p.split(":"); + + final String pluginName = pluginPair[0]; + configuredPlugins.add(pluginName); + final String pluginClass = pluginPair.length > 1 ? pluginPair[1] : pluginPair[0]; + configuredPlugins.add(pluginClass); + } + } + return configuredPlugins; + } + + private void discoverPluginsOnMainClassLoader() { + final ServiceLoader serviceLoader = ServiceLoader.load(ServerPlugin.class, getClass().getClassLoader()); + + for (ServerPlugin pluginInstance : serviceLoader) { + String name = pluginInstance.getClass().getSimpleName(); + if (configuredPlugins.contains(name) || configuredPlugins.contains(pluginInstance.getClass().getName())) { + // Register the plugin + final PluginDescriptor descriptor = new PluginDescriptor(name, getClass().getClassLoader()); + descriptor.setPluginInstance(pluginInstance); + plugins.put(name, descriptor); + + LogManager.instance().log(this, Level.INFO, "Discovered plugin on main class loader: %s", name); + } + } } /** @@ -55,6 +100,8 @@ public PluginManager(final ArcadeDBServer server, final ContextConfiguration con * Each plugin JAR is loaded in its own isolated class loader. */ public void discoverPlugins() { + discoverPluginsOnMainClassLoader(); + final File pluginsDir = new File(pluginsDirectory); if (!pluginsDir.exists() || !pluginsDir.isDirectory()) { LogManager.instance().log(this, Level.INFO, "Plugins directory not found: %s", pluginsDirectory); @@ -80,6 +127,7 @@ public void discoverPlugins() { * Load a plugin from a JAR file using an isolated class loader. */ private void loadPlugin(final File pluginJar) throws Exception { + final String jarName = pluginJar.getName(); final String pluginName = jarName.substring(0, jarName.lastIndexOf('.')); @@ -88,34 +136,41 @@ private void loadPlugin(final File pluginJar) throws Exception { // Create isolated class loader for this plugin final PluginClassLoader classLoader = new PluginClassLoader(pluginName, pluginJar, getClass().getClassLoader()); - // Create plugin descriptor - final PluginDescriptor descriptor = new PluginDescriptor(pluginName, pluginJar, classLoader); - // Use ServiceLoader to discover plugin implementations final ServiceLoader serviceLoader = ServiceLoader.load(ServerPlugin.class, classLoader); - final Iterator iterator = serviceLoader.iterator(); - - if (!iterator.hasNext()) { - LogManager.instance().log(this, Level.WARNING, - "No ServerPlugin implementation found in: %s (missing META-INF/services entry?)", pluginJar.getAbsolutePath()); - return; - } // Load the first plugin implementation (typically only one per JAR) - final ServerPlugin pluginInstance = iterator.next(); - descriptor.setPluginInstance(pluginInstance); + for (ServerPlugin pluginInstance : serviceLoader) { + // Create plugin descriptor + final PluginDescriptor descriptor = new PluginDescriptor(pluginInstance.getName(), classLoader); + descriptor.setPluginInstance(pluginInstance); - // Register the plugin - plugins.put(pluginName, descriptor); - classLoaderMap.put(classLoader, descriptor); + String name = pluginInstance.getName(); + LogManager.instance().log(this, Level.FINE, "Discovered plugin class: %s", name); - LogManager.instance().log(this, Level.INFO, "Discovered plugin: %s from %s", pluginName, pluginJar.getName()); + if (plugins.containsKey(name)) { + LogManager.instance().log(this, Level.WARNING, "Plugin with name '%s' is already loaded", name); + continue; + } + + if (configuredPlugins.contains(name) || configuredPlugins.contains(pluginName) || configuredPlugins.contains( + pluginInstance.getClass().getName())) { + // Register the plugin + plugins.put(name, descriptor); + classLoaderMap.put(classLoader, descriptor); + + LogManager.instance().log(this, Level.INFO, "Loaded plugin: %s from %s", name, pluginJar.getName()); + } else { + classLoader.close(); + LogManager.instance().log(this, Level.INFO, "Skipping plugin: %s as not registered in configuration", name); + } + } } /** * Start plugins based on their installation priority. */ - public void startPlugins(final ServerPlugin.INSTALLATION_PRIORITY priority) { + public void startPlugins(final ServerPlugin.PluginInstallationPriority priority) { for (final Map.Entry entry : plugins.entrySet()) { final String pluginName = entry.getKey(); final PluginDescriptor descriptor = entry.getValue(); diff --git a/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java b/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java index 4b79bc90ab..2d6488e790 100644 --- a/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java +++ b/server/src/test/java/com/arcadedb/server/plugin/PluginManagerTest.java @@ -23,14 +23,15 @@ import com.arcadedb.server.ArcadeDBServer; import com.arcadedb.server.ServerException; import com.arcadedb.server.ServerPlugin; -import com.arcadedb.server.http.HttpServer; -import io.undertow.server.handlers.PathHandler; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import java.io.*; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; @@ -40,7 +41,13 @@ import java.util.jar.JarEntry; import java.util.jar.JarOutputStream; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Test for PluginManager to verify plugin discovery and loading with isolated class loaders. @@ -59,6 +66,16 @@ public void setup() { final ContextConfiguration configuration = new ContextConfiguration(); configuration.setValue(GlobalConfiguration.SERVER_ROOT_PATH, tempDir.toString()); configuration.setValue(GlobalConfiguration.SERVER_DATABASE_DIRECTORY, tempDir.resolve("databases").toString()); + configuration.setValue(GlobalConfiguration.SERVER_PLUGINS, + TestPlugin1.class.getSimpleName() + "," + + TestPlugin2.class.getSimpleName() + "," + + LifecycleTestPlugin.class.getSimpleName() + "," + + AfterHttpPlugin.class.getSimpleName() + "," + + FailingPlugin.class.getSimpleName() + "," + + OrderTestPlugin1.class.getSimpleName() + "," + + OrderTestPlugin2.class.getSimpleName() + "," + + OrderTestPlugin3.class.getSimpleName() + "," + + BeforeHttpPlugin.class.getSimpleName()); server = new ArcadeDBServer(configuration); pluginManager = new PluginManager(server, configuration); @@ -128,7 +145,7 @@ public void testLoadPluginWithMetaInfServices() throws Exception { pluginManager.discoverPlugins(); assertEquals(1, pluginManager.getPluginCount()); - assertTrue(pluginManager.getPluginNames().contains("test-plugin")); + assertTrue(pluginManager.getPluginNames().contains(TestPlugin1.class.getSimpleName())); final Collection plugins = pluginManager.getPlugins(); assertEquals(1, plugins.size()); @@ -146,8 +163,8 @@ public void testLoadMultiplePlugins() throws Exception { assertEquals(2, pluginManager.getPluginCount()); final Set names = pluginManager.getPluginNames(); - assertTrue(names.contains("plugin1")); - assertTrue(names.contains("plugin2")); + assertTrue(names.contains(TestPlugin1.class.getSimpleName())); + assertTrue(names.contains(TestPlugin2.class.getSimpleName())); } @Test @@ -161,10 +178,10 @@ public void testPluginLifecycle() throws Exception { assertEquals(1, pluginManager.getPluginCount()); // Start the plugin - pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON); + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON); // Verify plugin was configured and started - final PluginDescriptor descriptor = pluginManager.getPluginDescriptor("lifecycle-plugin"); + final PluginDescriptor descriptor = pluginManager.getPluginDescriptor(LifecycleTestPlugin.class.getSimpleName()); assertNotNull(descriptor); assertTrue(descriptor.isStarted()); assertTrue(descriptor.getPluginInstance() instanceof LifecycleTestPlugin); @@ -192,16 +209,16 @@ public void testPluginStartOrderByPriority() throws Exception { assertEquals(2, pluginManager.getPluginCount()); // Start BEFORE_HTTP_ON plugins - pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON); + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON); - PluginDescriptor beforeDesc = pluginManager.getPluginDescriptor("before-plugin"); - PluginDescriptor afterDesc = pluginManager.getPluginDescriptor("after-plugin"); + PluginDescriptor beforeDesc = pluginManager.getPluginDescriptor(BeforeHttpPlugin.class.getSimpleName()); + PluginDescriptor afterDesc = pluginManager.getPluginDescriptor(AfterHttpPlugin.class.getSimpleName()); assertTrue(beforeDesc.isStarted()); assertFalse(afterDesc.isStarted()); // Start AFTER_HTTP_ON plugins - pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.AFTER_HTTP_ON); + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.AFTER_HTTP_ON); assertTrue(beforeDesc.isStarted()); assertTrue(afterDesc.isStarted()); @@ -239,7 +256,7 @@ public void testPluginStartException() throws Exception { // Starting the plugin should throw exception assertThrows(ServerException.class, () -> - pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON)); + pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.BEFORE_HTTP_ON)); } @Test @@ -251,10 +268,9 @@ public void testGetPluginDescriptor() throws Exception { pluginManager.discoverPlugins(); - final PluginDescriptor descriptor = pluginManager.getPluginDescriptor("test-plugin"); + final PluginDescriptor descriptor = pluginManager.getPluginDescriptor(TestPlugin1.class.getSimpleName()); assertNotNull(descriptor); - assertEquals("test-plugin", descriptor.getPluginName()); - assertNotNull(descriptor.getPluginJarFile()); + assertEquals(TestPlugin1.class.getSimpleName(), descriptor.getPluginName()); assertNotNull(descriptor.getClassLoader()); assertNotNull(descriptor.getPluginInstance()); assertFalse(descriptor.isStarted()); @@ -270,8 +286,8 @@ public void testClassLoaderIsolation() throws Exception { pluginManager.discoverPlugins(); - final PluginDescriptor desc1 = pluginManager.getPluginDescriptor("plugin1"); - final PluginDescriptor desc2 = pluginManager.getPluginDescriptor("plugin2"); + final PluginDescriptor desc1 = pluginManager.getPluginDescriptor(TestPlugin1.class.getSimpleName()); + final PluginDescriptor desc2 = pluginManager.getPluginDescriptor(TestPlugin2.class.getSimpleName()); // Each plugin should have its own class loader assertNotNull(desc1.getClassLoader()); @@ -283,37 +299,12 @@ public void testClassLoaderIsolation() throws Exception { assertTrue(desc2.getClassLoader() instanceof PluginClassLoader); } - @Test - public void testStopPluginsReverseOrder() throws Exception { - final Path pluginsDir = tempDir.resolve("lib/plugins"); - Files.createDirectories(pluginsDir); - - createTestPluginJar(pluginsDir, "plugin1", OrderTestPlugin1.class); - createTestPluginJar(pluginsDir, "plugin2", OrderTestPlugin2.class); - createTestPluginJar(pluginsDir, "plugin3", OrderTestPlugin3.class); - - OrderTestPlugin1.stopOrder.set(0); - OrderTestPlugin2.stopOrder.set(0); - OrderTestPlugin3.stopOrder.set(0); - OrderTestPlugin1.stopCounter.set(0); - OrderTestPlugin2.stopCounter.set(0); - OrderTestPlugin3.stopCounter.set(0); - - pluginManager.discoverPlugins(); - pluginManager.startPlugins(ServerPlugin.INSTALLATION_PRIORITY.BEFORE_HTTP_ON); - - // Stop plugins - should be in reverse order of discovery - pluginManager.stopPlugins(); - - // Verify plugins were stopped in reverse order - assertTrue(OrderTestPlugin3.stopOrder.get() < OrderTestPlugin2.stopOrder.get()); - assertTrue(OrderTestPlugin2.stopOrder.get() < OrderTestPlugin1.stopOrder.get()); - } - /** * Helper method to create a test plugin JAR with proper META-INF/services */ - private File createTestPluginJar(final Path pluginsDir, final String pluginName, final Class pluginClass) + private File createTestPluginJar(final Path pluginsDir, + final String pluginName, + final Class pluginClass) throws Exception { final File jarFile = pluginsDir.resolve(pluginName + ".jar").toFile(); @@ -354,8 +345,8 @@ public void startService() { public static class LifecycleTestPlugin implements ServerPlugin { public final AtomicBoolean configured = new AtomicBoolean(false); - public final AtomicBoolean started = new AtomicBoolean(false); - public final AtomicBoolean stopped = new AtomicBoolean(false); + public final AtomicBoolean started = new AtomicBoolean(false); + public final AtomicBoolean stopped = new AtomicBoolean(false); @Override public void configure(ArcadeDBServer arcadeDBServer, ContextConfiguration configuration) { @@ -379,8 +370,8 @@ public void startService() { } @Override - public INSTALLATION_PRIORITY getInstallationPriority() { - return INSTALLATION_PRIORITY.BEFORE_HTTP_ON; + public PluginInstallationPriority getInstallationPriority() { + return PluginInstallationPriority.BEFORE_HTTP_ON; } } @@ -390,8 +381,8 @@ public void startService() { } @Override - public INSTALLATION_PRIORITY getInstallationPriority() { - return INSTALLATION_PRIORITY.AFTER_HTTP_ON; + public PluginInstallationPriority getInstallationPriority() { + return PluginInstallationPriority.AFTER_HTTP_ON; } } @@ -404,7 +395,7 @@ public void startService() { public static class OrderTestPlugin1 implements ServerPlugin { public static final AtomicInteger stopCounter = new AtomicInteger(0); - public static final AtomicInteger stopOrder = new AtomicInteger(0); + public static final AtomicInteger stopOrder = new AtomicInteger(0); @Override public void startService() { @@ -418,7 +409,7 @@ public void stopService() { public static class OrderTestPlugin2 implements ServerPlugin { public static final AtomicInteger stopCounter = new AtomicInteger(0); - public static final AtomicInteger stopOrder = new AtomicInteger(0); + public static final AtomicInteger stopOrder = new AtomicInteger(0); @Override public void startService() { @@ -432,7 +423,7 @@ public void stopService() { public static class OrderTestPlugin3 implements ServerPlugin { public static final AtomicInteger stopCounter = new AtomicInteger(0); - public static final AtomicInteger stopOrder = new AtomicInteger(0); + public static final AtomicInteger stopOrder = new AtomicInteger(0); @Override public void startService() { From 61891b14a328f4b4c3fd7e48c6685c48cc4e5daf Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Thu, 29 Jan 2026 17:10:33 +0100 Subject: [PATCH 06/14] rebased, addded Bolt plugin --- .../META-INF/com.arcadedb.server.ServerPlugin | 1 + package/src/main/assembly/base.xml | 1 + package/src/main/assembly/full.xml | 18 ++++++++++-------- package/src/main/assembly/headless.xml | 1 + package/src/main/assembly/minimal.xml | 1 + .../com/arcadedb/server/ArcadeDBServer.java | 2 +- .../backup/AutoBackupSchedulerPlugin.java | 4 ++-- 7 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 bolt/src/main/resources/META-INF/com.arcadedb.server.ServerPlugin diff --git a/bolt/src/main/resources/META-INF/com.arcadedb.server.ServerPlugin b/bolt/src/main/resources/META-INF/com.arcadedb.server.ServerPlugin new file mode 100644 index 0000000000..62331a7d2a --- /dev/null +++ b/bolt/src/main/resources/META-INF/com.arcadedb.server.ServerPlugin @@ -0,0 +1 @@ +com.arcadedb.bolt.BoltProtocolPlugin diff --git a/package/src/main/assembly/base.xml b/package/src/main/assembly/base.xml index b2b88a2839..2422daf39c 100644 --- a/package/src/main/assembly/base.xml +++ b/package/src/main/assembly/base.xml @@ -107,6 +107,7 @@ com.arcadedb:arcadedb-console com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-bolt com.arcadedb:arcadedb-postgresw com.arcadedb:arcadedb-mongodbw com.arcadedb:arcadedb-redisw diff --git a/package/src/main/assembly/full.xml b/package/src/main/assembly/full.xml index ce98666ac3..e19dc4aa78 100644 --- a/package/src/main/assembly/full.xml +++ b/package/src/main/assembly/full.xml @@ -105,6 +105,7 @@ com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-bolt com.arcadedb:arcadedb-postgresw com.arcadedb:arcadedb-mongodbw com.arcadedb:arcadedb-redisw @@ -118,14 +119,15 @@ lib/plugins - com.arcadedb:arcadedb-gremlin - com.arcadedb:arcadedb-postgresw - com.arcadedb:arcadedb-mongodbw - com.arcadedb:arcadedb-redisw - com.arcadedb:arcadedb-grpcw - com.arcadedb:arcadedb-graphql - com.arcadedb:arcadedb-studio - com.arcadedb:arcadedb-metrics + com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-bolt + com.arcadedb:arcadedb-postgresw + com.arcadedb:arcadedb-mongodbw + com.arcadedb:arcadedb-redisw + com.arcadedb:arcadedb-grpcw + com.arcadedb:arcadedb-graphql + com.arcadedb:arcadedb-studio + com.arcadedb:arcadedb-metrics false diff --git a/package/src/main/assembly/headless.xml b/package/src/main/assembly/headless.xml index d15a499b8c..204e98ea86 100644 --- a/package/src/main/assembly/headless.xml +++ b/package/src/main/assembly/headless.xml @@ -104,6 +104,7 @@ com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-bolt com.arcadedb:arcadedb-postgresw com.arcadedb:arcadedb-mongodbw com.arcadedb:arcadedb-redisw diff --git a/package/src/main/assembly/minimal.xml b/package/src/main/assembly/minimal.xml index 18ace7c7a7..40d175fe74 100644 --- a/package/src/main/assembly/minimal.xml +++ b/package/src/main/assembly/minimal.xml @@ -104,6 +104,7 @@ com.arcadedb:arcadedb-gremlin + com.arcadedb:arcadedb-bolt com.arcadedb:arcadedb-postgresw com.arcadedb:arcadedb-mongodbw com.arcadedb:arcadedb-redisw diff --git a/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java b/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java index c9637deab9..ffb8a1b6f5 100644 --- a/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java +++ b/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java @@ -332,7 +332,7 @@ private void registerPlugins(final ServerPlugin.PluginInstallationPriority insta } // Auto-register backup scheduler plugin if backup.json exists and not already registered - if (installationPriority == ServerPlugin.INSTALLATION_PRIORITY.AFTER_DATABASES_OPEN + if (installationPriority == ServerPlugin.PluginInstallationPriority.AFTER_DATABASES_OPEN && !plugins.containsKey("auto-backup")) { registerAutoBackupPluginIfConfigured(); } diff --git a/server/src/main/java/com/arcadedb/server/backup/AutoBackupSchedulerPlugin.java b/server/src/main/java/com/arcadedb/server/backup/AutoBackupSchedulerPlugin.java index 1509a42313..14ff5d58b5 100644 --- a/server/src/main/java/com/arcadedb/server/backup/AutoBackupSchedulerPlugin.java +++ b/server/src/main/java/com/arcadedb/server/backup/AutoBackupSchedulerPlugin.java @@ -193,9 +193,9 @@ public void stopService() { } @Override - public INSTALLATION_PRIORITY getInstallationPriority() { + public PluginInstallationPriority getInstallationPriority() { // Install after databases are open so we can schedule backups for all databases - return INSTALLATION_PRIORITY.AFTER_DATABASES_OPEN; + return PluginInstallationPriority.AFTER_DATABASES_OPEN; } /** From 73f1af5bf6201eb3ffd7d706f4e86a55d87e3c7e Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Thu, 29 Jan 2026 17:51:33 +0100 Subject: [PATCH 07/14] fix: address code review feedback for plugin architecture - Fix resource leak in loadPlugin(): use try-finally to ensure classloader is closed when plugin is not registered - Fix duplicate plugin handling: close classloader and break loop instead of continuing iteration - Fix bolt plugin META-INF path: move service descriptor to correct location META-INF/services/com.arcadedb.server.ServerPlugin - Improve error message: include priority level in exception for better debugging Co-Authored-By: Claude Opus 4.5 --- .../com.arcadedb.server.ServerPlugin | 0 .../arcadedb/server/plugin/PluginManager.java | 54 +++++++++++-------- 2 files changed, 31 insertions(+), 23 deletions(-) rename bolt/src/main/resources/META-INF/{ => services}/com.arcadedb.server.ServerPlugin (100%) diff --git a/bolt/src/main/resources/META-INF/com.arcadedb.server.ServerPlugin b/bolt/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin similarity index 100% rename from bolt/src/main/resources/META-INF/com.arcadedb.server.ServerPlugin rename to bolt/src/main/resources/META-INF/services/com.arcadedb.server.ServerPlugin diff --git a/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java b/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java index 1fa9358c67..66d0808dfd 100644 --- a/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java +++ b/server/src/main/java/com/arcadedb/server/plugin/PluginManager.java @@ -127,7 +127,6 @@ public void discoverPlugins() { * Load a plugin from a JAR file using an isolated class loader. */ private void loadPlugin(final File pluginJar) throws Exception { - final String jarName = pluginJar.getName(); final String pluginName = jarName.substring(0, jarName.lastIndexOf('.')); @@ -135,34 +134,43 @@ private void loadPlugin(final File pluginJar) throws Exception { // Create isolated class loader for this plugin final PluginClassLoader classLoader = new PluginClassLoader(pluginName, pluginJar, getClass().getClassLoader()); + boolean registered = false; - // Use ServiceLoader to discover plugin implementations - final ServiceLoader serviceLoader = ServiceLoader.load(ServerPlugin.class, classLoader); + try { + // Use ServiceLoader to discover plugin implementations + final ServiceLoader serviceLoader = ServiceLoader.load(ServerPlugin.class, classLoader); - // Load the first plugin implementation (typically only one per JAR) - for (ServerPlugin pluginInstance : serviceLoader) { - // Create plugin descriptor - final PluginDescriptor descriptor = new PluginDescriptor(pluginInstance.getName(), classLoader); - descriptor.setPluginInstance(pluginInstance); + // Load the first plugin implementation (typically only one per JAR) + for (ServerPlugin pluginInstance : serviceLoader) { + // Create plugin descriptor + final PluginDescriptor descriptor = new PluginDescriptor(pluginInstance.getName(), classLoader); + descriptor.setPluginInstance(pluginInstance); - String name = pluginInstance.getName(); - LogManager.instance().log(this, Level.FINE, "Discovered plugin class: %s", name); + String name = pluginInstance.getName(); + LogManager.instance().log(this, Level.FINE, "Discovered plugin class: %s", name); - if (plugins.containsKey(name)) { - LogManager.instance().log(this, Level.WARNING, "Plugin with name '%s' is already loaded", name); - continue; - } + if (plugins.containsKey(name)) { + LogManager.instance().log(this, Level.WARNING, "Plugin with name '%s' is already loaded, skipping duplicate from %s", + name, pluginJar.getName()); + break; // Exit loop - classloader will be closed in finally block + } - if (configuredPlugins.contains(name) || configuredPlugins.contains(pluginName) || configuredPlugins.contains( - pluginInstance.getClass().getName())) { - // Register the plugin - plugins.put(name, descriptor); - classLoaderMap.put(classLoader, descriptor); + if (configuredPlugins.contains(name) || configuredPlugins.contains(pluginName) || configuredPlugins.contains( + pluginInstance.getClass().getName())) { + // Register the plugin + plugins.put(name, descriptor); + classLoaderMap.put(classLoader, descriptor); + registered = true; - LogManager.instance().log(this, Level.INFO, "Loaded plugin: %s from %s", name, pluginJar.getName()); - } else { + LogManager.instance().log(this, Level.INFO, "Loaded plugin: %s from %s", name, pluginJar.getName()); + } else { + LogManager.instance().log(this, Level.INFO, "Skipping plugin: %s as not registered in configuration", name); + } + break; // Only load the first plugin from each JAR + } + } finally { + if (!registered) { classLoader.close(); - LogManager.instance().log(this, Level.INFO, "Skipping plugin: %s as not registered in configuration", name); } } } @@ -202,7 +210,7 @@ public void startPlugins(final ServerPlugin.PluginInstallationPriority priority) currentThread.setContextClassLoader(originalClassLoader); } } catch (final Exception e) { - throw new ServerException("Error starting plugin: " + pluginName, e); + throw new ServerException("Error starting plugin: " + pluginName + " (priority: " + priority + ")", e); } } } From ba5f0c38945800473ddf093f501e6e039254330c Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Thu, 29 Jan 2026 17:52:41 +0100 Subject: [PATCH 08/14] use short names for plugin, disable postgres debug logging --- .../test/java/com/arcadedb/e2e/ArcadeContainerTemplate.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/src/test/java/com/arcadedb/e2e/ArcadeContainerTemplate.java b/e2e/src/test/java/com/arcadedb/e2e/ArcadeContainerTemplate.java index d19e38d118..f4ede9adb3 100644 --- a/e2e/src/test/java/com/arcadedb/e2e/ArcadeContainerTemplate.java +++ b/e2e/src/test/java/com/arcadedb/e2e/ArcadeContainerTemplate.java @@ -32,14 +32,14 @@ public abstract class ArcadeContainerTemplate { .withStartupTimeout(Duration.ofSeconds(90)) .withEnv("JAVA_OPTS", """ -Darcadedb.server.rootPassword=playwithdata - -Darcadedb.postgres.debug=true + -Darcadedb.postgres.debug=false -Darcadedb.grpc.enabled=true -Darcadedb.grpc.port=50051 -Darcadedb.grpc.mode=standard -Darcadedb.grpc.reflection.enabled=true -Darcadedb.grpc.health.enabled=true -Darcadedb.server.defaultDatabases=beer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz} - -Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin,GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin,GRPC:com.arcadedb.server.grpc.GrpcServerPlugin,PrometheusMetrics:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin + -Darcadedb.server.plugins=PostgresProtocolPlugin,GremlinServerPlugin,GrpcServerPlugin,PrometheusMetricsPlugin """) .waitingFor(Wait.forHttp("/api/v1/ready").forPort(2480).forStatusCode(204)); ARCADE.start(); From 41e5c158b78b94fed8f30a4bb8a095a032629d3b Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Thu, 29 Jan 2026 18:02:57 +0100 Subject: [PATCH 09/14] use short names for plugin on other e2e based on TC --- e2e-js/src/js-pg-e2e.test.js | 2 +- .../java/com/arcadedb/test/support/ContainersTestTemplate.java | 2 +- e2e-python/tests/test_arcadedb.py | 2 +- e2e-python/tests/test_asyncpg.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/e2e-js/src/js-pg-e2e.test.js b/e2e-js/src/js-pg-e2e.test.js index bd76f109fc..3e6fe7dcc3 100644 --- a/e2e-js/src/js-pg-e2e.test.js +++ b/e2e-js/src/js-pg-e2e.test.js @@ -30,7 +30,7 @@ describe("E2E tests using pg client", () => { .withExposedPorts(2480, 6379, 5432, 8182) .withEnvironment({ JAVA_OPTS: - "-Darcadedb.server.rootPassword=playwithdata -Darcadedb.server.defaultDatabases=beer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz} -Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin,GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin,PrometheusMetrics:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin", + "-Darcadedb.server.rootPassword=playwithdata -Darcadedb.server.defaultDatabases=beer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz} -Darcadedb.server.plugins=PostgresProtocolPlugin,GremlinServerPlugin,PrometheusMetricsPlugin", }) .withStartupTimeout(60000) .withWaitStrategy(Wait.forHttp("/api/v1/ready", 2480).forStatusCodeMatching((statusCode) => statusCode === 204)) diff --git a/e2e-perf/src/test/java/com/arcadedb/test/support/ContainersTestTemplate.java b/e2e-perf/src/test/java/com/arcadedb/test/support/ContainersTestTemplate.java index 3586c857d2..b15d19ad96 100644 --- a/e2e-perf/src/test/java/com/arcadedb/test/support/ContainersTestTemplate.java +++ b/e2e-perf/src/test/java/com/arcadedb/test/support/ContainersTestTemplate.java @@ -207,7 +207,7 @@ protected GenericContainer createArcadeContainer(String name, .withEnv("JAVA_OPTS", String.format(""" -Darcadedb.server.rootPassword=playwithdata - -Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin,GRPC:com.arcadedb.server.grpc.GrpcServerPlugin,PrometheusMetrics:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin + -Darcadedb.server.plugins=PostgresProtocolPlugin,GrpcServerPlugin,PrometheusMetricsPlugin -Darcadedb.server.httpsIoThreads=30 -Darcadedb.bucketReuseSpaceMode=low -Darcadedb.server.name=%s diff --git a/e2e-python/tests/test_arcadedb.py b/e2e-python/tests/test_arcadedb.py index 890eade4c5..ff5bd26c04 100644 --- a/e2e-python/tests/test_arcadedb.py +++ b/e2e-python/tests/test_arcadedb.py @@ -31,7 +31,7 @@ .with_env("JAVA_OPTS", "-Darcadedb.server.rootPassword=playwithdata " "-Darcadedb.server.defaultDatabases=beer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz} " - "-Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin,GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin,PrometheusMetrics:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin")) + "-Darcadedb.server.plugins=PostgresProtocolPlugin,GremlinServerPlugin,PrometheusMetricsPlugin")) def get_connection_params(container): diff --git a/e2e-python/tests/test_asyncpg.py b/e2e-python/tests/test_asyncpg.py index 2f011bef66..5953cc6435 100644 --- a/e2e-python/tests/test_asyncpg.py +++ b/e2e-python/tests/test_asyncpg.py @@ -49,7 +49,7 @@ .with_env("JAVA_OPTS", "-Darcadedb.server.rootPassword=playwithdata " "-Darcadedb.server.defaultDatabases=beer[root]{import:https://github.com/ArcadeData/arcadedb-datasets/raw/main/orientdb/OpenBeer.gz} " - "-Darcadedb.server.plugins=Postgres:com.arcadedb.postgres.PostgresProtocolPlugin,GremlinServer:com.arcadedb.server.gremlin.GremlinServerPlugin,PrometheusMetrics:com.arcadedb.metrics.prometheus.PrometheusMetricsPlugin")) + "-Darcadedb.server.plugins=PostgresProtocolPlugin,GremlinServerPlugin,PrometheusMetricsPlugin")) def get_connection_params(container): From 0a9686e835df9f3a21b778e683bce675b79c6334 Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Thu, 29 Jan 2026 21:44:55 +0100 Subject: [PATCH 10/14] cleapu, add bolt to packaging --- .../com/arcadedb/database/LocalDatabase.java | 44 ++++++++++++++----- .../arcadedb/query/QueryEngineManager.java | 4 +- package/pom.xml | 12 +++++ package/src/main/assembly/full.xml | 2 - .../com/arcadedb/server/ArcadeDBServer.java | 2 - 5 files changed, 47 insertions(+), 17 deletions(-) diff --git a/engine/src/main/java/com/arcadedb/database/LocalDatabase.java b/engine/src/main/java/com/arcadedb/database/LocalDatabase.java index 29f4616452..e4984f3eab 100644 --- a/engine/src/main/java/com/arcadedb/database/LocalDatabase.java +++ b/engine/src/main/java/com/arcadedb/database/LocalDatabase.java @@ -65,10 +65,10 @@ import com.arcadedb.query.opencypher.query.CypherPlanCache; import com.arcadedb.query.opencypher.query.CypherStatementCache; import com.arcadedb.query.select.Select; +import com.arcadedb.query.sql.SQLQueryEngine; import com.arcadedb.query.sql.executor.ResultSet; import com.arcadedb.query.sql.parser.ExecutionPlanCache; import com.arcadedb.query.sql.parser.StatementCache; -import com.arcadedb.query.sql.SQLQueryEngine; import com.arcadedb.schema.DocumentType; import com.arcadedb.schema.EdgeType; import com.arcadedb.schema.LocalDocumentType; @@ -85,15 +85,35 @@ import com.arcadedb.utility.MultiIterator; import com.arcadedb.utility.RWLockContext; -import java.io.*; -import java.nio.channels.*; -import java.nio.file.*; -import java.util.*; -import java.util.concurrent.*; -import java.util.concurrent.atomic.*; -import java.util.concurrent.locks.*; -import java.util.logging.*; -import java.util.stream.*; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.io.UncheckedIOException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import java.util.stream.Stream; /** * Local implementation of {@link Database}. It is based on files opened on the local file system. @@ -137,8 +157,8 @@ public class LocalDatabase extends RWLockContext implements DatabaseInternal { private final Map>> callbacks; private final StatementCache statementCache; private final ExecutionPlanCache executionPlanCache; - private final CypherStatementCache cypherStatementCache; - private final CypherPlanCache cypherPlanCache; + private final CypherStatementCache cypherStatementCache; + private final CypherPlanCache cypherPlanCache; private final File configurationFile; private DatabaseInternal wrappedDatabaseInstance = this; private int edgeListSize = EDGE_LIST_INITIAL_CHUNK_SIZE; diff --git a/engine/src/main/java/com/arcadedb/query/QueryEngineManager.java b/engine/src/main/java/com/arcadedb/query/QueryEngineManager.java index 5ed903e5eb..3ff8ac4378 100644 --- a/engine/src/main/java/com/arcadedb/query/QueryEngineManager.java +++ b/engine/src/main/java/com/arcadedb/query/QueryEngineManager.java @@ -49,11 +49,13 @@ public QueryEngineManager() { register("com.arcadedb.redis.query.RedisQueryEngineFactory"); } + public void register(final String className) { try { + register((QueryEngine.QueryEngineFactory) Class.forName(className).getConstructor().newInstance()); } catch (final Exception e) { - LogManager.instance().log(this, Level.FINE, "Unable to register engine '%s' (%s)", className, e.getMessage()); + LogManager.instance().log(this, Level.WARNING, "Unable to register engine '%s' (%s)", className, e.getMessage()); } } diff --git a/package/pom.xml b/package/pom.xml index e24d1a1c6a..1aad442dde 100644 --- a/package/pom.xml +++ b/package/pom.xml @@ -187,6 +187,18 @@ + + com.arcadedb + arcadedb-bolt + ${project.parent.version} + shaded + + + * + * + + + com.arcadedb arcadedb-redisw diff --git a/package/src/main/assembly/full.xml b/package/src/main/assembly/full.xml index e19dc4aa78..455a9df77c 100644 --- a/package/src/main/assembly/full.xml +++ b/package/src/main/assembly/full.xml @@ -111,7 +111,6 @@ com.arcadedb:arcadedb-redisw com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-graphql - com.arcadedb:arcadedb-studio com.arcadedb:arcadedb-metrics @@ -126,7 +125,6 @@ com.arcadedb:arcadedb-redisw com.arcadedb:arcadedb-grpcw com.arcadedb:arcadedb-graphql - com.arcadedb:arcadedb-studio com.arcadedb:arcadedb-metrics false diff --git a/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java b/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java index ffb8a1b6f5..3703f2b732 100644 --- a/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java +++ b/server/src/main/java/com/arcadedb/server/ArcadeDBServer.java @@ -192,7 +192,6 @@ public synchronized void start() { haServer.startService(); } -// registerPlugins(ServerPlugin.PluginInstallationPriority.AFTER_HTTP_ON); pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.AFTER_HTTP_ON); loadDefaultDatabases(); @@ -200,7 +199,6 @@ public synchronized void start() { // RELOAD DATABASE IF A PLUGIN REGISTERED A NEW DATABASE (LIKE THE GREMLIN SERVER) loadDatabases(); -// registerPlugins(ServerPlugin.PluginInstallationPriority.AFTER_DATABASES_OPEN); pluginManager.startPlugins(ServerPlugin.PluginInstallationPriority.AFTER_DATABASES_OPEN); status = STATUS.ONLINE; From b01edadbae773282d553e40fb0552ade25f74b42 Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Thu, 29 Jan 2026 22:16:12 +0100 Subject: [PATCH 11/14] plugins on main classloader waiting for a solutions of query engine --- package/src/main/assembly/full.xml | 4 +++- package/src/main/assembly/headless.xml | 4 +++- package/src/main/assembly/minimal.xml | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/package/src/main/assembly/full.xml b/package/src/main/assembly/full.xml index 455a9df77c..84a6918346 100644 --- a/package/src/main/assembly/full.xml +++ b/package/src/main/assembly/full.xml @@ -116,7 +116,9 @@ - lib/plugins + + lib + com.arcadedb:arcadedb-gremlin com.arcadedb:arcadedb-bolt diff --git a/package/src/main/assembly/headless.xml b/package/src/main/assembly/headless.xml index 204e98ea86..bd9410f607 100644 --- a/package/src/main/assembly/headless.xml +++ b/package/src/main/assembly/headless.xml @@ -116,7 +116,9 @@ - lib/plugins + + lib + com.arcadedb:arcadedb-postgresw diff --git a/package/src/main/assembly/minimal.xml b/package/src/main/assembly/minimal.xml index 40d175fe74..43c66dba22 100644 --- a/package/src/main/assembly/minimal.xml +++ b/package/src/main/assembly/minimal.xml @@ -115,7 +115,9 @@ - lib/plugins + + lib + com.arcadedb:arcadedb-postgresw From 18e956b612c67ee86eca9153c2c3d600388057f9 Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Fri, 30 Jan 2026 09:29:10 +0100 Subject: [PATCH 12/14] comment plugin section --- package/arcadedb-builder.sh | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/package/arcadedb-builder.sh b/package/arcadedb-builder.sh index 7aa5d53e26..57b3a6dac4 100755 --- a/package/arcadedb-builder.sh +++ b/package/arcadedb-builder.sh @@ -804,17 +804,17 @@ download_optional_modules() { local extracted_dir="$TEMP_DIR/arcadedb-${ARCADEDB_VERSION}" local lib_dir="${extracted_dir}/lib" - local plugins_dir="${extracted_dir}/lib/plugins" - - # Create plugins directory if it doesn't exist - if [[ "$DRY_RUN" != true ]]; then - if [[ ! -d "$plugins_dir" ]]; then - mkdir -p "$plugins_dir" - log_verbose "Created plugins directory: $plugins_dir" - fi - else - log_info "[DRY RUN] Would create plugins directory" - fi +# local plugins_dir="${extracted_dir}/lib/plugins" +# +# # Create plugins directory if it doesn't exist +# if [[ "$DRY_RUN" != true ]]; then +# if [[ ! -d "$plugins_dir" ]]; then +# mkdir -p "$plugins_dir" +# log_verbose "Created plugins directory: $plugins_dir" +# fi +# else +# log_info "[DRY RUN] Would create plugins directory" +# fi # Split modules by comma IFS=',' read -ra modules <<<"$SELECTED_MODULES" @@ -824,16 +824,16 @@ download_optional_modules() { # Determine if shaded or regular JAR local classifier="" - local dest_dir="$lib_dir" +# local dest_dir="$lib_dir" if [[ " $SHADED_MODULES " =~ " $module " ]]; then classifier="-shaded" # Shaded modules are plugins and go to lib/plugins/ - dest_dir="$plugins_dir" +# dest_dir="$plugins_dir" fi local artifact_id="arcadedb-${module}" local jar_filename="${artifact_id}-${ARCADEDB_VERSION}${classifier}.jar" - local jar_file="${dest_dir}/${jar_filename}" +# local jar_file="${dest_dir}/${jar_filename}" if [[ -n "$LOCAL_REPO" ]]; then # Local repository mode From 66348ff59bcffe57c09db7f63b714328ea944a7b Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Fri, 30 Jan 2026 09:52:03 +0100 Subject: [PATCH 13/14] fix jar_file --- package/arcadedb-builder.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/arcadedb-builder.sh b/package/arcadedb-builder.sh index 57b3a6dac4..ee67bca141 100755 --- a/package/arcadedb-builder.sh +++ b/package/arcadedb-builder.sh @@ -833,7 +833,7 @@ download_optional_modules() { local artifact_id="arcadedb-${module}" local jar_filename="${artifact_id}-${ARCADEDB_VERSION}${classifier}.jar" -# local jar_file="${dest_dir}/${jar_filename}" + local jar_file="${dest_dir}/${jar_filename}" if [[ -n "$LOCAL_REPO" ]]; then # Local repository mode From be0929aefded9e1ca9983ece0133acad1da545ca Mon Sep 17 00:00:00 2001 From: Roberto Franchini Date: Fri, 30 Jan 2026 10:09:57 +0100 Subject: [PATCH 14/14] fix lib dir --- package/arcadedb-builder.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package/arcadedb-builder.sh b/package/arcadedb-builder.sh index ee67bca141..826e34a3b8 100755 --- a/package/arcadedb-builder.sh +++ b/package/arcadedb-builder.sh @@ -833,7 +833,9 @@ download_optional_modules() { local artifact_id="arcadedb-${module}" local jar_filename="${artifact_id}-${ARCADEDB_VERSION}${classifier}.jar" - local jar_file="${dest_dir}/${jar_filename}" +# local jar_file="${dest_dir}/${jar_filename}" + + local jar_file="${lib_dir}/${jar_filename}" if [[ -n "$LOCAL_REPO" ]]; then # Local repository mode