Skip to content

Add support for YAML hot deployment and configuration reloading#2959

Open
christiangoerdes wants to merge 14 commits into
masterfrom
yaml-hot-deployment
Open

Add support for YAML hot deployment and configuration reloading#2959
christiangoerdes wants to merge 14 commits into
masterfrom
yaml-hot-deployment

Conversation

@christiangoerdes
Copy link
Copy Markdown
Collaborator

@christiangoerdes christiangoerdes commented May 27, 2026

Summary by CodeRabbit

  • New Features

    • YAML-based hot reloading to update configuration dynamically without restarting.
    • Hot deployment monitors YAML config files, can be enabled/disabled, and validates changes before applying with automatic recovery on failure.
    • Router exposes YAML config location and tracked files for monitoring.
  • Refactor

    • Simplified YAML bootstrap and startup flow for clearer startup/shutdown logging and more reliable lifecycle handling.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds YAML hot-reload: YamlRouterBootstrap to load YAML into a router, router APIs/state to store YAML location and orchestrate reloads, DefaultHotDeployer enable/disable lifecycle updates to manage YAML monitor threads, and YamlHotDeploymentThread that watches tracked files and triggers reloads.

Changes

YAML Hot Reload

Layer / File(s) Summary
YAML Bootstrap Mechanism
core/src/main/java/com/predic8/membrane/core/router/YamlRouterBootstrap.java, core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java
YamlRouterBootstrap.loadIntoRouter() parses YAML bean definitions, collects tracked source files, sets the router YAML configuration and base location; RouterCLI delegates YAML initialization to the bootstrapper and imports adjusted.
Router YAML Configuration & Reload API
core/src/main/java/com/predic8/membrane/core/router/DefaultRouter.java
Adds yamlConfigurationLocation, yamlTrackedFiles, and reloading state; provides synchronized setters/getters and reloadYamlConfiguration() to coordinate validation, shutdown, reload (via bootstrap), and restart; updates stop() and waitFor() and toggles the existing hotDeployer via setEnabled(...).
Hot Deployer Enable/Disable Lifecycle
core/src/main/java/com/predic8/membrane/core/router/hotdeploy/DefaultHotDeployer.java
Adds enabled flag and refines thread lifecycle handling; starts HotDeploymentThread (XML) or YamlHotDeploymentThread (YAML) when appropriate; stops router reinitializer and hot-deployment thread by concrete type on stop; isEnabled() reflects the flag.
YAML File Monitoring & Reload Trigger
core/src/main/java/com/predic8/membrane/core/router/hotdeploy/YamlHotDeploymentThread.java
New thread monitors YAML-tracked files and parent directories, compares lastModified timestamps, invokes router.reloadYamlConfiguration() on changes, refreshes tracked files, and provides stopASAP() for immediate interruption.

Sequence Diagram(s)

sequenceDiagram
  participant RouterCLI
  participant YamlRouterBootstrap
  participant DefaultRouter
  participant DefaultHotDeployer
  participant YamlHotDeploymentThread
  participant FileSystem
  RouterCLI->>YamlRouterBootstrap: loadIntoRouter(router, location)
  YamlRouterBootstrap->>DefaultRouter: setYamlConfiguration(location, trackedFiles)
  RouterCLI->>DefaultRouter: start()
  DefaultRouter->>DefaultHotDeployer: setEnabled(true)
  DefaultHotDeployer->>YamlHotDeploymentThread: new YamlHotDeploymentThread(router, hotDeployer)
  DefaultHotDeployer->>YamlHotDeploymentThread: start()
  YamlHotDeploymentThread->>FileSystem: record initial lastModified timestamps
  loop Every polling interval
    YamlHotDeploymentThread->>FileSystem: check file timestamps
    alt File changed
      YamlHotDeploymentThread->>DefaultRouter: reloadYamlConfiguration()
      DefaultRouter->>DefaultRouter: shutdown subsystems
      DefaultRouter->>YamlRouterBootstrap: loadIntoRouter(router, location)
      DefaultRouter->>DefaultRouter: restart components
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • rrayst
  • predic8

