[
https://issues.apache.org/jira/browse/CAMEL-23735?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Thomas LE GUEN updated CAMEL-23735:
-----------------------------------
Description:
h2. Summary
Since the CAMEL-20302 refactoring (Camel 4.4.0), the {{dynamic-router}}
component uses a private {{NoopAggregationStrategy}} as its default aggregation
strategy, which keeps the *first* recipient's result. Both the user
documentation and the component's own javadoc still state that the *last* reply
is used by default (the pre-4.4 behavior, {{{}UseLatestAggregationStrategy{}}}).
In {{{}recipientMode=allMatch{}}}, any modification made by a recipient other
than the first one (body, exchange properties) is silently discarded when the
exchange returns to the calling route. Whether a downstream {{choice()}} sees a
property set by a subscriber now depends on subscription priorities, which is
surprising and undocumented.
h2. Expected behavior (as documented)
The component documentation for the {{aggregationStrategy}} option states:
{quote}Refers to an AggregationStrategy to be used to assemble the replies from
the multicasts, into a single outgoing message from the Multicast. By default,
Camel will use the last reply as the outgoing message.
{quote}
The javadoc of {{DynamicRouterRecipientListHelper#createAggregationStrategy}}
also states:
{quote}If all else fails, then a {{UseLatestAggregationStrategy}} is used.
{quote}
h2. Actual behavior
{{DynamicRouterRecipientListHelper.createAggregationStrategy}} falls back to a
private {{{}NoopAggregationStrategy{}}}:
{code:java}
.orElse(new NoopAggregationStrategy());
...
public static class NoopAggregationStrategy implements AggregationStrategy {
public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
return oldExchange == null ? newExchange : oldExchange; // keeps the
FIRST result
}
}
{code}
The aggregated (first) copy is then written back onto the original exchange by
{{MulticastProcessor.doDone}} via {{{}ExchangeHelper.copyResults{}}}, so the
caller route only ever sees the first recipient's outcome.
Note that the unused import of {{UseLatestAggregationStrategy}} is still
present in {{{}DynamicRouterRecipientListHelper{}}}, which suggests the
behavior change was unintentional.
h2. Regression evidence
* Camel 4.3.0, {{{}DynamicRouterEndpoint#determineAggregationStrategy{}}}:
{code:java}
return cfgStrategy != null
? CamelContextHelper.mandatoryLookup(camelContext, cfgStrategy,
AggregationStrategy.class)
: new UseLatestAggregationStrategy();
{code}
* Camel 4.4.0 (commit {{{}c4711c97{}}}, CAMEL-20302) introduced
{{DynamicRouterRecipientListHelper}} with the {{NoopAggregationStrategy}}
fallback shown above.
* Still present and verified on 4.14.1 (LTS) and 4.20.0.
h2. Reproduction (JUnit 5, camel-test-junit5, verified on 4.14.1 and 4.20.0)
{code:java}
public class DynamicRouterDefaultAggregationTest extends CamelTestSupport {
@Override
protected RoutesBuilder createRouteBuilder() {
return new RouteBuilder() {
@Override
public void configure() {
from("direct:start")
.to("dynamic-router:demo?recipientMode=allMatch")
.to("mock:result");
// first recipient (priority 1): leaves its copy untouched
from("direct:first").log("first recipient");
// second recipient (priority 2): modifies its copy
from("direct:second")
.setProperty("enrichedBy", constant("second"))
.setBody(constant("modified by second"));
}
};
}
@Test
void defaultAggregationShouldKeepLastReplyAsDocumented() throws Exception {
subscribe("first-sub", "direct:first", 1);
subscribe("second-sub", "direct:second", 2);
getMockEndpoint("mock:result").expectedMessageCount(1);
template.sendBody("direct:start", "original");
MockEndpoint.assertIsSatisfied(context);
Exchange result = getMockEndpoint("mock:result").getExchanges().get(0);
// Documentation: "By default, Camel will use the last reply as the
outgoing message."
// Both assertions FAIL: the body is still "original" and the property
is null,
// because the first recipient's (untouched) copy wins.
assertEquals("modified by second",
result.getMessage().getBody(String.class));
assertEquals("second", result.getProperty("enrichedBy", String.class));
}
private void subscribe(String id, String uri, int priority) {
template.sendBody("dynamic-router-control:subscribe",
DynamicRouterControlMessage.Builder.newBuilder()
.subscribeChannel("demo")
.subscriptionId(id)
.destinationUri(uri)
.priority(priority)
.predicate("${body} != null")
.expressionLanguage("simple")
.build());
}
}
{code}
h2. Suggested resolution
Either:
# restore {{UseLatestAggregationStrategy}} as the default, matching the
documentation and the pre-4.4 behavior, or
# keep {{NoopAggregationStrategy}} but update the option description, the
javadoc of {{createAggregationStrategy}} (and remove the stale
{{UseLatestAggregationStrategy}} import), clearly stating that the first
matching recipient's result is kept by default.
Option 1 seems preferable since the current behavior looks unintentional (stale
import, contradicting javadoc) and silently drops data.
> camel-dynamic-router: default aggregation keeps the FIRST reply,
> contradicting the documented "last reply" behavior (regression since 4.4.0)
> --------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: CAMEL-23735
> URL: https://issues.apache.org/jira/browse/CAMEL-23735
> Project: Camel
> Issue Type: Bug
> Components: camel-core
> Affects Versions: 4.4.0
> Reporter: Thomas LE GUEN
> Priority: Minor
> Labels: dynamic-router, dynamic-router-eip
>
> h2. Summary
> Since the CAMEL-20302 refactoring (Camel 4.4.0), the {{dynamic-router}}
> component uses a private {{NoopAggregationStrategy}} as its default
> aggregation strategy, which keeps the *first* recipient's result. Both the
> user documentation and the component's own javadoc still state that the
> *last* reply is used by default (the pre-4.4 behavior,
> {{{}UseLatestAggregationStrategy{}}}).
> In {{{}recipientMode=allMatch{}}}, any modification made by a recipient other
> than the first one (body, exchange properties) is silently discarded when the
> exchange returns to the calling route. Whether a downstream {{choice()}} sees
> a property set by a subscriber now depends on subscription priorities, which
> is surprising and undocumented.
> h2. Expected behavior (as documented)
> The component documentation for the {{aggregationStrategy}} option states:
> {quote}Refers to an AggregationStrategy to be used to assemble the replies
> from the multicasts, into a single outgoing message from the Multicast. By
> default, Camel will use the last reply as the outgoing message.
> {quote}
> The javadoc of {{DynamicRouterRecipientListHelper#createAggregationStrategy}}
> also states:
> {quote}If all else fails, then a {{UseLatestAggregationStrategy}} is used.
> {quote}
> h2. Actual behavior
> {{DynamicRouterRecipientListHelper.createAggregationStrategy}} falls back to
> a private {{{}NoopAggregationStrategy{}}}:
> {code:java}
> .orElse(new NoopAggregationStrategy());
> ...
> public static class NoopAggregationStrategy implements AggregationStrategy {
> public Exchange aggregate(Exchange oldExchange, Exchange newExchange) {
> return oldExchange == null ? newExchange : oldExchange; // keeps the
> FIRST result
> }
> }
> {code}
> The aggregated (first) copy is then written back onto the original exchange
> by {{MulticastProcessor.doDone}} via {{{}ExchangeHelper.copyResults{}}}, so
> the caller route only ever sees the first recipient's outcome.
> Note that the unused import of {{UseLatestAggregationStrategy}} is still
> present in {{{}DynamicRouterRecipientListHelper{}}}, which suggests the
> behavior change was unintentional.
> h2. Regression evidence
> * Camel 4.3.0, {{{}DynamicRouterEndpoint#determineAggregationStrategy{}}}:
> {code:java}
> return cfgStrategy != null
> ? CamelContextHelper.mandatoryLookup(camelContext, cfgStrategy,
> AggregationStrategy.class)
> : new UseLatestAggregationStrategy();
> {code}
> * Camel 4.4.0 (commit {{{}c4711c97{}}}, CAMEL-20302) introduced
> {{DynamicRouterRecipientListHelper}} with the {{NoopAggregationStrategy}}
> fallback shown above.
> * Still present and verified on 4.14.1 (LTS) and 4.20.0.
> h2. Reproduction (JUnit 5, camel-test-junit5, verified on 4.14.1 and 4.20.0)
> {code:java}
> public class DynamicRouterDefaultAggregationTest extends CamelTestSupport {
> @Override
> protected RoutesBuilder createRouteBuilder() {
> return new RouteBuilder() {
> @Override
> public void configure() {
> from("direct:start")
> .to("dynamic-router:demo?recipientMode=allMatch")
> .to("mock:result");
> // first recipient (priority 1): leaves its copy untouched
> from("direct:first").log("first recipient");
> // second recipient (priority 2): modifies its copy
> from("direct:second")
> .setProperty("enrichedBy", constant("second"))
> .setBody(constant("modified by second"));
> }
> };
> }
> @Test
> void defaultAggregationShouldKeepLastReplyAsDocumented() throws Exception
> {
> subscribe("first-sub", "direct:first", 1);
> subscribe("second-sub", "direct:second", 2);
> getMockEndpoint("mock:result").expectedMessageCount(1);
> template.sendBody("direct:start", "original");
> MockEndpoint.assertIsSatisfied(context);
> Exchange result =
> getMockEndpoint("mock:result").getExchanges().get(0);
> // Documentation: "By default, Camel will use the last reply as the
> outgoing message."
> // Both assertions FAIL: the body is still "original" and the
> property is null,
> // because the first recipient's (untouched) copy wins.
> assertEquals("modified by second",
> result.getMessage().getBody(String.class));
> assertEquals("second", result.getProperty("enrichedBy",
> String.class));
> }
> private void subscribe(String id, String uri, int priority) {
> template.sendBody("dynamic-router-control:subscribe",
> DynamicRouterControlMessage.Builder.newBuilder()
> .subscribeChannel("demo")
> .subscriptionId(id)
> .destinationUri(uri)
> .priority(priority)
> .predicate("${body} != null")
> .expressionLanguage("simple")
> .build());
> }
> }
> {code}
> h2. Suggested resolution
> Either:
> # restore {{UseLatestAggregationStrategy}} as the default, matching the
> documentation and the pre-4.4 behavior, or
> # keep {{NoopAggregationStrategy}} but update the option description, the
> javadoc of {{createAggregationStrategy}} (and remove the stale
> {{UseLatestAggregationStrategy}} import), clearly stating that the first
> matching recipient's result is kept by default.
> Option 1 seems preferable since the current behavior looks unintentional
> (stale import, contradicting javadoc) and silently drops data.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)