This is an automated email from the ASF dual-hosted git repository. colegreer pushed a commit to branch TINKERPOP-3107 in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit 80a6ca8834352128705d4ef607d8e66e4e859d05 Author: Cole Greer <[email protected]> AuthorDate: Mon Apr 13 09:50:30 2026 -0700 initial PoC --- gremlin-server/conf/gremlin-server-classic.yaml | 9 +- .../conf/gremlin-server-modern-readonly.yaml | 11 +- gremlin-server/conf/gremlin-server-modern.yaml | 10 +- .../conf/gremlin-server-rest-modern.yaml | 9 +- .../conf/gremlin-server-rest-secure.yaml | 8 -- gremlin-server/conf/gremlin-server-secure.yaml | 8 -- .../conf/gremlin-server-transaction.yaml | 7 -- gremlin-server/conf/gremlin-server.yaml | 7 -- .../tinkerpop/gremlin/server/GremlinServer.java | 4 +- .../apache/tinkerpop/gremlin/server/Settings.java | 58 ++++++++++ .../gremlin/server/util/LifeCycleHook.java | 29 +++++ .../gremlin/server/util/ServerGremlinExecutor.java | 125 +++++++++++++++++++-- .../server/util/TinkerFactoryDataLoader.java | 99 ++++++++++++++++ 13 files changed, 316 insertions(+), 68 deletions(-) diff --git a/gremlin-server/conf/gremlin-server-classic.yaml b/gremlin-server/conf/gremlin-server-classic.yaml index 37b51f5380..b421c3f276 100644 --- a/gremlin-server/conf/gremlin-server-classic.yaml +++ b/gremlin-server/conf/gremlin-server-classic.yaml @@ -20,13 +20,8 @@ port: 8182 evaluationTimeout: 30000 graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-classic.groovy]}}}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: classic}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 } # application/vnd.graphbinary-v1.0 diff --git a/gremlin-server/conf/gremlin-server-modern-readonly.yaml b/gremlin-server/conf/gremlin-server-modern-readonly.yaml index 3c6ad637b7..975caed215 100644 --- a/gremlin-server/conf/gremlin-server-modern-readonly.yaml +++ b/gremlin-server/conf/gremlin-server-modern-readonly.yaml @@ -20,13 +20,10 @@ port: 8182 evaluationTimeout: 30000 graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-modern-readonly.groovy]}}}} +traversalSources: { + g: {graph: graph, query: "g.withStrategies(ReadOnlyStrategy)"}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: classic}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 } # application/vnd.graphbinary-v1.0 diff --git a/gremlin-server/conf/gremlin-server-modern.yaml b/gremlin-server/conf/gremlin-server-modern.yaml index 4551023d80..4a1fe9803e 100644 --- a/gremlin-server/conf/gremlin-server-modern.yaml +++ b/gremlin-server/conf/gremlin-server-modern.yaml @@ -21,14 +21,8 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyCompilerGremlinPlugin: {enableThreadInterrupt: true}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-modern.groovy]}}}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: modern}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4} # application/vnd.graphbinary-v4.0 diff --git a/gremlin-server/conf/gremlin-server-rest-modern.yaml b/gremlin-server/conf/gremlin-server-rest-modern.yaml index 905a9ca002..3b66b58177 100644 --- a/gremlin-server/conf/gremlin-server-rest-modern.yaml +++ b/gremlin-server/conf/gremlin-server-rest-modern.yaml @@ -21,13 +21,8 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/generate-modern.groovy]}}}} +lifecycleHooks: + - { className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader, config: {graph: graph, dataset: modern}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONUntypedMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/vnd.gremlin-v3.0+json;types=false - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/vnd.gremlin-v3.0+json diff --git a/gremlin-server/conf/gremlin-server-rest-secure.yaml b/gremlin-server/conf/gremlin-server-rest-secure.yaml index 8a62795351..642033b31d 100644 --- a/gremlin-server/conf/gremlin-server-rest-secure.yaml +++ b/gremlin-server/conf/gremlin-server-rest-secure.yaml @@ -29,14 +29,6 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyCompilerGremlinPlugin: {enableThreadInterrupt: true, timedInterrupt: 10000, compilation: COMPILE_STATIC, extensions: org.apache.tinkerpop.gremlin.groovy.jsr223.customizer.SimpleSandboxExtension}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/empty-sample-secure.groovy]}}}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json metrics: { diff --git a/gremlin-server/conf/gremlin-server-secure.yaml b/gremlin-server/conf/gremlin-server-secure.yaml index ddc57c0d85..644b275f70 100644 --- a/gremlin-server/conf/gremlin-server-secure.yaml +++ b/gremlin-server/conf/gremlin-server-secure.yaml @@ -29,14 +29,6 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyCompilerGremlinPlugin: {enableThreadInterrupt: true, timedInterrupt: 10000, compilation: COMPILE_STATIC, extensions: org.apache.tinkerpop.gremlin.groovy.jsr223.customizer.SimpleSandboxExtension}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/empty-sample-secure.groovy]}}}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 } # application/vnd.graphbinary-v1.0 diff --git a/gremlin-server/conf/gremlin-server-transaction.yaml b/gremlin-server/conf/gremlin-server-transaction.yaml index e4cc804ac2..6b61f40a25 100644 --- a/gremlin-server/conf/gremlin-server-transaction.yaml +++ b/gremlin-server/conf/gremlin-server-transaction.yaml @@ -21,13 +21,6 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkertransactiongraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/empty-sample.groovy]}}}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV3, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV1 } # application/vnd.graphbinary-v1.0 diff --git a/gremlin-server/conf/gremlin-server.yaml b/gremlin-server/conf/gremlin-server.yaml index 286f7f18ea..163d807849 100644 --- a/gremlin-server/conf/gremlin-server.yaml +++ b/gremlin-server/conf/gremlin-server.yaml @@ -21,13 +21,6 @@ evaluationTimeout: 30000 channelizer: org.apache.tinkerpop.gremlin.server.channel.HttpChannelizer graphs: { graph: conf/tinkergraph-empty.properties} -scriptEngines: { - gremlin-lang: {}, - gremlin-groovy: { - plugins: { org.apache.tinkerpop.gremlin.server.jsr223.GremlinServerGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.tinkergraph.jsr223.TinkerGraphGremlinPlugin: {}, - org.apache.tinkerpop.gremlin.jsr223.ImportGremlinPlugin: {classImports: [java.lang.Math], methodImports: [java.lang.Math#*]}, - org.apache.tinkerpop.gremlin.jsr223.ScriptFileGremlinPlugin: {files: [scripts/empty-sample.groovy]}}}} serializers: - { className: org.apache.tinkerpop.gremlin.util.ser.GraphSONMessageSerializerV4, config: { ioRegistries: [org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerIoRegistryV3] }} # application/json - { className: org.apache.tinkerpop.gremlin.util.ser.GraphBinaryMessageSerializerV4 } # application/vnd.graphbinary-v1.0 diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/GremlinServer.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/GremlinServer.java index 16d8a2d045..a0b68e5e3a 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/GremlinServer.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/GremlinServer.java @@ -159,7 +159,7 @@ public class GremlinServer { serverGremlinExecutor.getHooks().forEach(hook -> { logger.info("Executing start up {}", LifeCycleHook.class.getSimpleName()); try { - hook.onStartUp(new LifeCycleHook.Context(logger)); + hook.onStartUp(new LifeCycleHook.Context(logger, serverGremlinExecutor.getGraphManager())); } catch (UnsupportedOperationException uoe) { // if the user doesn't implement onStartUp the scriptengine will throw // this exception. it can safely be ignored. @@ -269,7 +269,7 @@ public class GremlinServer { serverGremlinExecutor.getHooks().forEach(hook -> { logger.info("Executing shutdown {}", LifeCycleHook.class.getSimpleName()); try { - hook.onShutDown(new LifeCycleHook.Context(logger)); + hook.onShutDown(new LifeCycleHook.Context(logger, serverGremlinExecutor.getGraphManager())); } catch (UnsupportedOperationException | UndeclaredThrowableException uoe) { // if the user doesn't implement onShutDown the scriptengine will throw // this exception. it can safely be ignored. diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java index 3489bd511e..5805c649f1 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/Settings.java @@ -244,6 +244,18 @@ public class Settings { */ public Map<String, ScriptEngineSettings> scriptEngines; + /** + * {@link Map} of {@link TraversalSource} configurations keyed by the binding name. Each entry specifies a + * graph reference and optionally a Gremlin query to configure strategies. + */ + public Map<String, TraversalSourceSettings> traversalSources = new LinkedHashMap<>(); + + /** + * List of {@link LifeCycleHook} implementations to instantiate via reflection and execute during server + * initialization and shutdown. + */ + public List<LifeCycleHookSettings> lifecycleHooks = new ArrayList<>(); + /** * List of {@link MessageSerializer} to configure. If no serializers are specified then default serializers for * the most current versions of "application/json" and "application/vnd.gremlin-v1.0+gryo" are applied. @@ -300,6 +312,8 @@ public class Settings { settingsDescription.addPropertyParameters("graphs", String.class, String.class); settingsDescription.addPropertyParameters("scriptEngines", String.class, ScriptEngineSettings.class); settingsDescription.addPropertyParameters("serializers", SerializerSettings.class); + settingsDescription.addPropertyParameters("traversalSources", String.class, TraversalSourceSettings.class); + settingsDescription.addPropertyParameters("lifecycleHooks", LifeCycleHookSettings.class); constructor.addTypeDescription(settingsDescription); final TypeDescription serializerSettingsDescription = new TypeDescription(SerializerSettings.class); @@ -314,6 +328,13 @@ public class Settings { scriptEngineSettingsDescription.addPropertyParameters("plugins", String.class, Object.class); constructor.addTypeDescription(scriptEngineSettingsDescription); + final TypeDescription traversalSourceSettingsDescription = new TypeDescription(TraversalSourceSettings.class); + constructor.addTypeDescription(traversalSourceSettingsDescription); + + final TypeDescription lifeCycleHookSettingsDescription = new TypeDescription(LifeCycleHookSettings.class); + lifeCycleHookSettingsDescription.addPropertyParameters("config", String.class, Object.class); + constructor.addTypeDescription(lifeCycleHookSettingsDescription); + final TypeDescription sslSettings = new TypeDescription(SslSettings.class); constructor.addTypeDescription(sslSettings); @@ -393,6 +414,43 @@ public class Settings { public Map<String,Map<String,Object>> plugins = new LinkedHashMap<>(); } + /** + * Settings for a declarative {@link TraversalSource} entry in the server YAML. + */ + public static class TraversalSourceSettings { + /** + * The name of the graph (as defined in {@code graphs}) to create the traversal source from. + */ + public String graph; + + /** + * An optional Gremlin query evaluated with a base traversal source bound as {@code g}. + * The result of the query becomes the final {@link TraversalSource}. + */ + public String query = null; + + /** + * The script engine language to use for evaluating {@link #query}. If not specified, resolution + * falls back to the single configured engine or {@code gremlin-lang}. + */ + public String language = null; + } + + /** + * Settings for a Java-based {@link LifeCycleHook} configured in the server YAML. + */ + public static class LifeCycleHookSettings { + /** + * The fully qualified class name of the {@link LifeCycleHook} implementation. + */ + public String className; + + /** + * Optional configuration passed to {@link LifeCycleHook#init(java.util.Map)}. + */ + public Map<String, Object> config = null; + } + /** * Settings for the {@link MessageSerializer} implementations. */ diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/LifeCycleHook.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/LifeCycleHook.java index 95884d8d7f..80696686b8 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/LifeCycleHook.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/LifeCycleHook.java @@ -18,8 +18,11 @@ */ package org.apache.tinkerpop.gremlin.server.util; +import org.apache.tinkerpop.gremlin.server.GraphManager; import org.slf4j.Logger; +import java.util.Map; + /** * Provides a method in which users can hook into the startup and shutdown lifecycle of Gremlin Server. Creating * an instance of this interface in a Gremlin Server initialization script enables the ability to get a callback @@ -29,6 +32,15 @@ import org.slf4j.Logger; */ public interface LifeCycleHook { + /** + * Called once after instantiation to pass configuration from the {@code lifecycleHooks} YAML section. + * Implementations that require configuration should override this method. The default implementation + * is a no-op so that existing hooks are not forced to implement it. + * + * @param config the key/value pairs from the {@code config} block in the YAML entry + */ + public default void init(final Map<String, Object> config) {} + /** * Called when the server starts up. The graph collection will have been initialized at this point * and all initialization scripts will have been executed when this callback is called. @@ -45,13 +57,30 @@ public interface LifeCycleHook { */ public static class Context { private final Logger logger; + private final GraphManager graphManager; + /** + * @deprecated As of release 4.0.0, replaced by {@link #Context(Logger, GraphManager)}. + */ + @Deprecated public Context(final Logger logger) { + this(logger, null); + } + + public Context(final Logger logger, final GraphManager graphManager) { this.logger = logger; + this.graphManager = graphManager; } public Logger getLogger() { return logger; } + + /** + * Gets the {@link GraphManager} which provides access to all configured graphs and traversal sources. + */ + public GraphManager getGraphManager() { + return graphManager; + } } } diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java index dce5fb931a..f3970b7a92 100644 --- a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/ServerGremlinExecutor.java @@ -36,9 +36,12 @@ import org.slf4j.LoggerFactory; import javax.script.SimpleBindings; import java.lang.reflect.Constructor; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; @@ -186,20 +189,110 @@ public class ServerGremlinExecutor { .forEach(kv -> this.graphManager.putGraph(kv.getKey(), (Graph) kv.getValue())); // script engine init may have constructed the TraversalSource bindings - store them in Graphs object - gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() - .filter(kv -> kv.getValue() instanceof TraversalSource) - .forEach(kv -> { - logger.info("A {} is now bound to [{}] with {}", kv.getValue().getClass().getSimpleName(), kv.getKey(), kv.getValue()); - this.graphManager.putTraversalSource(kv.getKey(), (TraversalSource) kv.getValue()); - }); + final boolean hasScriptTraversalSources = gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() + .anyMatch(kv -> kv.getValue() instanceof TraversalSource); + if (hasScriptTraversalSources) { + logger.warn("TraversalSource instances were created via script engine initialization scripts. " + + "This approach is deprecated - use the 'traversalSources' YAML configuration instead."); + gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() + .filter(kv -> kv.getValue() instanceof TraversalSource) + .forEach(kv -> { + logger.info("A {} is now bound to [{}] with {}", kv.getValue().getClass().getSimpleName(), kv.getKey(), kv.getValue()); + this.graphManager.putTraversalSource(kv.getKey(), (TraversalSource) kv.getValue()); + }); + } // determine if the initialization scripts introduced LifeCycleHook objects - if so we need to gather them // up for execution - hooks = gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() + final boolean hasScriptHooks = gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() + .anyMatch(kv -> kv.getValue() instanceof LifeCycleHook); + if (hasScriptHooks) { + logger.warn("LifeCycleHook instances were created via script engine initialization scripts. " + + "This approach is deprecated - use the 'lifecycleHooks' YAML configuration instead."); + } + final List<LifeCycleHook> scriptHooks = gremlinExecutor.getScriptEngineManager().getBindings().entrySet().stream() .filter(kv -> kv.getValue() instanceof LifeCycleHook) .map(kv -> (LifeCycleHook) kv.getValue()) .collect(Collectors.toList()); + // process declarative traversalSources from YAML config - track which graphs have explicit entries + final Set<String> graphsWithExplicitTraversalSources = new HashSet<>(); + if (settings.traversalSources != null && !settings.traversalSources.isEmpty()) { + settings.traversalSources.forEach((tsName, tsSettings) -> { + final Graph graph = this.graphManager.getGraph(tsSettings.graph); + if (null == graph) { + logger.warn("Could not create TraversalSource [{}] - graph [{}] not found in graphs configuration", + tsName, tsSettings.graph); + return; + } + + graphsWithExplicitTraversalSources.add(tsSettings.graph); + + if (tsSettings.query != null && !tsSettings.query.isEmpty()) { + // resolve which script engine to use for the query + final String language = resolveLanguage(tsSettings.language); + try { + // bind a base traversal source as 'g' for the query to operate on + final SimpleBindings bindings = new SimpleBindings(); + bindings.put("g", graph.traversal()); + final GremlinExecutor.LifeCycle lifeCycle = GremlinExecutor.LifeCycle.build() + .evaluationTimeoutOverride(0L).create(); + final TraversalSource ts = (TraversalSource) gremlinExecutor.eval( + tsSettings.query, language, bindings, lifeCycle).join(); + this.graphManager.putTraversalSource(tsName, ts); + logger.info("A {} is now bound to [{}] via query", ts.getClass().getSimpleName(), tsName); + } catch (Exception ex) { + logger.warn(String.format("Could not create TraversalSource [%s] from query - %s", + tsName, ex.getMessage()), ex); + } + } else { + final TraversalSource ts = graph.traversal(); + this.graphManager.putTraversalSource(tsName, ts); + logger.info("A {} is now bound to [{}]", ts.getClass().getSimpleName(), tsName); + } + }); + } + + // auto-create TraversalSources for graphs that don't have explicit traversalSources entries and + // were not already bound by script engine initialization + for (final String graphName : this.graphManager.getGraphNames()) { + if (graphsWithExplicitTraversalSources.contains(graphName)) + continue; + + final String tsName = graphName.equals("graph") ? "g" : "g_" + graphName; + if (this.graphManager.getTraversalSource(tsName) != null) + continue; + + final Graph graph = this.graphManager.getGraph(graphName); + final TraversalSource ts = graph.traversal(); + this.graphManager.putTraversalSource(tsName, ts); + logger.info("A {} is now auto-bound to [{}] for graph [{}]", + ts.getClass().getSimpleName(), tsName, graphName); + } + + // instantiate Java-based lifecycle hooks from YAML config + final List<LifeCycleHook> yamlHooks = new ArrayList<>(); + if (settings.lifecycleHooks != null) { + for (final Settings.LifeCycleHookSettings hookSettings : settings.lifecycleHooks) { + try { + final Class<?> clazz = Class.forName(hookSettings.className); + final LifeCycleHook hook = (LifeCycleHook) clazz.getDeclaredConstructor().newInstance(); + if (hookSettings.config != null) { + hook.init(hookSettings.config); + } + yamlHooks.add(hook); + logger.info("Instantiated LifeCycleHook: {}", hookSettings.className); + } catch (Exception ex) { + logger.error(String.format("Could not instantiate LifeCycleHook %s", hookSettings.className), ex); + } + } + } + + // combine YAML-configured hooks with any script-produced hooks (YAML hooks execute first) + final List<LifeCycleHook> allHooks = new ArrayList<>(yamlHooks); + allHooks.addAll(scriptHooks); + hooks = allHooks; + transactionManager = new TransactionManager( scheduledExecutorService, graphManager, @@ -214,6 +307,24 @@ public class ServerGremlinExecutor { MetricManager.INSTANCE.registerGremlinScriptEngineMetrics(engine, engineName, "sessionless", "class-cache"); } + /** + * Resolves the script engine language to use for evaluating a traversal source query. If an explicit language + * is provided, it is used directly. Otherwise, if only one non-{@code gremlin-lang} script engine is configured, + * that engine is used. Falls back to {@code gremlin-lang}. + */ + private String resolveLanguage(final String explicitLanguage) { + if (explicitLanguage != null && !explicitLanguage.isEmpty()) + return explicitLanguage; + + final List<String> nonLangEngines = settings.scriptEngines.keySet().stream() + .filter(name -> !name.equals("gremlin-lang")) + .collect(Collectors.toList()); + if (nonLangEngines.size() == 1) + return nonLangEngines.get(0); + + return "gremlin-lang"; + } + public void addHostOption(final String key, final Object value) { hostOptions.put(key, value); } diff --git a/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/TinkerFactoryDataLoader.java b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/TinkerFactoryDataLoader.java new file mode 100644 index 0000000000..5cc87f8835 --- /dev/null +++ b/gremlin-server/src/main/java/org/apache/tinkerpop/gremlin/server/util/TinkerFactoryDataLoader.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.tinkerpop.gremlin.server.util; + +import org.apache.tinkerpop.gremlin.structure.Graph; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.AbstractTinkerGraph; +import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory; + +import java.util.Map; + +/** + * A {@link LifeCycleHook} that loads TinkerFactory sample datasets into a graph during server startup. + * Replaces the Groovy init scripts (e.g. generate-modern.groovy) for default server configurations. + * + * <p>Configuration parameters (via {@code config} in YAML): + * <ul> + * <li>{@code graph} — name of the graph (as defined in {@code graphs:} config) to load data into</li> + * <li>{@code dataset} — which dataset to load: modern, classic, crew, grateful, sink</li> + * </ul> + * + * <p>Example YAML usage: + * <pre> + * lifecycleHooks: + * - className: org.apache.tinkerpop.gremlin.server.util.TinkerFactoryDataLoader + * config: {graph: graph, dataset: modern} + * </pre> + */ +public class TinkerFactoryDataLoader implements LifeCycleHook { + + private String graphName; + private String dataset; + + @Override + public void init(final Map<String, Object> config) { + graphName = (String) config.get("graph"); + dataset = (String) config.get("dataset"); + if (graphName == null || dataset == null) { + throw new IllegalArgumentException("TinkerFactoryDataLoader requires 'graph' and 'dataset' config parameters"); + } + } + + @Override + public void onStartUp(final Context c) { + final Graph graph = c.getGraphManager().getGraph(graphName); + if (null == graph) { + c.getLogger().warn("TinkerFactoryDataLoader could not find graph [{}]", graphName); + return; + } + if (!(graph instanceof AbstractTinkerGraph)) { + c.getLogger().warn("TinkerFactoryDataLoader requires an AbstractTinkerGraph but [{}] is {}", + graphName, graph.getClass().getName()); + return; + } + + final AbstractTinkerGraph tinkerGraph = (AbstractTinkerGraph) graph; + switch (dataset) { + case "modern": + TinkerFactory.generateModern(tinkerGraph); + break; + case "classic": + TinkerFactory.generateClassic(tinkerGraph); + break; + case "crew": + TinkerFactory.generateTheCrew(tinkerGraph); + break; + case "grateful": + TinkerFactory.generateGratefulDead(tinkerGraph); + break; + case "sink": + TinkerFactory.generateKitchenSink(tinkerGraph); + break; + default: + c.getLogger().warn("TinkerFactoryDataLoader unknown dataset [{}]", dataset); + return; + } + c.getLogger().info("TinkerFactoryDataLoader loaded [{}] dataset into graph [{}]", dataset, graphName); + } + + @Override + public void onShutDown(final Context c) { + // nothing to do on shutdown + } +}
