This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 48ea92c4092b CAMEL-26453: camel-dynamic-router - a route that sends to
a channel can be added again after it was removed (#28810)
48ea92c4092b is described below
commit 48ea92c4092b398f8f657320c7ac050d40da6bef
Author: allthingssecurity <[email protected]>
AuthorDate: Mon Nov 2 16:22:27 2026 +0530
CAMEL-26453: camel-dynamic-router - a route that sends to a channel can be
added again after it was removed (#28810)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../routing/DynamicRouterComponent.java | 90 ++++++-
.../routing/DynamicRouterEndpoint.java | 46 +++-
.../routing/DynamicRouterProcessor.java | 18 ++
.../routing/DynamicRouterProducer.java | 27 +-
.../routing/DynamicRouterComponentTest.java | 55 ++++-
.../routing/DynamicRouterProducerTest.java | 20 ++
.../routing/DynamicRouterRemoveRouteTest.java | 275 +++++++++++++++++++++
7 files changed, 497 insertions(+), 34 deletions(-)
diff --git
a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterComponent.java
b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterComponent.java
index 09113ba59a26..a8abafb0859e 100644
---
a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterComponent.java
+++
b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterComponent.java
@@ -16,8 +16,13 @@
*/
package org.apache.camel.component.dynamicrouter.routing;
-import java.util.HashMap;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiFunction;
import java.util.function.Supplier;
@@ -63,7 +68,21 @@ public class DynamicRouterComponent extends DefaultComponent
{
/**
* The {@link DynamicRouterProcessor}s, mapped by their channel, for the
Dynamic Router.
*/
- private final Map<String, DynamicRouterProcessor> processors = new
HashMap<>();
+ private final Map<String, DynamicRouterProcessor> processors = new
ConcurrentHashMap<>();
+
+ /**
+ * The endpoint that uses the processor of each channel, mapped by their
channel.
+ */
+ private final Map<String, DynamicRouterEndpoint> processorEndpoints = new
ConcurrentHashMap<>();
+
+ /**
+ * The processors that were replaced by a processor for an endpoint with
other options. Their recipient lists are
+ * only stopped when they are replaced, as another route can still have
exchanges in flight in them, and are shut
+ * down with the component. The threads of a thread pool of the default
profile end after its keep-alive time.
+ */
+ private final List<DynamicRouterProcessor> replacedProcessors = new
CopyOnWriteArrayList<>();
+
+ private final Lock processorsLock = new ReentrantLock();
/**
* Creates a {@link DynamicRouterEndpoint} instance.
@@ -158,22 +177,69 @@ public class DynamicRouterComponent extends
DefaultComponent {
*/
@Override
protected void doShutdown() throws Exception {
- ServiceHelper.stopAndShutdownService(processors);
+ // the recipient lists of the processors of the channels and of the
replaced processors, with their thread pools
+ List<RecipientList> recipientLists = new ArrayList<>();
+ processors.values().forEach(p ->
recipientLists.add(p.getRecipientList()));
+ replacedProcessors.forEach(p ->
recipientLists.add(p.getRecipientList()));
processors.clear();
- super.doShutdown();
+ processorEndpoints.clear();
+ replacedProcessors.clear();
+ try {
+ // tries every one of them, and rethrows the first failure
+ ServiceHelper.stopAndShutdownServices(recipientLists);
+ } finally {
+ super.doShutdown();
+ }
}
/**
- * If no processor has been added for the supplied channel, then add the
supplied processor.
+ * Get the processor of the channel of the endpoint, and create it if the
channel has none.
+ * <ul>
+ * <li>An endpoint with the same URI as the endpoint that uses the
processor uses the same processor, such as when
+ * the route that used the endpoint is removed and added again (also by a
route reload), or when a {@code toD}
+ * creates the endpoint again after the endpoint was removed or
evicted.</li>
+ * <li>An endpoint with another URI for the channel fails while the
endpoint that uses the processor is not stopped,
+ * as there can be only one processor per channel. When that endpoint is
stopped (such as with the route that used
+ * it), the processor is replaced by a processor for the new endpoint, and
the recipient list of the old processor
+ * is stopped (its thread pool is only shut down with the component, so
that the exchanges that another route still
+ * has in flight in it are not interrupted or dropped).</li>
+ * </ul>
*
- * @param channel the channel to add the processor to
- * @param processor the processor to add for the channel
+ * @param endpoint the endpoint
+ * @param factory creates the processor for the endpoint
+ * @return the processor of the channel of the endpoint
*/
- void addRoutingProcessor(final String channel, final
DynamicRouterProcessor processor) {
- if (processors.putIfAbsent(channel, processor) != null) {
- throw new IllegalArgumentException(
- "Dynamic Router can have only one processor per channel;
channel '"
- + channel + "' already has a
processor");
+ DynamicRouterProcessor getOrCreateRoutingProcessor(
+ final DynamicRouterEndpoint endpoint, final
Supplier<DynamicRouterProcessor> factory) {
+ final String channel = endpoint.getConfiguration().getChannel();
+ processorsLock.lock();
+ try {
+ DynamicRouterProcessor current = processors.get(channel);
+ if (current != null) {
+ DynamicRouterEndpoint user = processorEndpoints.get(channel);
+ if (user != null && (user == endpoint ||
user.getEndpointUri().equals(endpoint.getEndpointUri()))) {
+ processorEndpoints.put(channel, endpoint);
+ return current;
+ }
+ if (user == null || !user.isProcessorReleased()) {
+ throw new IllegalArgumentException(
+ "Dynamic Router can have only one processor per
channel; channel '"
+ + channel + "' already
has a processor");
+ }
+ LOG.info("Replacing the processor of channel '{}' of the
stopped endpoint {} for the endpoint {}",
+ channel, user.getEndpointUri(),
endpoint.getEndpointUri());
+ }
+ // create the new processor first, so that the channel keeps its
processor when that fails
+ DynamicRouterProcessor created = factory.get();
+ processors.put(channel, created);
+ processorEndpoints.put(channel, endpoint);
+ if (current != null) {
+ replacedProcessors.add(current);
+ current.stopRecipientList();
+ }
+ return created;
+ } finally {
+ processorsLock.unlock();
}
}
diff --git
a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpoint.java
b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpoint.java
index cda0e5fc8b51..c891fa9850df 100644
---
a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpoint.java
+++
b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterEndpoint.java
@@ -105,8 +105,14 @@ public class DynamicRouterEndpoint extends DefaultEndpoint
{
private final DynamicRouterFilterService filterService;
/**
- * Create the Dynamic Router {@link org.apache.camel.Endpoint} for the
given endpoint URI. This includes the
- * creation of a {@link DynamicRouterProcessor} that is registered with
the supplied {@link DynamicRouterComponent}.
+ * Whether this endpoint has been stopped (and not initialized again).
+ */
+ private volatile boolean processorReleased;
+
+ /**
+ * Create the Dynamic Router {@link org.apache.camel.Endpoint} for the
given endpoint URI. When it is initialized,
+ * the endpoint gets the {@link DynamicRouterProcessor} of its channel
from the supplied
+ * {@link DynamicRouterComponent}, which creates it when the channel has
none.
*
* @param uri the endpoint URI
* @param component the Dynamic Router {@link
org.apache.camel.Component}
@@ -160,7 +166,9 @@ public class DynamicRouterEndpoint extends DefaultEndpoint {
}
/**
- * Initialize the endpoint by creating (and adding) the {@link
DynamicRouterProcessor} instance.
+ * Initialize the endpoint by getting the {@link DynamicRouterProcessor}
of its channel from the component, which
+ * creates it with the {@link #processorFactorySupplier} when the channel
has none (see
+ * {@link DynamicRouterComponent#getOrCreateRoutingProcessor}).
*
* @throws Exception if there is a problem getting the {@link
DynamicRouterProcessor} instance through the
* {@link #processorFactorySupplier}
@@ -168,11 +176,27 @@ public class DynamicRouterEndpoint extends
DefaultEndpoint {
@Override
protected void doInit() throws Exception {
super.doInit();
- DynamicRouterComponent component = getDynamicRouterComponent();
- CamelContext camelContext = getCamelContext();
- DynamicRouterProcessor processor = processorFactorySupplier.get()
- .getInstance(camelContext, configuration, filterService,
recipientListSupplier);
- component.addRoutingProcessor(configuration.getChannel(), processor);
+ processorReleased = false;
+ getDynamicRouterComponent().getOrCreateRoutingProcessor(this, () ->
processorFactorySupplier.get()
+ .getInstance(getCamelContext(), configuration, filterService,
recipientListSupplier));
+ }
+
+ /**
+ * Marks the processor of the channel as released by this endpoint: an
endpoint with another URI for the channel can
+ * then replace it, such as when the route that used this endpoint is
removed and the route is added again with
+ * other options. An endpoint with the same URI uses the same processor
again.
+ */
+ @Override
+ protected void doStop() throws Exception {
+ processorReleased = true;
+ super.doStop();
+ }
+
+ /**
+ * Whether this endpoint has been stopped, so that the processor of its
channel can be replaced.
+ */
+ boolean isProcessorReleased() {
+ return processorReleased;
}
/**
@@ -220,9 +244,9 @@ public class DynamicRouterEndpoint extends DefaultEndpoint {
public static class DynamicRouterEndpointFactory {
/**
- * Create the Dynamic Router {@link org.apache.camel.Endpoint} for the
given endpoint URI. This includes the
- * creation of a {@link DynamicRouterProcessor} that is registered
with the supplied
- * {@link DynamicRouterComponent}.
+ * Create the Dynamic Router {@link org.apache.camel.Endpoint} for the
given endpoint URI. When it is
+ * initialized, the endpoint gets the {@link DynamicRouterProcessor}
of its channel from the supplied
+ * {@link DynamicRouterComponent}, which creates it when the channel
has none.
*
* @param uri the endpoint URI
* @param component the Dynamic Router {@link
org.apache.camel.Component}
diff --git
a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProcessor.java
b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProcessor.java
index 51ab3afc5502..da5dc7e67059 100644
---
a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProcessor.java
+++
b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProcessor.java
@@ -27,6 +27,7 @@ import
org.apache.camel.component.dynamicrouter.filter.DynamicRouterFilterServic
import org.apache.camel.component.dynamicrouter.filter.PrioritizedFilter;
import org.apache.camel.processor.RecipientList;
import org.apache.camel.support.AsyncProcessorSupport;
+import org.apache.camel.support.service.ServiceHelper;
import static
org.apache.camel.component.dynamicrouter.routing.DynamicRouterConstants.MODE_FIRST_MATCH;
import static
org.apache.camel.component.dynamicrouter.routing.DynamicRouterConstants.RECIPIENT_LIST_HEADER;
@@ -136,6 +137,23 @@ public class DynamicRouterProcessor extends
AsyncProcessorSupport {
return recipientList.process(exchange, callback);
}
+ /**
+ * Stops the recipient list of this processor (which is started when the
processor is created), when the processor
+ * is replaced by the processor of an endpoint with other options. Its
thread pool is not shut down, so that the
+ * exchanges that are still in flight in it are not interrupted or dropped.
+ */
+ void stopRecipientList() {
+ ServiceHelper.stopService(recipientList);
+ }
+
+ /**
+ * The recipient list of this processor, which the component stops and
shuts down (with the thread pool it created)
+ * when the component shuts down.
+ */
+ RecipientList getRecipientList() {
+ return recipientList;
+ }
+
/**
* Factory for creating {@link DynamicRouterProcessor} instances.
*/
diff --git
a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProducer.java
b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProducer.java
index c8e01d4e580b..f1cc3496bdad 100644
---
a/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProducer.java
+++
b/components/camel-dynamic-router/src/main/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProducer.java
@@ -63,8 +63,7 @@ public class DynamicRouterProducer extends
DefaultAsyncProducer {
*/
@Override
public void process(final Exchange exchange) throws Exception {
- component.getRoutingProcessor(configuration.getChannel())
- .process(exchange);
+ routingProcessor().process(exchange);
}
/**
@@ -76,7 +75,14 @@ public class DynamicRouterProducer extends
DefaultAsyncProducer {
*/
@Override
public boolean process(final Exchange exchange, final AsyncCallback
callback) {
- DynamicRouterProcessor routingProcessor =
component.getRoutingProcessor(configuration.getChannel());
+ DynamicRouterProcessor routingProcessor;
+ try {
+ routingProcessor = routingProcessor();
+ } catch (IllegalStateException e) {
+ exchange.setException(e);
+ callback.done(true);
+ return true;
+ }
if (configuration.isSynchronous()) {
try {
routingProcessor.process(exchange);
@@ -91,6 +97,21 @@ public class DynamicRouterProducer extends
DefaultAsyncProducer {
}
}
+ /**
+ * The processor of the channel, which the component keeps for the
endpoints of the channel.
+ *
+ * @throws IllegalStateException if the channel has no processor, such as
after the component has been shut down
+ */
+ private DynamicRouterProcessor routingProcessor() {
+ DynamicRouterProcessor routingProcessor =
component.getRoutingProcessor(configuration.getChannel());
+ if (routingProcessor == null) {
+ throw new IllegalStateException(
+ "The Dynamic Router channel '" + configuration.getChannel()
+ + "' has no processor");
+ }
+ return routingProcessor;
+ }
+
/**
* Create a {@link DynamicRouterProducer} instance.
*/
diff --git
a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterComponentTest.java
b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterComponentTest.java
index d4f6438ba74b..5c463eacf0e6 100644
---
a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterComponentTest.java
+++
b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterComponentTest.java
@@ -17,6 +17,7 @@
package org.apache.camel.component.dynamicrouter.routing;
import java.util.Collections;
+import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Supplier;
@@ -41,10 +42,13 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mock;
+import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class DynamicRouterComponentTest {
@@ -151,16 +155,51 @@ class DynamicRouterComponentTest {
}
@Test
- void testAddRoutingProcessor() {
- component.addRoutingProcessor(DYNAMIC_ROUTER_CHANNEL, processor);
+ void testGetOrCreateRoutingProcessor() throws Exception {
+ DynamicRouterConfiguration config = new DynamicRouterConfiguration();
+ config.setChannel(DYNAMIC_ROUTER_CHANNEL);
+ DynamicRouterEndpoint first =
Mockito.mock(DynamicRouterEndpoint.class);
+ DynamicRouterEndpoint sameUri =
Mockito.mock(DynamicRouterEndpoint.class);
+ DynamicRouterEndpoint otherUri =
Mockito.mock(DynamicRouterEndpoint.class);
+ for (DynamicRouterEndpoint e : List.of(first, sameUri, otherUri)) {
+ when(e.getConfiguration()).thenReturn(config);
+ }
+ when(first.getEndpointUri()).thenReturn("dynamic-router://test");
+ when(sameUri.getEndpointUri()).thenReturn("dynamic-router://test");
+
when(otherUri.getEndpointUri()).thenReturn("dynamic-router://test?recipientMode=allMatch");
+ DynamicRouterProcessor other =
Mockito.mock(DynamicRouterProcessor.class);
+
+ assertEquals(processor, component.getOrCreateRoutingProcessor(first,
() -> processor));
+ // an endpoint with the same uri uses the same processor
+ assertEquals(processor, component.getOrCreateRoutingProcessor(sameUri,
() -> other));
+ // an endpoint with another uri fails while the endpoint that uses the
processor is not stopped
+ assertThrows(IllegalArgumentException.class, () ->
component.getOrCreateRoutingProcessor(otherUri, () -> other));
assertEquals(processor,
component.getRoutingProcessor(DYNAMIC_ROUTER_CHANNEL));
- }
-
- @Test
- void testAddRoutingProcessorWithSecondProcessorForChannelError() {
- component.addRoutingProcessor(DYNAMIC_ROUTER_CHANNEL, processor);
+ Mockito.verify(processor, Mockito.never()).stopRecipientList();
+ // and replaces the processor once that endpoint is stopped; the
channel keeps its processor when the new one
+ // cannot be created
+ when(sameUri.isProcessorReleased()).thenReturn(true);
+ assertThrows(IllegalStateException.class, () ->
component.getOrCreateRoutingProcessor(otherUri, () -> {
+ throw new IllegalStateException("cannot create");
+ }));
assertEquals(processor,
component.getRoutingProcessor(DYNAMIC_ROUTER_CHANNEL));
- assertThrows(IllegalArgumentException.class, () ->
component.addRoutingProcessor(DYNAMIC_ROUTER_CHANNEL, processor));
+ Mockito.verify(processor, Mockito.never()).stopRecipientList();
+ assertEquals(other, component.getOrCreateRoutingProcessor(otherUri, ()
-> other));
+ assertEquals(other,
component.getRoutingProcessor(DYNAMIC_ROUTER_CHANNEL));
+ Mockito.verify(processor).stopRecipientList();
+
+ // the component shuts down the recipient lists of the processor of
the channel and of the replaced processor,
+ // all of them when one fails, and rethrows that failure
+ RecipientList replacedList = Mockito.mock(RecipientList.class);
+ RecipientList currentList = Mockito.mock(RecipientList.class);
+ when(processor.getRecipientList()).thenReturn(replacedList);
+ when(other.getRecipientList()).thenReturn(currentList);
+ Mockito.doThrow(new IllegalStateException("cannot
stop")).when(currentList).stop();
+ assertThrows(IllegalStateException.class, () ->
component.doShutdown());
+ Mockito.verify(replacedList).stop();
+ Mockito.verify(replacedList).shutdown();
+ Mockito.verify(currentList).stop();
+ assertNull(component.getRoutingProcessor(DYNAMIC_ROUTER_CHANNEL));
}
@Test
diff --git
a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProducerTest.java
b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProducerTest.java
index 54c13e79fd3c..f6155ac3c323 100644
---
a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProducerTest.java
+++
b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterProducerTest.java
@@ -33,11 +33,13 @@ import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
@@ -106,6 +108,24 @@ class DynamicRouterProducerTest {
assertTrue(result);
}
+ @Test
+ void testProcessSynchronousWithoutProcessor() {
+ when(configuration.getChannel()).thenReturn("testChannel");
+ when(component.getRoutingProcessor("testChannel")).thenReturn(null);
+ IllegalStateException e = assertThrows(IllegalStateException.class, ()
-> producer.process(exchange));
+ assertTrue(e.getMessage().contains("'testChannel'"), e.getMessage());
+ }
+
+ @Test
+ void testProcessAsynchronousWithoutProcessor() {
+ when(configuration.getChannel()).thenReturn("testChannel");
+ when(component.getRoutingProcessor("testChannel")).thenReturn(null);
+ boolean result = producer.process(exchange, asyncCallback);
+ assertTrue(result);
+ verify(exchange).setException(any(IllegalStateException.class));
+ verify(asyncCallback).done(true);
+ }
+
@Test
void testGetInstance() {
DynamicRouterProducer instance = new DynamicRouterProducerFactory()
diff --git
a/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterRemoveRouteTest.java
b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterRemoveRouteTest.java
new file mode 100644
index 000000000000..505930d9cad3
--- /dev/null
+++
b/components/camel-dynamic-router/src/test/java/org/apache/camel/component/dynamicrouter/routing/DynamicRouterRemoveRouteTest.java
@@ -0,0 +1,275 @@
+/*
+ * 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.camel.component.dynamicrouter.routing;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Endpoint;
+import org.apache.camel.Exchange;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.infra.core.CamelContextExtension;
+import org.apache.camel.test.infra.core.DefaultCamelContextExtension;
+import org.apache.camel.test.infra.core.annotations.RouteFixture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The channel of a Dynamic Router endpoint can be used again after the
endpoint was stopped, such as when the route
+ * that used it is removed.
+ */
+public class DynamicRouterRemoveRouteTest {
+
+ @RegisterExtension
+ static CamelContextExtension contextExtension = new
DefaultCamelContextExtension();
+
+ static volatile CountDownLatch reached = new CountDownLatch(1);
+
+ static volatile CountDownLatch release = new CountDownLatch(1);
+
+ CamelContext context;
+
+ ProducerTemplate template;
+
+ MockEndpoint mock;
+
+ @RouteFixture
+ public void createRouteBuilder(CamelContext context) throws Exception {
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:subscribe")
+ .routeId("subscribe")
+
.to("dynamic-router-control:subscribe?subscribeChannel=orders&subscriptionId=all"
+ +
"&destinationUri=mock:result&priority=1&predicate=true")
+
.to("dynamic-router-control:subscribe?subscribeChannel=sync&subscriptionId=all"
+ +
"&destinationUri=mock:result&priority=1&predicate=true")
+
.to("dynamic-router-control:subscribe?subscribeChannel=par&subscriptionId=all"
+ +
"&destinationUri=direct:slow&priority=1&predicate=true");
+ from("direct:slow")
+ .routeId("slow")
+ .process(e -> {
+ reached.countDown();
+ release.await(10, TimeUnit.SECONDS);
+ })
+ .to("mock:slow");
+ from("direct:dynamic")
+ .routeId("dynamic")
+ .toD("dynamic-router:${header.channel}");
+ }
+ });
+ }
+
+ @BeforeEach
+ void setUp() throws Exception {
+ context = contextExtension.getContext();
+ template = contextExtension.getProducerTemplate();
+ mock = contextExtension.getMockEndpoint("mock:result");
+ mock.reset();
+ for (String id : new String[] { "router", "syncRouter",
"parallelRouter" }) {
+ if (context.getRoute(id) != null) {
+ context.getRouteController().stopRoute(id);
+ context.removeRoute(id);
+ }
+ }
+ template.sendBody("direct:subscribe", "");
+ context.addRoutes(routerRoutes());
+ }
+
+ private static RoutesBuilder routerRoutes() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:start").routeId("router").to("dynamic-router:orders");
+
from("direct:sync").routeId("syncRouter").to("dynamic-router:sync?synchronous=true");
+
from("direct:parallel").routeId("parallelRouter").to("dynamic-router:par?parallelProcessing=true");
+ }
+ };
+ }
+
+ private DynamicRouterComponent component() {
+ return context.getComponent("dynamic-router",
DynamicRouterComponent.class);
+ }
+
+ @Test
+ void addRouteAgainAfterRemove() throws Exception {
+ mock.expectedBodiesReceived("before", "after");
+
+ template.sendBody("direct:start", "before");
+ DynamicRouterProcessor processor =
component().getRoutingProcessor("orders");
+
+ context.getRouteController().stopRoute("router");
+ context.removeRoute("router");
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:start").routeId("router").to("dynamic-router:orders");
+ }
+ });
+ template.sendBody("direct:start", "after");
+
+ mock.assertIsSatisfied();
+ // the endpoint of the route that is added again uses the processor of
the channel again
+ assertSame(processor, component().getRoutingProcessor("orders"));
+ }
+
+ @Test
+ void addRouteAgainWithOtherOptions() throws Exception {
+ mock.expectedBodiesReceived("before", "after");
+
+ template.sendBody("direct:start", "before");
+ DynamicRouterProcessor processor =
component().getRoutingProcessor("orders");
+
+ context.getRouteController().stopRoute("router");
+ context.removeRoute("router");
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+
from("direct:start").routeId("router").to("dynamic-router:orders?recipientMode=allMatch");
+ }
+ });
+ template.sendBody("direct:start", "after");
+
+ mock.assertIsSatisfied();
+ // the endpoint with other options gets a new processor, and the
processor of the removed endpoint is stopped
+ assertNotSame(processor, component().getRoutingProcessor("orders"));
+ assertTrue(processor.getRecipientList().isStopped());
+ }
+
+ @Test
+ void addRouteAgainWithOtherOptionsKeepsThePoolOfTheReplacedProcessor()
throws Exception {
+ DynamicRouterProcessor processor =
component().getRoutingProcessor("par");
+
+ context.getRouteController().stopRoute("parallelRouter");
+ context.removeRoute("parallelRouter");
+ context.addRoutes(new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:parallel").routeId("parallelRouter")
+
.to("dynamic-router:par?parallelProcessing=true&recipientMode=allMatch");
+ }
+ });
+
+ // the recipient list of the replaced processor is stopped, but its
thread pool is not shut down, as another
+ // route could still have exchanges in flight in it
+ assertNotSame(processor, component().getRoutingProcessor("par"));
+ assertTrue(processor.getRecipientList().isStopped());
+
assertFalse(processor.getRecipientList().getExecutorService().isShutdown());
+ }
+
+ @Test
+ void toDAfterRemovedRoute() throws Exception {
+ mock.expectedBodiesReceived("before", "after");
+
+ // the toD uses the endpoint of the route
+ template.sendBodyAndHeader("direct:dynamic", "before", "channel",
"orders");
+ DynamicRouterProcessor processor =
component().getRoutingProcessor("orders");
+
+ context.getRouteController().stopRoute("router");
+ context.removeRoute("router");
+
+ // the toD creates the endpoint of the channel again
+ template.sendBodyAndHeader("direct:dynamic", "after", "channel",
"orders");
+
+ mock.assertIsSatisfied();
+ assertSame(processor, component().getRoutingProcessor("orders"));
+ }
+
+ @Test
+ void stopAndStartEndpoint() throws Exception {
+ mock.expectedBodiesReceived("after");
+ Endpoint endpoint = context.getEndpoint("dynamic-router:orders");
+ DynamicRouterProcessor processor =
component().getRoutingProcessor("orders");
+ assertNotNull(processor);
+
+ // a stopped endpoint is initialized again when it starts, and uses
the same processor
+ endpoint.stop();
+ endpoint.start();
+ assertSame(processor, component().getRoutingProcessor("orders"));
+
+ template.sendBody("direct:start", "after");
+ mock.assertIsSatisfied();
+ }
+
+ @Test
+ void secondEndpointForChannelKeepsTheProcessor() throws Exception {
+ mock.expectedBodiesReceived("after");
+ DynamicRouterProcessor processor =
component().getRoutingProcessor("orders");
+
+ // another endpoint for the channel fails as before while the endpoint
of the channel is in use
+ assertThrows(Exception.class, () ->
context.getEndpoint("dynamic-router:orders?recipientMode=allMatch"));
+
+ assertSame(processor, component().getRoutingProcessor("orders"));
+ assertFalse(processor.getRecipientList().isStopped());
+ template.sendBody("direct:start", "after");
+ mock.assertIsSatisfied();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "orders", "sync" })
+ void sendThroughRemovedEndpoint(String channel) throws Exception {
+ mock.expectedBodiesReceived("routed");
+ // a route that still sends to an endpoint that was removed (such as
an endpoint that a toD producer cache
+ // evicted while it was used) still routes with the processor of the
channel
+ context.removeEndpoint(context.getEndpoint(channel.equals("sync")
+ ? "dynamic-router:sync?synchronous=true" :
"dynamic-router:orders"));
+
+ Exchange out = template.send(channel.equals("sync") ? "direct:sync" :
"direct:start",
+ e -> e.getMessage().setBody("routed"));
+
+ assertNull(out.getException());
+ mock.assertIsSatisfied();
+ }
+
+ @Test
+ void exchangeInFlightCompletesWhenEndpointIsRemoved() throws Exception {
+ reached = new CountDownLatch(1);
+ release = new CountDownLatch(1);
+ MockEndpoint slow = contextExtension.getMockEndpoint("mock:slow");
+ slow.reset();
+ slow.expectedBodiesReceived("in flight");
+
+ CompletableFuture<Exchange> future
+ = template.asyncSend("direct:parallel", e ->
e.getMessage().setBody("in flight"));
+ try {
+ assertTrue(reached.await(10, TimeUnit.SECONDS));
+
context.removeEndpoint(context.getEndpoint("dynamic-router:par?parallelProcessing=true"));
+ } finally {
+ release.countDown();
+ }
+
+ Exchange out = future.get(10, TimeUnit.SECONDS);
+ assertNull(out.getException());
+ slow.assertIsSatisfied();
+ }
+}