You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by an...@apache.org on 2021/10/11 14:05:07 UTC

[jena] branch main updated: JENA-2182: Fuseki Modules

This is an automated email from the ASF dual-hosted git repository.

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git


The following commit(s) were added to refs/heads/main by this push:
     new 3e49b31  JENA-2182: Fuseki Modules
     new bd1289b  Merge pull request #1084 from afs/fuseki-modules
3e49b31 is described below

commit 3e49b3126f3fa5a0f7d9e6c4a2d5cc8a4ee6c9f8
Author: Andy Seaborne <an...@apache.org>
AuthorDate: Thu Oct 7 13:07:06 2021 +0100

    JENA-2182: Fuseki Modules
---
 .../module/SubsystemRegistryServiceLoader.java     |  1 -
 .../main/java/org/apache/jena/sys/JenaSystem.java  |  5 +-
 .../org/apache/jena/fuseki/main/FusekiServer.java  | 23 ++++--
 .../apache/jena/fuseki/main/sys/FusekiModule.java  | 75 +++++++++++++++++++
 .../apache/jena/fuseki/main/sys/FusekiModules.java | 68 ++++++++++++++++++
 .../apache/jena/fuseki/main/sys/FusekiMonitor.java | 58 +++++++++++++++
 .../apache/jena/fuseki/main/sys/InitFuseki.java    | 61 ++++++++++++++++
 .../org.apache.jena.sys.JenaSubsystemLifecycle     |  1 +
 .../org/apache/jena/fuseki/main/ModuleForTest.java | 65 +++++++++++++++++
 .../org/apache/jena/fuseki/main/TS_FusekiMain.java |  4 +-
 .../apache/jena/fuseki/main/TestFusekiModules.java | 84 ++++++++++++++++++++++
 .../org.apache.jena.fuseki.main.sys.FusekiModule   |  1 +
 12 files changed, 436 insertions(+), 10 deletions(-)

diff --git a/jena-base/src/main/java/org/apache/jena/base/module/SubsystemRegistryServiceLoader.java b/jena-base/src/main/java/org/apache/jena/base/module/SubsystemRegistryServiceLoader.java
index f1a0ea4..764a015 100644
--- a/jena-base/src/main/java/org/apache/jena/base/module/SubsystemRegistryServiceLoader.java
+++ b/jena-base/src/main/java/org/apache/jena/base/module/SubsystemRegistryServiceLoader.java
@@ -45,7 +45,6 @@ public class SubsystemRegistryServiceLoader<T extends SubsystemLifecycle> implem
             ServiceLoader<T> sl =
                     // Use this->classloader form : better for OSGi
                     ServiceLoader.load(moduleClass, this.getClass().getClassLoader());
-            // Must also be T
             sl.forEach(this::add);
         }
     }
diff --git a/jena-core/src/main/java/org/apache/jena/sys/JenaSystem.java b/jena-core/src/main/java/org/apache/jena/sys/JenaSystem.java
index 86a754a..4b5cf0e 100644
--- a/jena-core/src/main/java/org/apache/jena/sys/JenaSystem.java
+++ b/jena-core/src/main/java/org/apache/jena/sys/JenaSystem.java
@@ -35,8 +35,9 @@ import org.apache.jena.base.module.SubsystemRegistryServiceLoader;
  * <li>20 - RIOT
  * <li>30 - ARQ
  * <li>40 - TDB
- * <li>50-99 - Other Jena system modules.
- * <li>100-9998 - Application
+ * <li>50-100 - Other Jena system modules.
+ * <li>101 - Fuseki
+ * <li>102-9998 - Application
  * <li>9999 - other
  * </ul>
  * See also the <a href="http://jena.apache.org/documentation/notes/system-initialization.html">notes on Jena initialization</a>.
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java
index ba202cf..43222fb 100644
--- a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/FusekiServer.java
@@ -50,6 +50,7 @@ import org.apache.jena.fuseki.ctl.*;
 import org.apache.jena.fuseki.jetty.FusekiErrorHandler;
 import org.apache.jena.fuseki.jetty.JettyLib;
 import org.apache.jena.fuseki.main.cmds.FusekiMain;
+import org.apache.jena.fuseki.main.sys.FusekiMonitor;
 import org.apache.jena.fuseki.metrics.MetricsProviderRegistry;
 import org.apache.jena.fuseki.server.*;
 import org.apache.jena.fuseki.servlets.*;