Poem

🐰 A rabbit nibbles YAML leaves,
He watches timestamps turn and weave—
When files twitch, the little rabbit sings,
Reloads the router, fluffs his springs.
Happy hops for config wings!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the primary changes: adding YAML hot deployment and configuration reloading support, which are the main features introduced across multiple files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch yaml-hot-deployment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
core/src/main/java/com/predic8/membrane/core/router/YamlRouterBootstrap.java (1)

37-57: 💤 Low value

Consider passing the root source file to collectTrackedFiles to avoid redundant computation.

getRootSourceFile(location) is called on line 47 when parsing definitions and again on line 62 inside collectTrackedFiles. The result could be computed once and passed as a parameter.

♻️ Suggested refactor
 public static void loadIntoRouter(DefaultRouter router, String location) throws Exception {
     var grammar = new GrammarAutoGenerated();
     var registry = new BeanRegistryImplementation(grammar);

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

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

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

-    router.setYamlConfiguration(location, collectTrackedFiles(location, definitions));
+    router.setYamlConfiguration(location, collectTrackedFiles(rootSourceFile, definitions));
     registry.finishStaticConfiguration();
     router.getConfiguration().setBaseLocation(location);
 }

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

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

Also applies to: 59-75

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/main/java/com/predic8/membrane/core/router/YamlRouterBootstrap.java`
around lines 37 - 57, Compute getRootSourceFile(location) once in loadIntoRouter
and pass that File (or Path) into collectTrackedFiles instead of calling
getRootSourceFile(location) twice; update the call site in loadIntoRouter to
store the result in a local (e.g., rootSourceFile) and use that variable for
both registry.parseYamlBeanDefinitions(..., getRootSourceFile(location)) and
router.setYamlConfiguration(location, collectTrackedFiles(location,
definitions)); adjust collectTrackedFiles signature if needed to accept the
precomputed root source file and update any usages of collectTrackedFiles
accordingly.
core/src/main/java/com/predic8/membrane/core/router/hotdeploy/DefaultHotDeployer.java (2)

92-94: 💤 Low value

Double-stop of reinitializer when router stops.

DefaultRouter.stop() calls hotDeployer.stop() (which now calls router.getReinitializer().stop() at line 93), and then DefaultRouter.stop() calls reinitializer.stop() again at line 312. This results in calling stop() twice on the same reinitializer.

If RuleReinitializer.stop() is idempotent this is harmless but inefficient. If not, it could cause issues. Consider removing this call from DefaultHotDeployer.stop() since the router already handles it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/predic8/membrane/core/router/hotdeploy/DefaultHotDeployer.java`
around lines 92 - 94, DefaultHotDeployer.stop() currently invokes
router.getReinitializer().stop(), which causes the same reinitializer to be
stopped again when DefaultRouter.stop() later calls reinitializer.stop(); remove
the explicit router.getReinitializer().stop() call from
DefaultHotDeployer.stop() so the router exclusively manages RuleReinitializer
lifecycle (keep the null-check/guarding logic around router but do not call
getReinitializer().stop()), and add a brief comment in DefaultHotDeployer.stop()
noting that DefaultRouter.stop() handles reinitializer shutdown to prevent
double-stop.

108-114: ⚡ Quick win

setEnabled(false) does not stop the running hot-deployment thread.

When enabled is set to false, the code only updates the field. The hot-deployment thread will continue running until it next checks hotDeployer.isEnabled() in its polling loop (up to 1 second later). For immediate shutdown semantics, call stop() when disabling.

