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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ protected String getClassImpl() {
* Include entries are resolved in the order they are listed. If an entry points to a directory,
* all matching <code>*.apis.yaml</code> / <code>*.apis.yml</code> files in that directory are included
* in lexicographical order. Includes are resolved recursively and cyclic include chains are rejected.
* Directory includes are not watched for YAML hot deployment; adding or removing files there does not
* trigger a reload automatically.
* </p>
* @yaml <pre><code>
* include:
Expand All @@ -62,7 +64,8 @@ public class IncludeList {
* </p>
* <p>
* Each string is a path to either a YAML file or a directory. Relative paths are resolved
* against the directory of the including file. Absolute paths are also supported.
* against the directory of the including file. Absolute paths are also supported. Directory
* entries are loaded during parsing, but they are not watched for YAML hot deployment.
* </p>
*/
@MCChildElement(allowForeign = true)
Expand Down
46 changes: 3 additions & 43 deletions core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,17 @@
package com.predic8.membrane.core.cli;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.predic8.membrane.annot.beanregistry.BeanDefinitions;
import com.predic8.membrane.annot.beanregistry.BeanRegistryImplementation;
import com.predic8.membrane.annot.yaml.ConfigurationParsingException;
import com.predic8.membrane.core.config.spring.GrammarAutoGenerated;
import com.predic8.membrane.core.config.spring.TrackingFileSystemXmlApplicationContext;
import com.predic8.membrane.core.exceptions.SpringConfigurationErrorHandler;
import com.predic8.membrane.core.openapi.serviceproxy.APIProxy;
import com.predic8.membrane.core.openapi.serviceproxy.OpenAPISpec;
import com.predic8.membrane.core.resolver.ResolverMap;
import com.predic8.membrane.core.resolver.ResourceRetrievalException;
import com.predic8.membrane.core.router.Configuration;
import com.predic8.membrane.core.router.DefaultRouter;
import com.predic8.membrane.core.router.Router;
import com.predic8.membrane.core.router.RouterXmlBootstrap;
import com.predic8.membrane.core.router.hotdeploy.YamlRouterReloader;
import org.apache.commons.cli.ParseException;
import org.apache.commons.codec.binary.Hex;
import org.jetbrains.annotations.NotNull;
Expand All @@ -39,11 +36,7 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
Expand All @@ -58,9 +51,9 @@
import static com.predic8.membrane.core.openapi.serviceproxy.OpenAPISpec.YesNoOpenAPIOption.YES;
import static com.predic8.membrane.core.openapi.util.OpenAPIUtil.isOpenAPIMisplacedError;
import static com.predic8.membrane.core.proxies.ApiInfo.logInfosAboutStartedProxies;
import static com.predic8.membrane.core.router.YamlRouterBootstrap.loadIntoRouter;
import static com.predic8.membrane.core.util.ExceptionUtil.concatMessageAndCauseMessages;
import static com.predic8.membrane.core.util.OSUtil.fixBackslashes;
import static com.predic8.membrane.core.util.OSUtil.isWindowsAbsolutePath;
import static com.predic8.membrane.core.util.URIUtil.pathFromFileURI;
import static com.predic8.membrane.core.util.text.TerminalColors.*;
import static java.lang.Integer.parseInt;
Expand Down Expand Up @@ -236,46 +229,13 @@ private static Router initRouterByYAML(MembraneCommandLine commandLine, String o

static Router initRouterByYAML(String location) throws Exception {
var router = new DefaultRouter();
var grammar = new GrammarAutoGenerated();
var reg = new BeanRegistryImplementation(grammar);

router.setRegistry(reg);
reg.register("router", router);

BeanDefinitions beanDefinitions = new BeanDefinitions(reg.parseYamlBeanDefinitions(router.getResolverMap().resolve(location), grammar, getRootSourceFile(location)));
beanDefinitions.ensureSingleGlobalDefinition();
beanDefinitions.getConfigDefinition()
.ifPresent(configBd -> router.applyConfiguration((Configuration) reg.resolve(configBd.getName())));

reg.finishStaticConfiguration();
reg.start();

// Must be set after reading the YAML. Otherwise, it will be overwritten
router.getConfiguration().setBaseLocation(location);

router.setConfigurationReloader(new YamlRouterReloader(router, loadIntoRouter(router, location)));
router.start();
logInfosAboutStartedProxies(router.getRuleManager());
logStartupMessage();
return router;
}

private static Path getRootSourceFile(String location) {
if (!isWindowsAbsolutePath(location)) {
try {
URI uri = new URI(location);
String scheme = uri.getScheme();
if (scheme != null && !"file".equalsIgnoreCase(scheme))
return null;
} catch (URISyntaxException ignored) {}
}

try {
return Path.of(pathFromFileURI(location));
} catch (InvalidPathException ignored) {
return null;
}
}

private static @NotNull APIProxy getApiProxy(MembraneCommandLine commandLine) throws IOException {
APIProxy api = new APIProxy();
api.setPort(commandLine.getCommand().isOptionSet("p") ?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@
import com.predic8.membrane.core.proxies.RuleManager.RuleDefinitionSource;
import com.predic8.membrane.core.proxies.SSLableProxy;
import com.predic8.membrane.core.resolver.ResolverMap;
import com.predic8.membrane.core.router.hotdeploy.ConfigurationReloader;
import com.predic8.membrane.core.router.hotdeploy.DefaultHotDeployer;
import com.predic8.membrane.core.router.hotdeploy.HotDeployer;
import com.predic8.membrane.core.router.hotdeploy.NullHotDeployer;
import com.predic8.membrane.core.transport.PortOccupiedException;
import com.predic8.membrane.core.transport.Transport;
import com.predic8.membrane.core.transport.http.HttpClient;
Expand Down Expand Up @@ -111,6 +111,9 @@ public class DefaultRouter extends AbstractRouter implements ApplicationContextA
@GuardedBy("lock")
private boolean initialized;

@GuardedBy("lock")
private boolean reloading;

Comment thread
christiangoerdes marked this conversation as resolved.
/**
* HotDeployer for changes on the configuration file.
* Not synchronized, since only modified during initialization
Expand Down Expand Up @@ -299,12 +302,16 @@ public void stop() {
getRegistry().getBean(KubernetesWatcher.class).ifPresent(KubernetesWatcher::stop);
hotDeployer.stop();

if (mainComponents.getTransport() != null)
if (reinitializer != null)
reinitializer.stop();
if (mainComponents.getTransport() != null)
mainComponents.getTransport().closeAll();
mainComponents.getTimerManager().shutdown();
closeRegistryIfSupported();

synchronized (lock) {
running = false;
reloading = false;
lock.notifyAll();
}
}
Expand Down Expand Up @@ -347,7 +354,7 @@ public String getId() {
*/
public void waitFor() {
synchronized (lock) {
while (running) {
while (running || reloading) {
try {
lock.wait();
} catch (InterruptedException ignored) {
Expand Down Expand Up @@ -430,7 +437,7 @@ public BeanRegistry getRegistry() {
}

public void applyConfiguration(Configuration configuration) {
hotDeployer = configuration.isHotDeploy() ? new DefaultHotDeployer() : new NullHotDeployer();
hotDeployer.setEnabled(configuration.isHotDeploy());
this.configuration = configuration;
}

Expand All @@ -454,6 +461,68 @@ public RuleReinitializer getReinitializer() {
return reinitializer;
}

public void setConfigurationReloader(ConfigurationReloader configurationReloader) {
hotDeployer.setConfigurationReloader(configurationReloader);
}

public boolean markReloading() {
synchronized (lock) {
if (reloading) {
return false;
}
reloading = true;
lock.notifyAll();
return true;
}
}

public void clearReloading() {
synchronized (lock) {
reloading = false;
lock.notifyAll();
}
}

public void stopRuntimeForReload() {
getRegistry().getBean(KubernetesWatcher.class).ifPresent(KubernetesWatcher::stop);

if (reinitializer != null)
reinitializer.stop();
if (mainComponents.getTransport() != null)
mainComponents.getTransport().closeAll(false);

synchronized (lock) {
running = false;
lock.notifyAll();
}
}

public void resetRuntime() {
synchronized (lock) {
mainComponents = new DefaultMainComponents(this);
configuration = new Configuration();
initialized = false;
reinitializer = null;
}
}

public void disposeRuntime() {
if (reinitializer != null)
reinitializer.stop();
mainComponents.getTimerManager().shutdown();
closeRegistryIfSupported();
}

private void closeRegistryIfSupported() {
closeRegistryIfSupported(getRegistry());
}

private void closeRegistryIfSupported(BeanRegistry registry) {
if (registry instanceof BeanRegistryImplementation beanRegistry) {
beanRegistry.close();
}
}

private static void handleOpenAPIParsingException(OpenAPIParsingException e) {
System.err.printf("""
================================================================================================
Expand Down Expand Up @@ -483,4 +552,4 @@ private static void handleDuplicateOpenAPIPaths(DuplicatePathException e) {
%n""", e.getPath());
throw new ExitException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* Copyright 2026 predic8 GmbH, www.predic8.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. */

package com.predic8.membrane.core.router;

import java.nio.file.Path;
import java.util.List;

/** Root YAML location and included local YAML files. */
public record YamlConfigurationSource(String location, List<Path> trackedFiles) {

public YamlConfigurationSource {
trackedFiles = trackedFiles == null ? List.of() : List.copyOf(trackedFiles);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/* Copyright 2026 predic8 GmbH, www.predic8.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. */

package com.predic8.membrane.core.router;

import com.predic8.membrane.annot.beanregistry.BeanDefinition;
import com.predic8.membrane.annot.beanregistry.BeanDefinitions;
import com.predic8.membrane.annot.beanregistry.BeanRegistryImplementation;
import com.predic8.membrane.core.config.spring.GrammarAutoGenerated;

import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.List;

import static com.predic8.membrane.core.util.OSUtil.isWindowsAbsolutePath;
import static com.predic8.membrane.core.util.URIUtil.pathFromFileURI;

/**
* Parses YAML into a router instance and returns the local files that can later be watched for hot reloads.
*/
public final class YamlRouterBootstrap {

private YamlRouterBootstrap() {
}

public static YamlConfigurationSource loadIntoRouter(DefaultRouter router, String location) throws Exception {
var grammar = new GrammarAutoGenerated();
var registry = new BeanRegistryImplementation(grammar);

router.setRegistry(registry);
registry.register("router", router);

List<BeanDefinition> definitions = registry.parseYamlBeanDefinitions(
router.getResolverMap().resolve(location),
grammar,
getRootSourceFile(location)
);

BeanDefinitions beanDefinitions = new BeanDefinitions(definitions);
beanDefinitions.ensureSingleGlobalDefinition();
beanDefinitions.getConfigDefinition().ifPresent(configBd -> router.applyConfiguration((Configuration) registry.resolve(configBd.getName())));

registry.finishStaticConfiguration();
router.getConfiguration().setBaseLocation(location);
return new YamlConfigurationSource(location, collectTrackedFiles(location, definitions));
}

static List<Path> collectTrackedFiles(String location, List<BeanDefinition> definitions) {
LinkedHashSet<Path> trackedFiles = new LinkedHashSet<>();

Path rootSourceFile = getRootSourceFile(location);
if (rootSourceFile != null) {
trackedFiles.add(rootSourceFile.toAbsolutePath().normalize());
}

for (BeanDefinition definition : definitions) {
if (definition.getSourceMetadata() == null || definition.getSourceMetadata().sourceFile() == null) {
continue;
}
trackedFiles.add(definition.getSourceMetadata().sourceFile().toAbsolutePath().normalize());
}

return List.copyOf(trackedFiles);
}

static Path getRootSourceFile(String location) {
if (!isWindowsAbsolutePath(location)) {
try {
URI uri = new URI(location);
String scheme = uri.getScheme();
if (scheme != null && !"file".equalsIgnoreCase(scheme))
return null;
} catch (URISyntaxException ignored) {
}
}

try {
return Path.of(pathFromFileURI(location));
} catch (InvalidPathException ignored) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* Copyright 2026 predic8 GmbH, www.predic8.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. */

package com.predic8.membrane.core.router.hotdeploy;

import java.nio.file.Path;
import java.util.List;

public interface ConfigurationReloader {

boolean reload();

List<Path> trackedFiles();
}
Loading
Loading