@@ -273,7 +274,10 @@ public class FusekiServer {
      * To synchronise with the server stopping, call {@link #join}.
      */
     public FusekiServer start() {
-        try { server.start(); }
+        try {
+            FusekiMonitor.serverStarting(this);
+            server.start();
+        }
         catch (IOException ex) {
             if ( ex.getCause() instanceof java.security.UnrecoverableKeyException )
                 // Unbundle for clearer message.
@@ -333,8 +337,10 @@ public class FusekiServer {
     /** Stop the server. */
     public void stop() {
         Fuseki.serverLog.info("Stop Fuseki");
-        try { server.stop(); }
-        catch (Exception e) { throw new FusekiException(e); }
+        try {
+            server.stop();
+            FusekiMonitor.serverStopped(this);
+        } catch (Exception e) { throw new FusekiException(e); }
     }
 
     /** Wait for the server to exit. This call is blocking. */
@@ -372,7 +378,6 @@ public class FusekiServer {
         // DataServices provided from the caller. These are immutable.
         private Registry<String, DataService> providedDataServices = new Registry<>();
 
-
         private final OperationRegistry  operationRegistry;
         // Default values.
         private int                      serverHttpPort     = PortUnset;
@@ -389,7 +394,7 @@ public class FusekiServer {
         private boolean                  withTasks          = false;
 
         private String                   jettyServerConfig  = null;
-
+        private Model                    configModel        = null;
         private Map<String, String>      corsInitParams     = null;
 
         // Server wide authorization policy.
@@ -662,6 +667,7 @@ public class FusekiServer {
             List<DataAccessPoint> x = FusekiConfig.processServerConfiguration(model, Fuseki.getContext());
             // Can further modify the services in the configuration file.
             x.forEach(dap->addDataAccessPoint(dap));
+            configModel = model;
             return this;
         }
 
@@ -1045,6 +1051,8 @@ public class FusekiServer {
 
             DataAccessPointRegistry dapRegistry = buildStart();
 
+            FusekiMonitor.configuration(this, dapRegistry, configModel);
+
             buildSecurity(dapRegistry);
             try {
                 validate();
@@ -1070,7 +1078,10 @@ public class FusekiServer {
                 }
                 if ( networkLoopback )
                     applyLocalhost(server);
-                return new FusekiServer(httpPort, httpsPort, server, staticContentDir, handler.getServletContext());
+
+                FusekiServer fusekiServer = new FusekiServer(httpPort, httpsPort, server, staticContentDir, handler.getServletContext());
+                FusekiMonitor.server(fusekiServer);
+                return fusekiServer;
             } finally {
                 buildFinish();
             }
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModule.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModule.java
new file mode 100644
index 0000000..842dc2a
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModule.java
@@ -0,0 +1,75 @@
+/*
+ * 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.jena.fuseki.main.sys;
+
+import java.util.UUID;
+
+import org.apache.jena.base.module.SubsystemLifecycle;
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.server.DataAccessPointRegistry;
+import org.apache.jena.rdf.model.Model;
+
+/**
+ * Module interface for Fuseki.
+ * <p>
+ * A module is additional code, usually in a separate jar, but can also be part of the application code.
+ */
+public interface FusekiModule extends SubsystemLifecycle {
+    /**
+     * Unique (within this server) name to identify this module.
+     * The default is to generate an UUID.
+     */
+    public default String name() { return UUID.randomUUID().toString(); }
+
+    /** Module loaded */
+    @Override
+    public default void start() {}
+
+    // ---- Build cycle
+
+    /**
+     * Call at the start of "build" step.
+     * The builder has been set according to the configuration.
+     * The "configModel" parameter is set if a configuration file was used else it is null.
+     */
+    public default void configuration(FusekiServer.Builder builder, DataAccessPointRegistry dapRegistry, Model configModel) {}
+
+    /** Built, not started, about to be returned to the builder caller. */
+    public default void server(FusekiServer server) { }
+
+    /** Server starting - called just before server.start happens. */
+    public default void serverStarting(FusekiServer server) { }
+
+    /** Server stopping.
+     * Do not rely on this to clear up external resources.
+     * Usually there is no stop phase and the JVM just exits or is killed externally.
+     *
+     */
+    public default void serverStopped(FusekiServer server) { }
+
+    /** Module unloaded */
+    @Override
+    public default void stop() {}
+
+    // Maybe later.
+//    // ---- Execution.
+//
+//    public default void action(HttpAction action) { }
+//    public default void action(Endpoint endpoint) { }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModules.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModules.java
new file mode 100644
index 0000000..8b3a8e4
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiModules.java
@@ -0,0 +1,68 @@
+/*
+ * 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.jena.fuseki.main.sys;
+
+import java.util.function.BiConsumer;
+
+import org.apache.jena.atlas.lib.Registry;
+import org.apache.jena.base.module.Subsystem;
+
+/** Registry of modules */
+public class FusekiModules {
+
+    private static Registry<String, FusekiModule> registry = null;
+
+    private static Subsystem<FusekiModule> subsystem = null;
+
+    public static void load() {
+        if ( registry == null )
+            reload();
+    }
+
+    public static void reload() {
+        registry = new Registry<>();
+        subsystem = new Subsystem<FusekiModule>(FusekiModule.class);
+        subsystem.initialize();
+        subsystem.forEach(FusekiModules::add);
+    }
+
+    /** Add a code module */
+    public static void add(FusekiModule module) {
+        registry.put(module.name(), module);
+    }
+
+    /** Remove a code module */
+    public static void remove(FusekiModule module) {
+        registry.remove(module.name());
+        module.stop();
+    }
+
+    /** Test whether a code module is registered. */
+    public static boolean contains(FusekiModule module) {
+        return registry.isRegistered(module.name());
+    }
+
+    /*package*/ static void forEachModule(BiConsumer<String, FusekiModule> action) {
+        if ( registry == null )
+            load();
+        if ( registry == null || registry.isEmpty() )
+            return ;
+        registry.forEach(action);
+    }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiMonitor.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiMonitor.java
new file mode 100644
index 0000000..62b7366
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/FusekiMonitor.java
@@ -0,0 +1,58 @@
+/*
+ * 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.jena.fuseki.main.sys;
+
+import org.apache.jena.fuseki.main.FusekiServer;
+import org.apache.jena.fuseki.server.DataAccessPointRegistry;
+import org.apache.jena.rdf.model.Model;
+
+public class FusekiMonitor {
+
+    /**
+     * Call at the start of "build" step.
+     * The builder has been set according to the configuration.
+     * The "configModel" parameter is set if a configuration file was used else it is null.
+     */
+    public static void configuration(FusekiServer.Builder builder, DataAccessPointRegistry dapRegistry, Model configModel) {
+        FusekiModules.forEachModule((n,m) -> m.configuration(builder, dapRegistry, configModel));
+    }
+
+    /**
+     * The outcome of the "build" step.
+     */
+    public static void server(FusekiServer server) {
+        FusekiModules.forEachModule((n,m) -> m.server(server));
+    }
+
+    /**
+     * Called just before {@code server.start()} called.
+     */
+    public static void serverStarting(FusekiServer server) {
+        FusekiModules.forEachModule((n,m) -> m.serverStarting(server));
+    }
+
+    /**
+     * Called just after {@code server.stop()} has happened.
+     * Often, this is not called - the server, or application containing the server,
+     * simply exits the JVM or is killed externally.
+     */
+    public static void serverStopped(FusekiServer server) {
+        FusekiModules.forEachModule((n,m) -> m.serverStopped(server));
+    }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/InitFuseki.java b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/InitFuseki.java
new file mode 100644
index 0000000..b8204ab
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/java/org/apache/jena/fuseki/main/sys/InitFuseki.java
@@ -0,0 +1,61 @@
+/*
+ * 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.jena.fuseki.main.sys;
+
+import org.apache.jena.sys.JenaSubsystemLifecycle;
+import org.apache.jena.sys.JenaSystem;
+
+/**
+ * Jena initialization.
+ * Level 101 - called during Jena system initialization
+ * and after Jena itself has initialized.
+ */
+public class InitFuseki implements JenaSubsystemLifecycle {
+
+    private static volatile boolean initialized = false;
+
+    @Override
+    public void start() { init(); }
+
+    @Override
+    public void stop() {}
+
+    @Override
+    public int level() { return 101; }
+
+    /**
+     * Load modules, call "start()".
+     * Each {@link FusekiModule} will be called during the server build process.
+     */
+    public static void init() {
+        if ( initialized ) {
+            return;
+        }
+        synchronized (InitFuseki.class) {
+            if ( initialized ) {
+                JenaSystem.logLifecycle("Fuseki.init - skip");
+                return;
+            }
+            initialized = true;
+            JenaSystem.logLifecycle("Fuseki.init - start");
+            FusekiModules.load();
+            JenaSystem.logLifecycle("Fuseki.init - finish");
+        }
+    }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/main/resources/META-INF/services/org.apache.jena.sys.JenaSubsystemLifecycle b/jena-fuseki2/jena-fuseki-main/src/main/resources/META-INF/services/org.apache.jena.sys.JenaSubsystemLifecycle
new file mode 100644
index 0000000..892450f
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/main/resources/META-INF/services/org.apache.jena.sys.JenaSubsystemLifecycle
@@ -0,0 +1 @@
+org.apache.jena.fuseki.main.sys.InitFuseki
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/ModuleForTest.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/ModuleForTest.java
new file mode 100644
index 0000000..47d5ec9
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/ModuleForTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.jena.fuseki.main;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.jena.fuseki.main.sys.FusekiModule;
+import org.apache.jena.fuseki.server.DataAccessPointRegistry;
+import org.apache.jena.rdf.model.Model;
+
+public class ModuleForTest implements FusekiModule {
+
+    public static ModuleForTest module = null;
+
+    public AtomicInteger countStart = new AtomicInteger(0);
+    public AtomicInteger countConfiguration = new AtomicInteger(0);
+    public AtomicInteger countServer = new AtomicInteger(0);
+    public AtomicInteger countServerStarting = new AtomicInteger(0);
+
+    public ModuleForTest() {
+        module = this;
+    }
+
+    public void clearLifecycle() {
+        countConfiguration.set(0);
+        countServer.set(0);
+        countServerStarting.set(0);
+    }
+
+    @Override
+    public void start() {
+        countStart.getAndIncrement();
+    }
+
+    @Override
+    public void configuration(FusekiServer.Builder builder, DataAccessPointRegistry dapRegistry, Model configModel) {
+        countConfiguration.getAndIncrement();
+    }
+
+    // Built, not started, about to be returned to the builder caller
+    @Override public void server(FusekiServer server) {
+        countServer.getAndIncrement();
+    }
+
+    // Server starting
+    @Override public void serverStarting(FusekiServer server) {
+        countServerStarting.getAndIncrement();
+    }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_FusekiMain.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_FusekiMain.java
index f4b8090..28efc20 100644
--- a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_FusekiMain.java
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TS_FusekiMain.java
@@ -24,9 +24,11 @@ import org.junit.runners.Suite.SuiteClasses;
 
 @RunWith(Suite.class)
 @SuiteClasses({
-    //17
 
   TestPlainServer.class
+
+  // This tests modules and modifies the system state.
+  , TestFusekiModules.class
   , TestEmbeddedFuseki.class
   , TestMultipleEmbedded.class
   , TestFusekiCustomOperation.class
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiModules.java b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiModules.java
new file mode 100644
index 0000000..9938f61
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/java/org/apache/jena/fuseki/main/TestFusekiModules.java
@@ -0,0 +1,84 @@
+/*
+ * 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.jena.fuseki.main;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.jena.fuseki.main.sys.FusekiModules;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class TestFusekiModules {
+
+    // Module loading.
+    // file :: src/test/resources/META-INF/services/org.apache.jena.fuseki.main.sys.FusekiModule
+    // Module: ModuleForTest
+
+    @BeforeClass public static void beforeClass() {
+        FusekiModules.load();
+    }
+
+    @Before public void beforeTest() {
+        ModuleForTest.module.clearLifecycle();
+    }
+
+    @AfterClass public static void afterTest() {
+        FusekiModules.remove(ModuleForTest.module);
+    }
+
+    @Test public void modules_0() {
+        ModuleForTest module = findModule();
+        module.clearLifecycle();
+        assertEquals(1, module.countStart.get());
+        assertEquals(0, module.countConfiguration.get());
+    }
+
+    @Test public void lifecycle_1() {
+        ModuleForTest module = findModule();
+
+        FusekiServer.Builder builder = FusekiServer.create().port(0);
+
+        assertEquals(1, module.countStart.get());
+        assertEquals(0, module.countConfiguration.get());
+        assertEquals(0, module.countServer.get());
+        assertEquals(0, module.countServerStarting.get());
+
+        FusekiServer server = builder.build();
+
+        assertEquals(1, module.countStart.get());
+        assertEquals(1, module.countConfiguration.get());
+        assertEquals(1, module.countServer.get());
+        assertEquals(0, module.countServerStarting.get());
+
+        server.start();
+
+        assertEquals(1, module.countStart.get());
+        assertEquals(1, module.countConfiguration.get());
+        assertEquals(1, module.countServer.get());
+        assertEquals(1, module.countServerStarting.get());
+
+        server.stop();
+    }
+
+    private ModuleForTest findModule() {
+        return ModuleForTest.module;
+    }
+}
diff --git a/jena-fuseki2/jena-fuseki-main/src/test/resources/META-INF/services/org.apache.jena.fuseki.main.sys.FusekiModule b/jena-fuseki2/jena-fuseki-main/src/test/resources/META-INF/services/org.apache.jena.fuseki.main.sys.FusekiModule
new file mode 100644
index 0000000..d40dfe8
--- /dev/null
+++ b/jena-fuseki2/jena-fuseki-main/src/test/resources/META-INF/services/org.apache.jena.fuseki.main.sys.FusekiModule
@@ -0,0 +1 @@
+org.apache.jena.fuseki.main.ModuleForTest