Suggested fix
 `@Override`
 public void setEnabled(boolean enabled) {
     this.enabled = enabled;

     if (enabled && router != null) {
         startInternal();
+    } else if (!enabled) {
+        stop();
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/predic8/membrane/core/router/hotdeploy/DefaultHotDeployer.java`
around lines 108 - 114, The setter setEnabled(boolean enabled) only flips the
flag and starts the deployer when enabling, but does not stop the running
hot-deployment thread immediately when disabling; modify setEnabled to call
stop() when enabled is false (and router != null if you need same null-check as
start) so the hot-deployer thread is shut down immediately; update setEnabled to
invoke stop() before/after updating this.enabled (preserve thread-safety
semantics used elsewhere) and reference DefaultHotDeployer.startInternal(),
DefaultHotDeployer.stop(), the enabled field and the hotDeployer.isEnabled()
check in the polling loop when making the change.
core/src/main/java/com/predic8/membrane/core/router/hotdeploy/YamlHotDeploymentThread.java (1)

39-44: ⚡ Quick win

Consider making this a daemon thread.

Non-daemon threads prevent JVM shutdown. If the application wants to terminate but this thread is still running (e.g., waiting in sleep()), the JVM will hang. Setting daemon mode allows clean shutdown.

Suggested fix
 public YamlHotDeploymentThread(DefaultRouter router, DefaultHotDeployer hotDeployer) {
     super("yaml-hotdeploy");
+    setDaemon(true);
     this.router = router;
     this.hotDeployer = hotDeployer;
     refreshTrackedFiles();
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/src/main/java/com/predic8/membrane/core/router/hotdeploy/YamlHotDeploymentThread.java`
around lines 39 - 44, The YamlHotDeploymentThread constructor currently creates
a non-daemon thread which can prevent JVM shutdown; update the constructor in
class YamlHotDeploymentThread so the thread is marked as a daemon (call
setDaemon(true))—do this in the YamlHotDeploymentThread(DefaultRouter,
DefaultHotDeployer) constructor (before the thread might be started) so that
this background thread won’t block JVM exit while still calling
refreshTrackedFiles() as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/src/main/java/com/predic8/membrane/core/router/DefaultRouter.java`:
- Around line 117-119: The yamlConfigurationLocation and yamlTrackedFiles fields
are currently synchronized on this in their getters/setters but are read under
synchronized(lock) in reloadYamlConfiguration(), causing inconsistent locking;
update the getters/setters for yamlConfigurationLocation and yamlTrackedFiles to
use synchronized(lock) instead of synchronized(this), and add `@GuardedBy`("lock")
annotations to those fields (similar to reloading) so all accesses (fields and
methods including reloadYamlConfiguration(), getYamlConfigurationLocation(),
setYamlConfigurationLocation(), getYamlTrackedFiles(), setYamlTrackedFiles())
consistently synchronize on the same lock.
- Around line 496-510: The reload leaves the router stopped if loadIntoRouter()
or start() fails because shutdownForYamlReload() is called before attempting the
load; change the flow so you either perform the YAML load/validation before
shutting down or, at minimum, attempt to recover on failure: before calling
shutdownForYamlReload() keep whatever state/reference is needed, then wrap
YamlRouterBootstrap.loadIntoRouter(this, location) and start() in a try, and in
the catch block attempt to restart the previous runtime by invoking start() (or
restore the backed-up state) and log both the original error and the recovery
attempt/result; reference shutdownForYamlReload(), resetForYamlReload(),
YamlRouterBootstrap.loadIntoRouter(), and start() to locate the code to modify.
- Around line 535-540: resetForYamlReload() violates the `@GuardedBy`("lock")
contract by setting the guarded field initialized = false without holding lock;
either acquire the same lock in resetForYamlReload() (wrap the body in
synchronized(lock)) or move the initialized = false assignment into the
synchronized(lock) block inside reloadYamlConfiguration() so that modifications
to initialized (and any other guarded fields like mainComponents, configuration,
reinitializer) always occur while holding lock; update the method
resetForYamlReload() or the synchronized section in reloadYamlConfiguration()
accordingly to ensure the lock protects initialized.

In
`@core/src/main/java/com/predic8/membrane/core/router/hotdeploy/DefaultHotDeployer.java`:
- Line 34: The field 'enabled' has a race/visibility issue; change its
declaration (private boolean enabled = true) to be volatile (private volatile
boolean enabled = true) so updates made in setEnabled(...) are immediately
visible to threads reading it in startInternal() and other methods (e.g.,
isEnabled()); you can then keep setEnabled() and isEnabled() unsynchronized, or
alternatively protect all reads/writes with synchronized(lock) if you prefer
explicit locking—pick one approach and apply it consistently across all accesses
to 'enabled'.

---

Nitpick comments:
In
`@core/src/main/java/com/predic8/membrane/core/router/hotdeploy/DefaultHotDeployer.java`:
- Around line 92-94: DefaultHotDeployer.stop() currently invokes
router.getReinitializer().stop(), which causes the same reinitializer to be
stopped again when DefaultRouter.stop() later calls reinitializer.stop(); remove
the explicit router.getReinitializer().stop() call from
DefaultHotDeployer.stop() so the router exclusively manages RuleReinitializer
lifecycle (keep the null-check/guarding logic around router but do not call
getReinitializer().stop()), and add a brief comment in DefaultHotDeployer.stop()
noting that DefaultRouter.stop() handles reinitializer shutdown to prevent
double-stop.
- Around line 108-114: The setter setEnabled(boolean enabled) only flips the
flag and starts the deployer when enabling, but does not stop the running
hot-deployment thread immediately when disabling; modify setEnabled to call
stop() when enabled is false (and router != null if you need same null-check as
start) so the hot-deployer thread is shut down immediately; update setEnabled to
invoke stop() before/after updating this.enabled (preserve thread-safety
semantics used elsewhere) and reference DefaultHotDeployer.startInternal(),
DefaultHotDeployer.stop(), the enabled field and the hotDeployer.isEnabled()
check in the polling loop when making the change.

In
`@core/src/main/java/com/predic8/membrane/core/router/hotdeploy/YamlHotDeploymentThread.java`:
- Around line 39-44: The YamlHotDeploymentThread constructor currently creates a
non-daemon thread which can prevent JVM shutdown; update the constructor in
class YamlHotDeploymentThread so the thread is marked as a daemon (call
setDaemon(true))—do this in the YamlHotDeploymentThread(DefaultRouter,
DefaultHotDeployer) constructor (before the thread might be started) so that
this background thread won’t block JVM exit while still calling
refreshTrackedFiles() as before.

In
`@core/src/main/java/com/predic8/membrane/core/router/YamlRouterBootstrap.java`:
- Around line 37-57: Compute getRootSourceFile(location) once in loadIntoRouter
and pass that File (or Path) into collectTrackedFiles instead of calling
getRootSourceFile(location) twice; update the call site in loadIntoRouter to
store the result in a local (e.g., rootSourceFile) and use that variable for
both registry.parseYamlBeanDefinitions(..., getRootSourceFile(location)) and
router.setYamlConfiguration(location, collectTrackedFiles(location,
definitions)); adjust collectTrackedFiles signature if needed to accept the
precomputed root source file and update any usages of collectTrackedFiles
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f1a19c3c-8c30-47c3-8bde-8533f0bb2cf4

📥 Commits

Reviewing files that changed from the base of the PR and between c5aff4a and 5cdec9b.

📒 Files selected for processing (5)
  • core/src/main/java/com/predic8/membrane/core/cli/RouterCLI.java
  • core/src/main/java/com/predic8/membrane/core/router/DefaultRouter.java
  • core/src/main/java/com/predic8/membrane/core/router/YamlRouterBootstrap.java
  • core/src/main/java/com/predic8/membrane/core/router/hotdeploy/DefaultHotDeployer.java
  • core/src/main/java/com/predic8/membrane/core/router/hotdeploy/YamlHotDeploymentThread.java

Comment thread core/src/main/java/com/predic8/membrane/core/router/DefaultRouter.java Outdated
Comment thread core/src/main/java/com/predic8/membrane/core/router/DefaultRouter.java Outdated
@membrane-ci-server
Copy link
Copy Markdown

This pull request needs "/ok-to-test" from an authorized committer.

@christiangoerdes
Copy link
Copy Markdown
Collaborator Author

/ok-to-test

Comment thread core/src/main/java/com/predic8/membrane/core/router/DefaultRouter.java Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants