This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-25620 in repository https://gitbox.apache.org/repos/asf/camel.git
commit de1c42ab64fb6694e5bb4001ee6b6c40c565ca0f Author: Claus Ibsen <[email protected]> AuthorDate: Tue Oct 13 17:05:22 2026 +0200 CAMEL-25620: camel-console - allow dev consoles to be registered while the registry starts DefaultDevConsoleRegistry kept its consoles in a TreeSet, so a console registered from another thread (such as camel-cli-connector resolving consoles while CamelContext is starting) failed the iteration in doStart with ConcurrentModificationException and CamelContext did not start. Use a ConcurrentSkipListSet sorted by id, and let resolveById return the registered console when another thread registered the same id first. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../impl/console/DefaultDevConsoleRegistry.java | 12 +++-- .../console/DefaultDevConsoleRegistryTest.java | 55 ++++++++++++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) diff --git a/core/camel-console/src/main/java/org/apache/camel/impl/console/DefaultDevConsoleRegistry.java b/core/camel-console/src/main/java/org/apache/camel/impl/console/DefaultDevConsoleRegistry.java index 8b6c2b882c32..1c3ef4dc61d9 100644 --- a/core/camel-console/src/main/java/org/apache/camel/impl/console/DefaultDevConsoleRegistry.java +++ b/core/camel-console/src/main/java/org/apache/camel/impl/console/DefaultDevConsoleRegistry.java @@ -19,7 +19,7 @@ package org.apache.camel.impl.console; import java.util.Collection; import java.util.Comparator; import java.util.Set; -import java.util.TreeSet; +import java.util.concurrent.ConcurrentSkipListSet; import java.util.stream.Stream; import org.apache.camel.CamelContext; @@ -56,8 +56,9 @@ public class DefaultDevConsoleRegistry extends ServiceSupport implements DevCons } public DefaultDevConsoleRegistry(CamelContext camelContext) { - // sort by id - this.consoles = new TreeSet<>(Comparator.comparing(DevConsole::getId)); + // sort by id, and allow consoles to be registered while the registry is iterating its consoles + // (such as camel-cli-connector resolving consoles while camel is starting) + this.consoles = new ConcurrentSkipListSet<>(Comparator.comparing(DevConsole::getId)); setCamelContext(camelContext); } @@ -120,8 +121,9 @@ public class DefaultDevConsoleRegistry extends ServiceSupport implements DevCons if (answer == null) { DevConsoleResolver resolver = PluginHelper.getDevConsoleResolver(camelContext); answer = resolver.resolveDevConsole(id); - if (answer != null) { - register(answer); + if (answer != null && !register(answer)) { + // another thread registered the console in the meantime, so use that one + answer = getConsole(id).orElse(answer); } } diff --git a/core/camel-console/src/test/java/org/apache/camel/impl/console/DefaultDevConsoleRegistryTest.java b/core/camel-console/src/test/java/org/apache/camel/impl/console/DefaultDevConsoleRegistryTest.java index 4e5af79bd7d0..79538b1581c8 100644 --- a/core/camel-console/src/test/java/org/apache/camel/impl/console/DefaultDevConsoleRegistryTest.java +++ b/core/camel-console/src/test/java/org/apache/camel/impl/console/DefaultDevConsoleRegistryTest.java @@ -16,10 +16,13 @@ */ package org.apache.camel.impl.console; +import java.util.Map; + import org.apache.camel.ContextTestSupport; import org.apache.camel.console.DevConsole; import org.apache.camel.console.DevConsoleRegistry; import org.apache.camel.support.PluginHelper; +import org.apache.camel.support.console.AbstractDevConsole; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -74,4 +77,56 @@ public class DefaultDevConsoleRegistryTest extends ContextTestSupport { Assertions.assertTrue(unregistered); Assertions.assertFalse(registry.getConsole("custom-test").isPresent()); } + + @Test + public void testRegisterWhileStarting() throws Exception { + DefaultDevConsoleRegistry registry = new DefaultDevConsoleRegistry(context); + registry.start(); + + // a console that registers another console when started, such as camel-cli-connector + // resolving consoles from its own thread while the registry is starting its consoles + DevConsole first = new TestConsole("a-first") { + @Override + protected void doStart() throws Exception { + registry.register(new TestConsole("z-late-" + registry.stream().count())); + } + }; + registry.register(first); + registry.register(new TestConsole("b-second")); + registry.stop(); + + // starting again registers a console while the registry iterates its consoles + registry.start(); + + Assertions.assertTrue(registry.getConsole("a-first").isPresent()); + Assertions.assertTrue(registry.getConsole("b-second").isPresent()); + Assertions.assertEquals(4, registry.stream().count()); + } + + @Test + public void testRegisterSameIdTwice() { + DefaultDevConsoleRegistry registry = new DefaultDevConsoleRegistry(context); + + DevConsole console = new TestConsole("same"); + Assertions.assertTrue(registry.register(console)); + Assertions.assertFalse(registry.register(new TestConsole("same"))); + Assertions.assertSame(console, registry.getConsole("same").orElse(null)); + } + + private static class TestConsole extends AbstractDevConsole { + + TestConsole(String id) { + super("test", id, id, id); + } + + @Override + protected Map<String, Object> doCallJson(Map<String, Object> options) { + return Map.of(); + } + + @Override + protected String doCallText(Map<String, Object> options) { + return ""; + } + } }
