From 05dd8adf7797f5f16cb6f5f3d64311ed1c8d3a6e Mon Sep 17 00:00:00 2001 From: Keuin Date: Mon, 7 Feb 2022 19:00:30 +0800 Subject: Hot reload. --- .../java/com/keuin/crosslink/api/ApiServer.java | 3 +- .../crosslink/config/GlobalConfigManager.java | 73 +++++++++++--- .../com/keuin/crosslink/config/IConfigView.java | 5 - .../messaging/config/router/IRouterConfigurer.java | 1 + .../messaging/config/router/RouterConfigurer.java | 1 + .../crosslink/messaging/router/ConcreteRouter.java | 4 + .../messaging/router/IRouterConfigurable.java | 5 + .../keuin/crosslink/plugin/common/PluginMain.java | 111 +++++++++++---------- 8 files changed, 129 insertions(+), 74 deletions(-) delete mode 100644 src/main/java/com/keuin/crosslink/config/IConfigView.java (limited to 'src') diff --git a/src/main/java/com/keuin/crosslink/api/ApiServer.java b/src/main/java/com/keuin/crosslink/api/ApiServer.java index d7599a5..ebd1820 100644 --- a/src/main/java/com/keuin/crosslink/api/ApiServer.java +++ b/src/main/java/com/keuin/crosslink/api/ApiServer.java @@ -26,7 +26,7 @@ import java.util.stream.Collectors; */ public class ApiServer implements IApiServer { private final Logger logger = LoggerFactory.getLogger(LoggerNaming.name().of("api").of("server").toString()); - private HttpServer server = null; + private volatile HttpServer server = null; private final ICoreAccessor coreAccessor; @Inject @@ -38,6 +38,7 @@ public class ApiServer implements IApiServer { @Override public void startup(InetSocketAddress listen) throws ApiStartupException { try { + if (this.server != null) throw new IllegalStateException("API server is already started."); this.server = HttpServer.create(listen, 0); ImmutableMap.builder() .put("/", new JsonReqHandler("GET") { diff --git a/src/main/java/com/keuin/crosslink/config/GlobalConfigManager.java b/src/main/java/com/keuin/crosslink/config/GlobalConfigManager.java index 26996c2..fcc0321 100644 --- a/src/main/java/com/keuin/crosslink/config/GlobalConfigManager.java +++ b/src/main/java/com/keuin/crosslink/config/GlobalConfigManager.java @@ -1,35 +1,75 @@ package com.keuin.crosslink.config; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.jetbrains.annotations.NotNull; import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Objects; +import java.util.Optional; public class GlobalConfigManager { + + private static final Object lock = new Object(); + public static final ObjectMapper mapper = new ObjectMapper(); + + private volatile static GlobalConfigManager instance; + private JsonNode configMessaging; // mutable root node of file "messaging.json" + private JsonNode configApi; // mutable root node of file "api.json" + + private GlobalConfigManager() { + mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true); + } + /** - * Load config from disk. + * Load config from disk. Create the global instance. * If loaded successfully, the global 'loaded' status will be set to true. + * * @throws ConfigLoadException failed to load. The 'loaded' status will be set to false. */ - public static void initializeGlobalManager(File configFile) throws ConfigLoadException { - // TODO read config from disk, create the singleton object -// throw new RuntimeException(); + public static void initializeGlobalManager(@NotNull File configFile) throws ConfigLoadException, IOException { + Objects.requireNonNull(configFile); + synchronized (lock) { + if (instance == null) { + throw new IllegalStateException("already initialized"); + } + instance = new GlobalConfigManager(); + } + instance.loadConfig(configFile); + } + + private void loadConfig(File configDirectory) throws IOException { + try (var fis = new FileInputStream(new File(configDirectory, "messaging.json"))) { + configMessaging = Optional.ofNullable(mapper.readTree(fis)).orElse(mapper.readTree("{}")); + } + try (var fis = new FileInputStream(new File(configDirectory, "api.json"))) { + configApi = Optional.ofNullable(mapper.readTree(fis)).orElse(mapper.readTree("{}")); + } } public static @NotNull GlobalConfigManager getInstance() { - // TODO get the singleton object -// throw new RuntimeException(); - throw new RuntimeException("GlobalConfigManager is not initialized"); + final var in = instance; + if (in == null) { + throw new RuntimeException("GlobalConfigManager is not initialized"); + } + return in; } /** - * Get an immutable view of the global config. - * A view is a consistent, but not up-to-date snapshot. - * - * @return the config view. + * Config tree for messaging module. */ - public IConfigView getConfig() { - // TODO - throw new RuntimeException("Global config is not loaded"); + public @NotNull JsonNode messaging() { + return configMessaging.deepCopy(); + } + + /** + * Config tree for HTTP API module. + */ + public @NotNull JsonNode api() { + return configApi.deepCopy(); } public boolean isLoaded() { @@ -39,9 +79,10 @@ public class GlobalConfigManager { /** * Reload the config file from disk. * If loaded successfully, the global 'loaded' status will be set to true. + * * @throws ConfigLoadException failed to reload. - * If previously loaded, the global config won't be modified. - * Otherwise, the 'loaded' status will be set to false. + * If previously loaded, the global config won't be modified. + * Otherwise, the 'loaded' status will be set to false. */ public void reload() throws ConfigLoadException { // TODO diff --git a/src/main/java/com/keuin/crosslink/config/IConfigView.java b/src/main/java/com/keuin/crosslink/config/IConfigView.java deleted file mode 100644 index a3cab1b..0000000 --- a/src/main/java/com/keuin/crosslink/config/IConfigView.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.keuin.crosslink.config; - -public interface IConfigView { - int pingTimeoutMillis(); -} diff --git a/src/main/java/com/keuin/crosslink/messaging/config/router/IRouterConfigurer.java b/src/main/java/com/keuin/crosslink/messaging/config/router/IRouterConfigurer.java index 359ede4..a085b67 100644 --- a/src/main/java/com/keuin/crosslink/messaging/config/router/IRouterConfigurer.java +++ b/src/main/java/com/keuin/crosslink/messaging/config/router/IRouterConfigurer.java @@ -7,6 +7,7 @@ import com.keuin.crosslink.messaging.router.IRouterConfigurable; public interface IRouterConfigurer { /** * Parse and configure the router with internal configuration string. + * All existing endpoints and rule chains will be removed. * @throws JsonProcessingException cannot parse JSON string. * @throws ConfigSyntaxError config content is invalid. */ diff --git a/src/main/java/com/keuin/crosslink/messaging/config/router/RouterConfigurer.java b/src/main/java/com/keuin/crosslink/messaging/config/router/RouterConfigurer.java index 578fcd5..1e4e718 100644 --- a/src/main/java/com/keuin/crosslink/messaging/config/router/RouterConfigurer.java +++ b/src/main/java/com/keuin/crosslink/messaging/config/router/RouterConfigurer.java @@ -243,6 +243,7 @@ public class RouterConfigurer implements IRouterConfigurer { @Override public void configure(IRouterConfigurable router) throws JsonProcessingException, ConfigSyntaxError { + router.clearEndpoints(); router.updateRuleChain(loadRuleChain(router, config)); } diff --git a/src/main/java/com/keuin/crosslink/messaging/router/ConcreteRouter.java b/src/main/java/com/keuin/crosslink/messaging/router/ConcreteRouter.java index 6433f8e..0888910 100644 --- a/src/main/java/com/keuin/crosslink/messaging/router/ConcreteRouter.java +++ b/src/main/java/com/keuin/crosslink/messaging/router/ConcreteRouter.java @@ -37,6 +37,10 @@ public class ConcreteRouter implements IRouter { return true; } + public void clearEndpoints() { + endpoints.clear(); + } + @Override public @NotNull Set resolveEndpoints(@NotNull String namespace, @NotNull Pattern idPattern) { Objects.requireNonNull(namespace); diff --git a/src/main/java/com/keuin/crosslink/messaging/router/IRouterConfigurable.java b/src/main/java/com/keuin/crosslink/messaging/router/IRouterConfigurable.java index d1871b9..aac1f93 100644 --- a/src/main/java/com/keuin/crosslink/messaging/router/IRouterConfigurable.java +++ b/src/main/java/com/keuin/crosslink/messaging/router/IRouterConfigurable.java @@ -41,4 +41,9 @@ public interface IRouterConfigurable { @NotNull Set resolveEndpoints(@NotNull String namespace, @NotNull Pattern idPattern); void updateRuleChain(@NotNull List newChain); + + /** + * Remove all existing endpoints. + */ + void clearEndpoints(); } diff --git a/src/main/java/com/keuin/crosslink/plugin/common/PluginMain.java b/src/main/java/com/keuin/crosslink/plugin/common/PluginMain.java index 288af95..aa97a0d 100644 --- a/src/main/java/com/keuin/crosslink/plugin/common/PluginMain.java +++ b/src/main/java/com/keuin/crosslink/plugin/common/PluginMain.java @@ -1,19 +1,18 @@ package com.keuin.crosslink.plugin.common; -import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Inject; import com.keuin.crosslink.api.IApiServer; import com.keuin.crosslink.api.error.ApiStartupException; +import com.keuin.crosslink.config.ConfigLoadException; +import com.keuin.crosslink.config.GlobalConfigManager; import com.keuin.crosslink.messaging.config.ConfigSyntaxError; import com.keuin.crosslink.messaging.config.remote.InvalidEndpointConfigurationException; import com.keuin.crosslink.messaging.config.remote.RemoteEndpointFactory; import com.keuin.crosslink.messaging.config.router.RouterConfigurer; import com.keuin.crosslink.messaging.endpoint.IEndpoint; import com.keuin.crosslink.messaging.router.IRouter; -import com.keuin.crosslink.messaging.router.IRouterConfigurable; import com.keuin.crosslink.plugin.common.environ.PluginEnvironment; import com.keuin.crosslink.util.LoggerNaming; import com.keuin.crosslink.util.StartupMessagePrinter; @@ -22,13 +21,14 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.HashSet; import java.util.Optional; +import static com.keuin.crosslink.config.GlobalConfigManager.mapper; + public final class PluginMain { private final PluginEnvironment environment; private final IApiServer apiServer; @@ -54,60 +54,53 @@ public final class PluginMain { // the plugin is not constructed here, so don't register event listener here } - public void enable() { - final ObjectMapper mapper = new ObjectMapper(); - mapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true); - // TODO refactor setup and teardown routine, split into hooks - // load config - // TODO global config (such as API config) - logger.info("Loading message routing configuration."); - JsonNode messagingConfig = null, routingConfig = null, remoteConfig = null; - try (var fis = new FileInputStream(new File(environment.pluginDataPath().toFile(), "messaging.json"))) { - messagingConfig = Optional.ofNullable(mapper.readTree(fis)).orElse(mapper.readTree("{}")); - routingConfig = Optional.ofNullable(messagingConfig.get("routing")).orElse(mapper.readTree("[]")); - remoteConfig = Optional.ofNullable(messagingConfig.get("remotes")).orElse(mapper.readTree("[]")); - } catch (IOException ex) { - logger.error("Failed to load message routing configuration", ex); - throw new RuntimeException(ex); - } - + private void initialize() { + logger.info("Initializing components."); // initialize message routing logger.info("Initializing message routing."); - var endpoints = new HashSet(); + var endpoints = new HashSet<>(coreAccessor.getServerEndpoints()); // All local and remote endpoints. remotes will be added later try { + var messaging = GlobalConfigManager.getInstance().messaging(); + var routing = Optional.ofNullable(messaging.get("routing")) + .orElse(mapper.readTree("[]")); + var remote = Optional.ofNullable(messaging.get("remotes")) + .orElse(mapper.readTree("[]")); + + // load routing table try { logger.debug("Loading rule chain."); - var rc = new RouterConfigurer(routingConfig); - rc.configure(messageRouter); + var rc = new RouterConfigurer(routing); + rc.configure(messageRouter); // update routing table, clear endpoints logger.debug("Message router is configured successfully."); } catch (JsonProcessingException | ConfigSyntaxError ex) { - throw new IRouterConfigurable.ConfigLoadException(ex); + logger.error("Failed to load routing config", ex); + throw new RuntimeException(ex); } - //noinspection CollectionAddAllCanBeReplacedWithConstructor - endpoints.addAll(coreAccessor.getServerEndpoints()); - } catch (IRouter.ConfigLoadException ex) { - logger.error("Failed to read routing config", ex); - throw new RuntimeException(ex); - } - try { - logger.debug("Loading remote endpoints."); - if (!remoteConfig.isArray()) { - logger.error("Failed to load remote endpoints: remotes should be a JSON array."); - throw new RuntimeException("Invalid remotes type"); - } - for (JsonNode remote : remoteConfig) { - var ep = RemoteEndpointFactory.create(remote); - if (ep != null) { - logger.debug("Add remote endpoint: " + ep); - endpoints.add(ep); + // load remote endpoints + try { + logger.debug("Loading remote endpoints."); + if (!remote.isArray()) { + logger.error("Failed to load remote endpoints: remotes should be a JSON array."); + throw new RuntimeException("Invalid remotes type"); } + for (var r : remote) { + var ep = RemoteEndpointFactory.create(r); + if (ep != null) { + logger.debug("Add remote endpoint: " + ep); + endpoints.add(ep); + } + } + } catch (InvalidEndpointConfigurationException ex) { + logger.error("Invalid remote endpoint", ex); + throw new RuntimeException(ex); } - } catch (InvalidEndpointConfigurationException ex) { - logger.error("Invalid remote endpoint", ex); + } catch (JsonProcessingException ex) { + logger.error("Failed to parse JSON config", ex); throw new RuntimeException(ex); } + // register all endpoints on the router for (IEndpoint ep : endpoints) { if (!messageRouter.addEndpoint(ep)) { logger.error("Cannot add endpoint " + ep); @@ -117,8 +110,8 @@ public final class PluginMain { logger.info(String.format("Added %d sub-server(s) to message router.", endpoints.size())); logger.info("Starting API server."); - try (var fis = new FileInputStream(new File(environment.pluginDataPath().toFile(), "api.json"))) { - var apiConfig = Optional.ofNullable(mapper.readTree(fis)).orElse(mapper.readTree("{}")); + try { + var apiConfig = GlobalConfigManager.getInstance().api(); var host = Optional.ofNullable(apiConfig.get("host")).map(JsonNode::textValue).orElse(null); if (host == null || host.isEmpty() @@ -129,13 +122,26 @@ public final class PluginMain { if (port <= 0) throw new ApiStartupException("Invalid port to listen on"); // now the host is guaranteed to be an IP address string, so no DNS lookup will be performed apiServer.startup(new InetSocketAddress(InetAddress.getByName(host), port)); - } catch (IOException ex) { - logger.error("Failed to load message routing configuration", ex); - throw new RuntimeException(ex); - } catch (ApiStartupException ex) { + } catch (IOException | ApiStartupException ex) { logger.error("Failed to start API server", ex); - return; + throw new RuntimeException(ex); + } + + logger.info("Finish initializing."); + } + + public void enable() { + // TODO refactor setup and teardown routine, split into hooks + logger.info("Loading config from disk..."); + try { + GlobalConfigManager.initializeGlobalManager(new File( + environment.pluginDataPath().toFile(), "messaging.json")); + } catch (ConfigLoadException | IOException ex) { + logger.error("Failed to load configuration", ex); + throw new RuntimeException(ex); } + logger.info("Config files are loaded."); + initialize(); logger.info("CrossLink is enabled."); } @@ -147,7 +153,8 @@ public final class PluginMain { // may throw unchecked exception public void reload() { - // TODO make api server and router reloadable + disable(); + initialize(); } private String capital(String s) { -- cgit v1.2.3