This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26607-multi-hook in repository https://gitbox.apache.org/repos/asf/camel.git
commit cd553ed4ec0ad071acfb93a4b956ba83f3dcd74d Author: Claus Ibsen <[email protected]> AuthorDate: Sat Nov 7 11:05:19 2026 +0100 CAMEL-26607: camel-core - more than one StepHook and SendHook, ordered, so chaos runs with other dev tools ExtendedCamelContext gets addStepHook/removeStepHook and addSendHook/removeSendHook in place of the single-slot setters (unreleased, 4.24). The context keeps one volatile field that is null, the one hook, or a copy-on-write CompositeStepHook/CompositeSendHook (camel-support), so the hot path stays one null check. Hooks are sorted by Ordered (stable, no Ordered counts as 0). A composite step hook asks the hooks in order, stops at the first that fails the step, and adds up the delays. A composite send hook asks in order, the first SKIP wins, the longest delay wins, and afterSend is called only on the hooks that were asked, in reverse order. camel-chaos installs its hooks with add*, with Ordered.LOWEST so they run innermost, and no longer refuses to install when another hook is there. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../org/apache/camel/catalog/docs/chaos.adoc | 7 +- components/camel-chaos/src/main/docs/chaos.adoc | 7 +- .../apache/camel/component/chaos/ChaosManager.java | 14 +- .../camel/component/chaos/ChaosSendHook.java | 11 +- .../camel/component/chaos/ChaosStepHook.java | 11 +- .../camel/component/chaos/ChaosActivationTest.java | 13 +- .../java/org/apache/camel/jta/StepHookJtaTest.java | 2 +- .../org/apache/camel/ExtendedCamelContext.java | 52 ++- .../main/java/org/apache/camel/spi/SendHook.java | 17 +- .../main/java/org/apache/camel/spi/StepHook.java | 18 +- .../impl/engine/DefaultCamelContextExtension.java | 58 ++- .../camel/impl/ExtendedCamelContextConfigurer.java | 12 - .../apache/camel/processor/MultipleHooksTest.java | 157 ++++++++ .../apache/camel/processor/SendHookStressTest.java | 2 +- .../org/apache/camel/processor/SendHookTest.java | 10 +- .../apache/camel/processor/StepHookStressTest.java | 2 +- .../org/apache/camel/processor/StepHookTest.java | 6 +- .../apache/camel/support/CompositeHookTest.java | 423 +++++++++++++++++++++ .../org/apache/camel/support/SendHelperTest.java | 5 +- .../org/apache/camel/support/StepHelperTest.java | 5 +- .../camel/management/ManagedStepHookTest.java | 2 +- .../apache/camel/support/CompositeSendHook.java | 136 +++++++ .../apache/camel/support/CompositeStepHook.java | 101 +++++ .../java/org/apache/camel/support/SendHelper.java | 71 +++- design/exchange-sanitizer.adoc | 15 +- .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 2 +- 26 files changed, 1074 insertions(+), 85 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc index 9776b4e8b418..54671d202bf6 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/chaos.adoc @@ -386,6 +386,11 @@ The steps inside `onException` and `doCatch` are steps of the route, and the sen letter channel) are sends: so `step=*` also fails the steps that handle a failure, and `endpoint=*` the sends of `onException`. Name the endpoints or steps under test in the pattern to keep them out. +Chaos shares the send hook and the step hook of camel-core with the other tools that use them: they all run, each +send and step asks every hook. The hooks of chaos have the lowest order (`Ordered.LOWEST`), so they are asked last +and run innermost, right next to the producer or the processor: a hook that observes the sends or steps sees the +faults of chaos as failures of the producer or the step. + == Route lifecycle faults `stall` and `lifecycle` act on a route, not on its messages: `route=poller stall duration=20s` suspends route @@ -667,7 +672,5 @@ expectation is decided) into the report history, where their ids can no longer b keeps the last 100 removed, replaced or ended ones. An unmet expectation of a rule that leaves the report still counts in the verdict and in `lost`. * A rule is at most 2000 characters long. -* There is one send hook and one step hook per `CamelContext`: when another one is installed already, chaos keeps it, -logs a WARN, and its rules do not fire at those hooks. * A `stall` or `lifecycle` rule acts on one route, once, or `repeat=` times (at most 100). To take a route down again after that, add the rule again. diff --git a/components/camel-chaos/src/main/docs/chaos.adoc b/components/camel-chaos/src/main/docs/chaos.adoc index 9776b4e8b418..54671d202bf6 100644 --- a/components/camel-chaos/src/main/docs/chaos.adoc +++ b/components/camel-chaos/src/main/docs/chaos.adoc @@ -386,6 +386,11 @@ The steps inside `onException` and `doCatch` are steps of the route, and the sen letter channel) are sends: so `step=*` also fails the steps that handle a failure, and `endpoint=*` the sends of `onException`. Name the endpoints or steps under test in the pattern to keep them out. +Chaos shares the send hook and the step hook of camel-core with the other tools that use them: they all run, each +send and step asks every hook. The hooks of chaos have the lowest order (`Ordered.LOWEST`), so they are asked last +and run innermost, right next to the producer or the processor: a hook that observes the sends or steps sees the +faults of chaos as failures of the producer or the step. + == Route lifecycle faults `stall` and `lifecycle` act on a route, not on its messages: `route=poller stall duration=20s` suspends route @@ -667,7 +672,5 @@ expectation is decided) into the report history, where their ids can no longer b keeps the last 100 removed, replaced or ended ones. An unmet expectation of a rule that leaves the report still counts in the verdict and in `lost`. * A rule is at most 2000 characters long. -* There is one send hook and one step hook per `CamelContext`: when another one is installed already, chaos keeps it, -logs a WARN, and its rules do not fire at those hooks. * A `stall` or `lifecycle` rule acts on one route, once, or `repeat=` times (at most 100). To take a route down again after that, add the rule again. diff --git a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java index 532f439ccdb6..67d589e5bb26 100644 --- a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java +++ b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosManager.java @@ -744,21 +744,15 @@ public final class ChaosManager { */ void install() { ExtendedCamelContext ecc = camelContext.getCamelContextExtension(); + // the hooks of chaos take the lowest order, so they run innermost, next to the producer and the processor, + // after the other hooks that may be installed (such as hooks that observe the sends and steps) try { - if (ecc.getSendHook() != null) { - LOG.warn("A SendHook is installed already ({}), so chaos rules do not fire at sends", ecc.getSendHook()); - } else { - ecc.setSendHook(new ChaosSendHook(this)); - } + ecc.addSendHook(new ChaosSendHook(this)); } catch (UnsupportedOperationException e) { LOG.warn("This CamelContext does not support a SendHook, so chaos rules do not fire at sends"); } try { - if (ecc.getStepHook() != null) { - LOG.warn("A StepHook is installed already ({}), so chaos rules do not fire at steps", ecc.getStepHook()); - } else { - ecc.setStepHook(new ChaosStepHook(this)); - } + ecc.addStepHook(new ChaosStepHook(this)); } catch (UnsupportedOperationException e) { LOG.warn("This CamelContext does not support a StepHook, so chaos rules do not fire at steps"); } diff --git a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosSendHook.java b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosSendHook.java index 17384f2bb64d..b013e4b29946 100644 --- a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosSendHook.java +++ b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosSendHook.java @@ -18,6 +18,7 @@ package org.apache.camel.component.chaos; import org.apache.camel.Endpoint; import org.apache.camel.Exchange; +import org.apache.camel.Ordered; import org.apache.camel.component.chaos.ChaosRule.Hook; import org.apache.camel.spi.SendHook; import org.apache.camel.util.TimeSource; @@ -31,7 +32,7 @@ import org.apache.camel.util.TimeSource; * rules with {@code at=reply} are asked after the producer succeeded: {@code error} fails the send after its side * effect happened, and {@code corrupt} changes the reply. */ -final class ChaosSendHook implements SendHook { +final class ChaosSendHook implements SendHook, Ordered { private final ChaosManager manager; @@ -124,6 +125,14 @@ final class ChaosSendHook implements SendHook { } } + /** + * The lowest order: the hook runs innermost, after the other hooks, right next to the producer. + */ + @Override + public int getOrder() { + return Ordered.LOWEST; + } + @Override public String toString() { return "ChaosSendHook"; diff --git a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosStepHook.java b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosStepHook.java index 501b5e866373..2cf2f5216335 100644 --- a/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosStepHook.java +++ b/components/camel-chaos/src/main/java/org/apache/camel/component/chaos/ChaosStepHook.java @@ -17,6 +17,7 @@ package org.apache.camel.component.chaos; import org.apache.camel.Exchange; +import org.apache.camel.Ordered; import org.apache.camel.component.chaos.ChaosRule.Fault; import org.apache.camel.component.chaos.ChaosRule.Hook; import org.apache.camel.spi.StepHook; @@ -28,7 +29,7 @@ import org.apache.camel.util.TimeSource; * {@code delay} (the thread that runs the step waits, through {@link TimeSource}, before it calls the processor). The * first rule that fires decides the fault of a step. */ -final class ChaosStepHook implements StepHook { +final class ChaosStepHook implements StepHook, Ordered { private final ChaosManager manager; @@ -58,6 +59,14 @@ final class ChaosStepHook implements StepHook { return PROCEED; } + /** + * The lowest order: the hook runs innermost, after the other hooks, right next to the processor of the step. + */ + @Override + public int getOrder() { + return Ordered.LOWEST; + } + @Override public String toString() { return "ChaosStepHook"; diff --git a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosActivationTest.java b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosActivationTest.java index d9d5e2223810..81eac47ddcc3 100644 --- a/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosActivationTest.java +++ b/components/camel-chaos/src/test/java/org/apache/camel/component/chaos/ChaosActivationTest.java @@ -27,6 +27,7 @@ import org.apache.camel.component.chaos.ChaosRoutePolicyFactory.ChaosRoutePolicy import org.apache.camel.component.properties.PropertiesComponent; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.spi.SendHook; +import org.apache.camel.support.CompositeSendHook; import org.apache.camel.support.component.PropertyConfigurerSupport; import org.apache.camel.util.json.JsonArray; import org.junit.jupiter.api.AfterEach; @@ -283,7 +284,7 @@ class ChaosActivationTest { } @Test - void anotherSendHookIsKept() throws Exception { + void anotherSendHookRunsWithChaos() throws Exception { context = new DefaultCamelContext(); SendHook mine = new SendHook() { @Override @@ -296,10 +297,16 @@ class ChaosActivationTest { // nothing } }; - context.getCamelContextExtension().setSendHook(mine); + context.getCamelContextExtension().addSendHook(mine); context.getPropertiesComponent().addInitialProperty("camel.chaos.enabled", "true"); context.start(); - assertThat(context.getCamelContextExtension().getSendHook()).isSameAs(mine); + // both run: the hook of chaos has the lowest order, so it is asked last, next to the producer + assertThat(context.getCamelContextExtension().getSendHook()).isInstanceOf(CompositeSendHook.class); + assertThat(((CompositeSendHook) context.getCamelContextExtension().getSendHook()).getHooks()) + .hasSize(2) + .first().isSameAs(mine); + assertThat(((CompositeSendHook) context.getCamelContextExtension().getSendHook()).getHooks().get(1)) + .isInstanceOf(ChaosSendHook.class); assertThat(context.getCamelContextExtension().getStepHook()).isInstanceOf(ChaosStepHook.class); } } diff --git a/components/camel-jta/src/test/java/org/apache/camel/jta/StepHookJtaTest.java b/components/camel-jta/src/test/java/org/apache/camel/jta/StepHookJtaTest.java index 5033ba383af9..8cd34edf222e 100644 --- a/components/camel-jta/src/test/java/org/apache/camel/jta/StepHookJtaTest.java +++ b/components/camel-jta/src/test/java/org/apache/camel/jta/StepHookJtaTest.java @@ -76,7 +76,7 @@ class StepHookJtaTest { commits.incrementAndGet(); } }); - camelContext.getCamelContextExtension().setStepHook(new FailingHook()); + camelContext.getCamelContextExtension().addStepHook(new FailingHook()); camelContext.addRoutes(new RouteBuilder() { @Override public void configure() { diff --git a/core/camel-api/src/main/java/org/apache/camel/ExtendedCamelContext.java b/core/camel-api/src/main/java/org/apache/camel/ExtendedCamelContext.java index a703b5e0d01b..05892874356b 100644 --- a/core/camel-api/src/main/java/org/apache/camel/ExtendedCamelContext.java +++ b/core/camel-api/src/main/java/org/apache/camel/ExtendedCamelContext.java @@ -521,7 +521,8 @@ public interface ExtendedCamelContext { List<InterceptStrategy> getInterceptStrategies(); /** - * Gets the {@link StepHook} that is asked before every processing step of a route. + * Gets the {@link StepHook} that is asked before every processing step of a route: the one hook that is installed, + * or a hook that asks all the installed hooks in their order (see {@link #addStepHook(StepHook)}). * * @return the hook, or <tt>null</tt> if no hook is installed * @since 4.24 @@ -531,18 +532,30 @@ public interface ExtendedCamelContext { } /** - * Installs a {@link StepHook} that is asked before every processing step of a route, or removes it with - * <tt>null</tt>. There is one hook; a new hook replaces the one installed before. A step that waits for a delay of - * the hook already is called when the delay ends. The CamelContext does not start or stop the hook, and the hook - * stays installed when the CamelContext is stopped and started again. + * Installs a {@link StepHook} that is asked before every processing step of a route. More than one hook can be + * installed: they are asked in their {@link Ordered order} (a hook that does not implement {@link Ordered} has + * order 0, and hooks with the same order are asked in the order they were added), see {@link StepHook} for how they + * combine. Adding a hook that is installed already does nothing. The CamelContext does not start or stop the hook, + * and the hook stays installed when the CamelContext is stopped and started again. * - * @param stepHook the hook, or <tt>null</tt> to remove it + * @param stepHook the hook * @since 4.24 */ - default void setStepHook(@Nullable StepHook stepHook) { + default void addStepHook(StepHook stepHook) { throw new UnsupportedOperationException("This CamelContext does not support a StepHook"); } + /** + * Removes a {@link StepHook} that was installed with {@link #addStepHook(StepHook)}; a hook that is not installed + * is ignored. A step that waits for a delay of the hook already is called when the delay ends. + * + * @param stepHook the hook + * @since 4.24 + */ + default void removeStepHook(StepHook stepHook) { + // a CamelContext without hooks has nothing to remove + } + /** * Adds a {@link StepListener} that observes each call of the processor of a step of a route. Many listeners can be * installed; adding a listener that is already installed (equal to one installed) is ignored. Whether a route calls @@ -645,7 +658,8 @@ public interface ExtendedCamelContext { /** * Gets the {@link SendHook} that runs around the sends to the producer of an endpoint that fire the - * {@link org.apache.camel.spi.CamelEvent.ExchangeSendingEvent}. + * {@link org.apache.camel.spi.CamelEvent.ExchangeSendingEvent}: the one hook that is installed, or a hook that runs + * all the installed hooks in their order (see {@link #addSendHook(SendHook)}). * * @return the hook, or <tt>null</tt> if no hook is installed * @since 4.24 @@ -656,17 +670,29 @@ public interface ExtendedCamelContext { /** * Installs a {@link SendHook} that runs around the sends to the producer of an endpoint that fire the - * {@link org.apache.camel.spi.CamelEvent.ExchangeSendingEvent}, or removes it with <tt>null</tt>. There is one - * hook; a new hook replaces the one installed before. Sends that are already in the hook complete with the hook - * that started them. + * {@link org.apache.camel.spi.CamelEvent.ExchangeSendingEvent}. More than one hook can be installed: they are asked + * before the send in their {@link Ordered order} (a hook that does not implement {@link Ordered} has order 0, and + * hooks with the same order are asked in the order they were added), and after the send in the reverse order, see + * {@link SendHook} for how they combine. Adding a hook that is installed already does nothing. * - * @param sendHook the hook, or <tt>null</tt> to remove it + * @param sendHook the hook * @since 4.24 */ - default void setSendHook(@Nullable SendHook sendHook) { + default void addSendHook(SendHook sendHook) { throw new UnsupportedOperationException("This CamelContext does not support a SendHook"); } + /** + * Removes a {@link SendHook} that was installed with {@link #addSendHook(SendHook)}; a hook that is not installed + * is ignored. Sends that are already in the hook complete with the hooks that started them. + * + * @param sendHook the hook + * @since 4.24 + */ + default void removeSendHook(SendHook sendHook) { + // a CamelContext without hooks has nothing to remove + } + /** * Internal {@link RouteController} that are only used internally by Camel to perform basic route operations. Do not * use this as end user. diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/SendHook.java b/core/camel-api/src/main/java/org/apache/camel/spi/SendHook.java index 8db73d822927..59e3e97ba7f9 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/SendHook.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/SendHook.java @@ -28,9 +28,20 @@ import org.apache.camel.Exchange; * Before the producer is called, the hook can change the message, stop the send, skip it or delay it. After the * producer is done, it can change the reply. * <p/> - * The hook is installed with {@link org.apache.camel.ExtendedCamelContext#setSendHook(SendHook)}. There is one hook per - * {@link org.apache.camel.CamelContext}; to use more than one, combine them in one hook. Without a hook, a send costs - * one check whether a hook is installed. + * The hook is installed with {@link org.apache.camel.ExtendedCamelContext#addSendHook(SendHook)}. Without a hook, a + * send costs one check whether a hook is installed. + * <p/> + * <b>More than one hook.</b> Several hooks can be installed. {@link #beforeSend(Exchange, Endpoint)} is called on them + * one after the other, in their {@link org.apache.camel.Ordered order} (lower first; a hook that does not implement + * {@link org.apache.camel.Ordered} has order 0, and hooks with the same order are asked in the order they were added). + * A hook that stops the send (it throws an exception or sets one on the exchange), skips it ({@link #SKIP}, so the + * first {@link #SKIP} wins) or returns a reserved value ends it: the hooks after it are not asked, and the send does + * not wait for a delay. Otherwise the longest of the delays wins, and the send waits for it once. + * {@link #afterSend(Exchange, Endpoint)} is called only on the hooks whose {@link #beforeSend(Exchange, Endpoint)} + * returned, in the reverse order: the hook asked first before the send is called last after it, so the hooks nest. A + * hook that only observes the sends takes a low order, so it is outside the hooks that change the outcome and sees the + * send as the route sees it; a hook that changes the outcome of a send (such as the faults of camel-chaos, with + * {@link org.apache.camel.Ordered#LOWEST}) takes a high order, to run innermost, right next to the producer. * <p/> * <b>Where it runs.</b> The hook runs right around the call of the producer, inside the error handler that handles a * failure of that producer: the error handler of the route for {@code to}, {@code toD} and {@code enrich}, the error diff --git a/core/camel-api/src/main/java/org/apache/camel/spi/StepHook.java b/core/camel-api/src/main/java/org/apache/camel/spi/StepHook.java index 15ec859c781f..065d3384118f 100644 --- a/core/camel-api/src/main/java/org/apache/camel/spi/StepHook.java +++ b/core/camel-api/src/main/java/org/apache/camel/spi/StepHook.java @@ -23,11 +23,19 @@ import org.apache.camel.Exchange; * of a route, with its node id: {@code to}, {@code process}, {@code log}, {@code split}, {@code choice} and each step * inside them, not only the {@code step} EIP. * <p/> - * The hook is installed with {@link org.apache.camel.ExtendedCamelContext#setStepHook(StepHook)}. There is one hook per - * {@link org.apache.camel.CamelContext}; to use more than one, combine them in one hook. Without a hook, a step costs - * one check whether a hook is installed (with JMX and an error handler that can redeliver, one more small processor - * inside the JMX instrumentation of the step makes that check). The {@link org.apache.camel.CamelContext} does not - * start or stop the hook, also when it is a {@link org.apache.camel.Service}: whoever installs it manages it. + * The hook is installed with {@link org.apache.camel.ExtendedCamelContext#addStepHook(StepHook)}. Without a hook, a + * step costs one check whether a hook is installed (with JMX and an error handler that can redeliver, one more small + * processor inside the JMX instrumentation of the step makes that check). The {@link org.apache.camel.CamelContext} + * does not start or stop the hook, also when it is a {@link org.apache.camel.Service}: whoever installs it manages it. + * <p/> + * <b>More than one hook.</b> Several hooks can be installed. They are asked one after the other, in their + * {@link org.apache.camel.Ordered order} (lower first; a hook that does not implement {@link org.apache.camel.Ordered} + * has order 0, and hooks with the same order are asked in the order they were added). A hook that fails the step (it + * throws an exception or sets one on the exchange) ends it: the hooks after it are not asked. The delays of the hooks + * add up, and the step waits for their sum once, after all the hooks were asked. So a hook that only observes the steps + * takes a low order, to be asked before the hooks that can fail a step; a hook that changes the outcome of a step (such + * as the faults of camel-chaos, with {@link org.apache.camel.Ordered#LOWEST}) takes a high order, to be asked last, + * right next to the processor. * <p/> * <b>Where it is asked.</b> Each step runs through its {@link org.apache.camel.Channel}, which runs its advices (such * as tracing, the debuggers and JMX) and calls the error handler of the step, which calls the processor of the step. diff --git a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java index 7f31883855bc..776be4df8db4 100644 --- a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java +++ b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java @@ -86,6 +86,8 @@ import org.apache.camel.spi.TransformerRegistry; import org.apache.camel.spi.TypeConverterRegistry; import org.apache.camel.spi.UuidGenerator; import org.apache.camel.spi.ValidatorRegistry; +import org.apache.camel.support.CompositeSendHook; +import org.apache.camel.support.CompositeStepHook; import org.apache.camel.support.EndpointHelper; import org.apache.camel.support.NormalizedUri; import org.apache.camel.support.PluginHelper; @@ -127,7 +129,10 @@ class DefaultCamelContextExtension implements ExtendedCamelContext { private volatile ManagementMBeanAssembler managementMBeanAssembler; private volatile HeadersMapFactory headersMapFactory; private volatile boolean eventNotificationApplicable; + // null, the one hook, or a composite of the hooks: the senders read this field only private volatile SendHook sendHook; + // the installed hooks, in the order they were added (guarded by the lock) + private final List<SendHook> sendHooks = new ArrayList<>(); private volatile CamelContextNameStrategy nameStrategy; private volatile ManagementNameStrategy managementNameStrategy; private volatile PropertiesComponent propertiesComponent; @@ -141,7 +146,10 @@ class DefaultCamelContextExtension implements ExtendedCamelContext { private volatile ErrorRegistry errorRegistry; private volatile UuidGenerator uuidGenerator; private volatile Tracer tracer; + // null, the one hook, or a composite of the hooks: the steps read this field only private volatile StepHook stepHook; + // the installed hooks, in the order they were added (guarded by the lock) + private final List<StepHook> stepHooks = new ArrayList<>(); private volatile TransformerRegistry transformerRegistry; private volatile ValidatorRegistry validatorRegistry; private volatile TypeConverterRegistry typeConverterRegistry; @@ -536,8 +544,29 @@ class DefaultCamelContextExtension implements ExtendedCamelContext { } @Override - public void setStepHook(StepHook stepHook) { - this.stepHook = stepHook; + public void addStepHook(StepHook stepHook) { + ObjectHelper.notNull(stepHook, "stepHook"); + lock.lock(); + try { + if (stepHooks.stream().noneMatch(h -> h == stepHook)) { + stepHooks.add(stepHook); + this.stepHook = CompositeStepHook.of(stepHooks); + } + } finally { + lock.unlock(); + } + } + + @Override + public void removeStepHook(StepHook stepHook) { + lock.lock(); + try { + if (stepHooks.removeIf(h -> h == stepHook)) { + this.stepHook = CompositeStepHook.of(stepHooks); + } + } finally { + lock.unlock(); + } } @Override @@ -595,8 +624,29 @@ class DefaultCamelContextExtension implements ExtendedCamelContext { } @Override - public void setSendHook(SendHook sendHook) { - this.sendHook = sendHook; + public void addSendHook(SendHook sendHook) { + ObjectHelper.notNull(sendHook, "sendHook"); + lock.lock(); + try { + if (sendHooks.stream().noneMatch(h -> h == sendHook)) { + sendHooks.add(sendHook); + this.sendHook = CompositeSendHook.of(sendHooks); + } + } finally { + lock.unlock(); + } + } + + @Override + public void removeSendHook(SendHook sendHook) { + lock.lock(); + try { + if (sendHooks.removeIf(h -> h == sendHook)) { + this.sendHook = CompositeSendHook.of(sendHooks); + } + } finally { + lock.unlock(); + } } @Override diff --git a/core/camel-core-engine/src/generated/java/org/apache/camel/impl/ExtendedCamelContextConfigurer.java b/core/camel-core-engine/src/generated/java/org/apache/camel/impl/ExtendedCamelContextConfigurer.java index 8dd15e260652..1112afcc55e6 100644 --- a/core/camel-core-engine/src/generated/java/org/apache/camel/impl/ExtendedCamelContextConfigurer.java +++ b/core/camel-core-engine/src/generated/java/org/apache/camel/impl/ExtendedCamelContextConfigurer.java @@ -54,12 +54,8 @@ public class ExtendedCamelContextConfigurer extends org.apache.camel.support.com case "reactiveexecutor": case "reactiveExecutor": target.setReactiveExecutor(property(camelContext, org.apache.camel.spi.ReactiveExecutor.class, value)); return true; case "registry": target.setRegistry(property(camelContext, org.apache.camel.spi.Registry.class, value)); return true; - case "sendhook": - case "sendHook": target.setSendHook(property(camelContext, org.apache.camel.spi.SendHook.class, value)); return true; case "startupsteprecorder": case "startupStepRecorder": target.setStartupStepRecorder(property(camelContext, org.apache.camel.spi.StartupStepRecorder.class, value)); return true; - case "stephook": - case "stepHook": target.setStepHook(property(camelContext, org.apache.camel.spi.StepHook.class, value)); return true; default: return false; } } @@ -97,12 +93,8 @@ public class ExtendedCamelContextConfigurer extends org.apache.camel.support.com case "reactiveexecutor": case "reactiveExecutor": return org.apache.camel.spi.ReactiveExecutor.class; case "registry": return org.apache.camel.spi.Registry.class; - case "sendhook": - case "sendHook": return org.apache.camel.spi.SendHook.class; case "startupsteprecorder": case "startupStepRecorder": return org.apache.camel.spi.StartupStepRecorder.class; - case "stephook": - case "stepHook": return org.apache.camel.spi.StepHook.class; default: return null; } } @@ -141,12 +133,8 @@ public class ExtendedCamelContextConfigurer extends org.apache.camel.support.com case "reactiveexecutor": case "reactiveExecutor": return target.getReactiveExecutor(); case "registry": return target.getRegistry(); - case "sendhook": - case "sendHook": return target.getSendHook(); case "startupsteprecorder": case "startupStepRecorder": return target.getStartupStepRecorder(); - case "stephook": - case "stepHook": return target.getStepHook(); default: return null; } } diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/MultipleHooksTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/MultipleHooksTest.java new file mode 100644 index 000000000000..999418413b15 --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/processor/MultipleHooksTest.java @@ -0,0 +1,157 @@ +/* + * 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.processor; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.camel.CamelContext; +import org.apache.camel.ContextTestSupport; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.apache.camel.Ordered; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.spi.SendHook; +import org.apache.camel.spi.StepHook; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Two step hooks and two send hooks active together (CAMEL-26607): a hook that fails the first attempt, as chaos does, + * with the lowest order, and a hook that records, with order 0. Both run, and the recording hook sees each attempt of + * the error handler, with the failure of the other hook. + */ +class MultipleHooksTest extends ContextTestSupport { + + private final List<String> recorded = new CopyOnWriteArrayList<>(); + private final AtomicInteger stepFailures = new AtomicInteger(); + private final AtomicInteger sendFailures = new AtomicInteger(); + + @Override + protected CamelContext createCamelContext() throws Exception { + CamelContext context = super.createCamelContext(); + // the failing hooks are added first, but their order puts them last + context.getCamelContextExtension().addStepHook(new FailingStepHook()); + context.getCamelContextExtension().addStepHook(new RecordingStepHook()); + context.getCamelContextExtension().addSendHook(new FailingSendHook()); + context.getCamelContextExtension().addSendHook(new RecordingSendHook()); + return context; + } + + @Override + protected RouteBuilder createRouteBuilder() { + return new RouteBuilder() { + @Override + public void configure() { + errorHandler(defaultErrorHandler().maximumRedeliveries(2).redeliveryDelay(0)); + + from("direct:start").routeId("start") + .process(e -> e.getMessage().setHeader("worked", true)).id("work") + .to("mock:result").id("toResult"); + } + }; + } + + @Test + void testBothHooksRun() throws Exception { + getMockEndpoint("mock:result").expectedBodiesReceived("Hello"); + getMockEndpoint("mock:result").expectedHeaderReceived("worked", true); + + template.sendBody("direct:start", "Hello"); + + assertMockEndpointsSatisfied(); + // the failing hooks failed the first attempt of the step and of the send, and the error handler redelivered + assertEquals(1, stepFailures.get()); + assertEquals(1, sendFailures.get()); + // the recording hooks were asked first, for each attempt, and saw the failure of the send after it + assertEquals(List.of( + "step:work", "step:work", + "step:toResult", + "before:mock://result", "after:mock://result:failed", + "step:toResult", + "before:mock://result", "after:mock://result:ok"), + recorded); + } + + private final class RecordingStepHook implements StepHook { + + @Override + public long beforeStep(Exchange exchange, String nodeId) { + recorded.add("step:" + nodeId); + return PROCEED; + } + } + + private final class FailingStepHook implements StepHook, Ordered { + + @Override + public long beforeStep(Exchange exchange, String nodeId) { + if ("work".equals(nodeId) && stepFailures.get() == 0) { + stepFailures.incrementAndGet(); + exchange.setException(new IllegalStateException("Chaos at the step")); + } + return PROCEED; + } + + @Override + public int getOrder() { + return Ordered.LOWEST; + } + } + + private final class RecordingSendHook implements SendHook { + + @Override + public long beforeSend(Exchange exchange, Endpoint endpoint) { + if (endpoint.getEndpointUri().startsWith("mock:")) { + recorded.add("before:" + endpoint.getEndpointUri()); + } + return PROCEED; + } + + @Override + public void afterSend(Exchange exchange, Endpoint endpoint) { + if (endpoint.getEndpointUri().startsWith("mock:")) { + recorded.add("after:" + endpoint.getEndpointUri() + (exchange.getException() != null ? ":failed" : ":ok")); + } + } + } + + private final class FailingSendHook implements SendHook, Ordered { + + @Override + public long beforeSend(Exchange exchange, Endpoint endpoint) { + if ("mock://result".equals(endpoint.getEndpointUri()) && sendFailures.get() == 0) { + sendFailures.incrementAndGet(); + exchange.setException(new IllegalStateException("Chaos at the send")); + } + return PROCEED; + } + + @Override + public void afterSend(Exchange exchange, Endpoint endpoint) { + // nothing + } + + @Override + public int getOrder() { + return Ordered.LOWEST; + } + } +} diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/SendHookStressTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/SendHookStressTest.java index 6fc199518110..42bb97d0fdef 100644 --- a/core/camel-core/src/test/java/org/apache/camel/processor/SendHookStressTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/processor/SendHookStressTest.java @@ -77,7 +77,7 @@ class SendHookStressTest extends ContextTestSupport { } } }); - context.getCamelContextExtension().setSendHook(new RandomHook()); + context.getCamelContextExtension().addSendHook(new RandomHook()); return context; } diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/SendHookTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/SendHookTest.java index 48faab98af3b..0392ac3561e3 100644 --- a/core/camel-core/src/test/java/org/apache/camel/processor/SendHookTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/processor/SendHookTest.java @@ -77,7 +77,7 @@ class SendHookTest extends ContextTestSupport { } } }); - context.getCamelContextExtension().setSendHook(hook); + context.getCamelContextExtension().addSendHook(hook); return context; } @@ -283,21 +283,21 @@ class SendHookTest extends ContextTestSupport { String hooked = SendHelper.class.getName() + "$SendHookProducer"; // no hook: the error handler of the recipient calls the producer itself, as when there is no send hook at all - context.getCamelContextExtension().setSendHook(null); + context.getCamelContextExtension().removeSendHook(hook); template.sendBodyAndHeader("direct:recipientList", "Hello", "to", "mock:rl"); assertTrue(stack.get(stack.size() - 1).startsWith(RedeliveryErrorHandler.class.getName()), stack::toString); assertFalse(stack.contains(hooked), stack::toString); // a hook installed later runs for the same recipient, through another error handler stack.clear(); - context.getCamelContextExtension().setSendHook(hook); + context.getCamelContextExtension().addSendHook(hook); template.sendBodyAndHeader("direct:recipientList", "Hello", "to", "mock:rl"); assertTrue(stack.contains(hooked), stack::toString); assertEquals(1, hook.before("mock://rl")); // and when it is removed again, the producer is called by itself again stack.clear(); - context.getCamelContextExtension().setSendHook(null); + context.getCamelContextExtension().removeSendHook(hook); template.sendBodyAndHeader("direct:recipientList", "Hello", "to", "mock:rl"); assertFalse(stack.contains(hooked), stack::toString); assertEquals(1, hook.before("mock://rl")); @@ -411,7 +411,7 @@ class SendHookTest extends ContextTestSupport { assertEquals(List.of("sending:mock://a", "before:mock://a", "after:mock://a", "sent:mock://a"), calls("mock://a")); - context.getCamelContextExtension().setSendHook(null); + context.getCamelContextExtension().removeSendHook(hook); assertNull(context.getCamelContextExtension().getSendHook()); List<String> withoutHook = runAllPaths(); diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/StepHookStressTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/StepHookStressTest.java index 702fec327057..d160c94ab0ee 100644 --- a/core/camel-core/src/test/java/org/apache/camel/processor/StepHookStressTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/processor/StepHookStressTest.java @@ -75,7 +75,7 @@ class StepHookStressTest extends ContextTestSupport { } } }); - context.getCamelContextExtension().setStepHook(new RandomHook()); + context.getCamelContextExtension().addStepHook(new RandomHook()); return context; } diff --git a/core/camel-core/src/test/java/org/apache/camel/processor/StepHookTest.java b/core/camel-core/src/test/java/org/apache/camel/processor/StepHookTest.java index 220865fe3042..45352c755363 100644 --- a/core/camel-core/src/test/java/org/apache/camel/processor/StepHookTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/processor/StepHookTest.java @@ -59,7 +59,7 @@ class StepHookTest extends ContextTestSupport { @Override protected CamelContext createCamelContext() throws Exception { CamelContext context = super.createCamelContext(); - context.getCamelContextExtension().setStepHook(hook); + context.getCamelContextExtension().addStepHook(hook); return context; } @@ -434,7 +434,7 @@ class StepHookTest extends ContextTestSupport { @Test void testNoHook() throws Exception { - context.getCamelContextExtension().setStepHook(null); + context.getCamelContextExtension().removeStepHook(hook); failures.put("flaky", -1); getMockEndpoint("mock:flaky").expectedBodiesReceived("Hello"); getMockEndpoint("mock:dead").expectedMessageCount(0); @@ -447,7 +447,7 @@ class StepHookTest extends ContextTestSupport { assertTrue(asked.isEmpty(), asked::toString); // installed again, it is asked by the routes that already run - context.getCamelContextExtension().setStepHook(hook); + context.getCamelContextExtension().addStepHook(hook); template.sendBody("direct:steps", "Hello"); assertEquals(List.of("setHeader", "log", "process", "toResult"), new ArrayList<>(asked)); } diff --git a/core/camel-core/src/test/java/org/apache/camel/support/CompositeHookTest.java b/core/camel-core/src/test/java/org/apache/camel/support/CompositeHookTest.java new file mode 100644 index 000000000000..885ab249fc9a --- /dev/null +++ b/core/camel-core/src/test/java/org/apache/camel/support/CompositeHookTest.java @@ -0,0 +1,423 @@ +/* + * 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.support; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.apache.camel.AsyncProcessor; +import org.apache.camel.ContextTestSupport; +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.apache.camel.ExtendedCamelContext; +import org.apache.camel.Ordered; +import org.apache.camel.spi.SendHook; +import org.apache.camel.spi.StepHook; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * More than one {@link StepHook} and {@link SendHook} (CAMEL-26607): the composites the context installs, and how + * {@link StepHelper} and {@link SendHelper} run them. + */ +class CompositeHookTest extends ContextTestSupport { + + // what the hooks and the producer did, in the order they did it + private final List<String> calls = new CopyOnWriteArrayList<>(); + + @Override + public boolean isUseRouteBuilder() { + return false; + } + + // the step hooks + + @Test + void testStepHooksInstalledAndRemoved() { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + assertNull(ecc.getStepHook()); + + StepHook a = new TestStepHook("a", 0, PROCEED_STEP); + StepHook b = new TestStepHook("b", 0, PROCEED_STEP); + ecc.addStepHook(a); + assertSame(a, ecc.getStepHook(), "one hook is installed as it is"); + // the same hook again changes nothing + ecc.addStepHook(a); + assertSame(a, ecc.getStepHook()); + + ecc.addStepHook(b); + CompositeStepHook composite = assertInstanceOf(CompositeStepHook.class, ecc.getStepHook()); + assertEquals(List.of(a, b), composite.getHooks()); + + ecc.removeStepHook(a); + assertSame(b, ecc.getStepHook()); + // a hook that is not installed is ignored + ecc.removeStepHook(a); + assertSame(b, ecc.getStepHook()); + ecc.removeStepHook(b); + assertNull(ecc.getStepHook()); + } + + @Test + void testStepHooksAskedInOrder() throws Exception { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + // added out of order: sorted by Ordered, lower first, a hook without Ordered counts as 0, and hooks with the + // same order keep the order they were added + ecc.addStepHook(new TestStepHook("late", 10, PROCEED_STEP)); + ecc.addStepHook(new UnorderedStepHook("plain1")); + ecc.addStepHook(new TestStepHook("early", -5, PROCEED_STEP)); + ecc.addStepHook(new UnorderedStepHook("plain2")); + ecc.addStepHook(new TestStepHook("zero", 0, PROCEED_STEP)); + ecc.addStepHook(new TestStepHook("last", Ordered.LOWEST, PROCEED_STEP)); + + Exchange exchange = new DefaultExchange(context); + StepHelper.process(ecc.getStepHook(), exchange, "step1", processor()); + + assertEquals(List.of("early:step1", "plain1:step1", "plain2:step1", "zero:step1", "late:step1", "last:step1", + "processor"), calls); + assertNull(exchange.getException()); + } + + @Test + void testStepHookThatThrowsStopsTheChain() throws Exception { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + ecc.addStepHook(new TestStepHook("first", 1, PROCEED_STEP)); + ecc.addStepHook(new TestStepHook("throws", 2, THROW)); + ecc.addStepHook(new TestStepHook("third", 3, PROCEED_STEP)); + + Exchange exchange = new DefaultExchange(context); + StepHelper.process(ecc.getStepHook(), exchange, "step1", processor()); + + // the later hook is not asked, and the processor is not called + assertEquals(List.of("first:step1", "throws:step1"), calls); + assertEquals("throws failed the step", exchange.getException().getMessage()); + } + + @Test + void testStepHookThatSetsAnExceptionStopsTheChain() throws Exception { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + ecc.addStepHook(new TestStepHook("fails", 1, SET_EXCEPTION)); + ecc.addStepHook(new TestStepHook("second", 2, PROCEED_STEP)); + + Exchange exchange = new DefaultExchange(context); + StepHelper.process(ecc.getStepHook(), exchange, "step1", processor()); + + assertEquals(List.of("fails:step1"), calls); + assertEquals("fails failed the step", exchange.getException().getMessage()); + } + + @Test + void testStepHookWithReservedValueStopsTheChain() throws Exception { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + ecc.addStepHook(new TestStepHook("reserved", 1, -7)); + ecc.addStepHook(new TestStepHook("second", 2, PROCEED_STEP)); + + Exchange exchange = new DefaultExchange(context); + StepHelper.process(ecc.getStepHook(), exchange, "step1", processor()); + + assertEquals(List.of("reserved:step1"), calls); + assertInstanceOf(IllegalArgumentException.class, exchange.getException()); + } + + @Test + void testStepHookDelaysAddUp() { + StepHook composite = CompositeStepHook.of(List.of( + new TestStepHook("a", 1, 10), + new TestStepHook("b", 2, PROCEED_STEP), + new TestStepHook("c", 3, 25))); + + assertEquals(35, composite.beforeStep(new DefaultExchange(context), "step1")); + assertEquals(List.of("a:step1", "b:step1", "c:step1"), calls); + + // and do not overflow + StepHook huge = CompositeStepHook.of(List.of( + new TestStepHook("x", 1, Long.MAX_VALUE - 1), new TestStepHook("y", 2, 10))); + assertEquals(Long.MAX_VALUE, huge.beforeStep(new DefaultExchange(context), "step1")); + } + + @Test + void testStepHookOfNoneOrOne() { + assertNull(CompositeStepHook.of(List.of())); + StepHook one = new TestStepHook("one", 0, PROCEED_STEP); + assertSame(one, CompositeStepHook.of(List.of(one))); + } + + // the send hooks + + @Test + void testSendHooksInstalledAndRemoved() { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + assertNull(ecc.getSendHook()); + + SendHook a = new TestSendHook("a", 0, SendHook.PROCEED); + SendHook b = new TestSendHook("b", 0, SendHook.PROCEED); + ecc.addSendHook(a); + assertSame(a, ecc.getSendHook()); + ecc.addSendHook(a); + assertSame(a, ecc.getSendHook()); + ecc.addSendHook(b); + CompositeSendHook composite = assertInstanceOf(CompositeSendHook.class, ecc.getSendHook()); + assertEquals(List.of(a, b), composite.getHooks()); + + ecc.removeSendHook(b); + assertSame(a, ecc.getSendHook()); + ecc.removeSendHook(a); + assertNull(ecc.getSendHook()); + } + + @Test + void testSendHooksInOrderAndAfterInReverse() { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + ecc.addSendHook(new TestSendHook("c", 3, SendHook.PROCEED)); + ecc.addSendHook(new TestSendHook("a", 1, SendHook.PROCEED)); + ecc.addSendHook(new TestSendHook("b", 2, SendHook.PROCEED)); + + Exchange exchange = send(); + + assertEquals(List.of("before:a", "before:b", "before:c", "producer", "after:c", "after:b", "after:a"), calls); + assertNull(exchange.getException()); + } + + @Test + void testFirstSkipWins() { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + ecc.addSendHook(new TestSendHook("a", 1, SendHook.PROCEED)); + ecc.addSendHook(new TestSendHook("skip", 2, SendHook.SKIP)); + ecc.addSendHook(new TestSendHook("c", 3, SendHook.PROCEED)); + + Exchange exchange = send(); + + // the later hook is not asked, so it is not called after the send either; the producer is not called + assertEquals(List.of("before:a", "before:skip", "after:skip", "after:a"), calls); + assertNull(exchange.getException()); + } + + @Test + void testSendHookThatThrowsStopsTheChain() { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + ecc.addSendHook(new TestSendHook("a", 1, SendHook.PROCEED)); + ecc.addSendHook(new TestSendHook("throws", 2, THROW)); + ecc.addSendHook(new TestSendHook("c", 3, SendHook.PROCEED)); + + Exchange exchange = send(); + + // the hook that threw is not called after the send (its before did not return), the one asked before it is + assertEquals(List.of("before:a", "before:throws", "after:a"), calls); + assertEquals("throws stopped the send", exchange.getException().getMessage()); + } + + @Test + void testSendHookThatSetsAnExceptionStopsTheChain() { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + ecc.addSendHook(new TestSendHook("a", 1, SendHook.PROCEED)); + ecc.addSendHook(new TestSendHook("stops", 2, SET_EXCEPTION)); + ecc.addSendHook(new TestSendHook("c", 3, SendHook.PROCEED)); + + Exchange exchange = send(); + + assertEquals(List.of("before:a", "before:stops", "after:stops", "after:a"), calls); + assertEquals("stops stopped the send", exchange.getException().getMessage()); + } + + @Test + void testSendHookWithReservedValueStopsTheChain() { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + ecc.addSendHook(new TestSendHook("a", 1, SendHook.PROCEED)); + ecc.addSendHook(new TestSendHook("reserved", 2, -99)); + ecc.addSendHook(new TestSendHook("c", 3, SendHook.PROCEED)); + + Exchange exchange = send(); + + assertEquals(List.of("before:a", "before:reserved", "after:reserved", "after:a"), calls); + assertInstanceOf(IllegalArgumentException.class, exchange.getException()); + } + + @Test + void testAfterSendThatThrowsDoesNotStopTheOthers() { + ExtendedCamelContext ecc = context.getCamelContextExtension(); + ecc.addSendHook(new TestSendHook("a", 1, SendHook.PROCEED)); + ecc.addSendHook(new TestSendHook("b", 2, SendHook.PROCEED, true)); + + Exchange exchange = send(); + + assertEquals(List.of("before:a", "before:b", "producer", "after:b", "after:a"), calls); + assertEquals("b failed after the send", exchange.getException().getMessage()); + } + + @Test + void testLongestSendDelayWins() { + SendHook composite = CompositeSendHook.of(List.of( + new TestSendHook("a", 1, 10), + new TestSendHook("b", 2, 40), + new TestSendHook("c", 3, 20))); + Endpoint endpoint = context.getEndpoint("mock:target"); + + assertEquals(40, composite.beforeSend(new DefaultExchange(context), endpoint)); + assertEquals(List.of("before:a", "before:b", "before:c"), calls); + } + + @Test + void testCompositeCalledDirectly() { + SendHook composite = CompositeSendHook.of(List.of( + new TestSendHook("a", 1, SendHook.PROCEED), + new TestSendHook("skip", 2, SendHook.SKIP), + new TestSendHook("c", 3, SendHook.PROCEED))); + Endpoint endpoint = context.getEndpoint("mock:target"); + Exchange exchange = new DefaultExchange(context); + + // the first SKIP wins; called directly, afterSend calls every hook in the reverse order + assertEquals(SendHook.SKIP, composite.beforeSend(exchange, endpoint)); + composite.afterSend(exchange, endpoint); + assertEquals(List.of("before:a", "before:skip", "after:c", "after:skip", "after:a"), calls); + } + + @Test + void testSendHookOfNoneOrOne() { + assertNull(CompositeSendHook.of(List.of())); + SendHook one = new TestSendHook("one", 0, SendHook.PROCEED); + assertSame(one, CompositeSendHook.of(List.of(one))); + } + + private Exchange send() { + Endpoint endpoint = context.getEndpoint("mock:target"); + Exchange exchange = new DefaultExchange(context); + boolean[] done = new boolean[1]; + boolean sync = SendHelper.process(exchange, endpoint, producer(), doneSync -> done[0] = true); + assertTrue(sync); + assertTrue(done[0], "the callback is called"); + return exchange; + } + + private AsyncProcessor processor() { + return AsyncProcessorConverterHelper.convert(e -> calls.add("processor")); + } + + private AsyncProcessor producer() { + return AsyncProcessorConverterHelper.convert(e -> calls.add("producer")); + } + + // the values a test hook returns that are not a delay + private static final long PROCEED_STEP = StepHook.PROCEED; + private static final long THROW = Long.MIN_VALUE; + private static final long SET_EXCEPTION = Long.MIN_VALUE + 1; + + private final class TestStepHook implements StepHook, Ordered { + + private final String name; + private final int order; + private final long answer; + + private TestStepHook(String name, int order, long answer) { + this.name = name; + this.order = order; + this.answer = answer; + } + + @Override + public long beforeStep(Exchange exchange, String nodeId) { + calls.add(name + ":" + nodeId); + if (answer == THROW) { + throw new IllegalStateException(name + " failed the step"); + } + if (answer == SET_EXCEPTION) { + exchange.setException(new IllegalStateException(name + " failed the step")); + return PROCEED; + } + return answer; + } + + @Override + public int getOrder() { + return order; + } + + @Override + public String toString() { + return name; + } + } + + private final class UnorderedStepHook implements StepHook { + + private final String name; + + private UnorderedStepHook(String name) { + this.name = name; + } + + @Override + public long beforeStep(Exchange exchange, String nodeId) { + calls.add(name + ":" + nodeId); + return PROCEED; + } + } + + private final class TestSendHook implements SendHook, Ordered { + + private final String name; + private final int order; + private final long answer; + private final boolean afterThrows; + + private TestSendHook(String name, int order, long answer) { + this(name, order, answer, false); + } + + private TestSendHook(String name, int order, long answer, boolean afterThrows) { + this.name = name; + this.order = order; + this.answer = answer; + this.afterThrows = afterThrows; + } + + @Override + public long beforeSend(Exchange exchange, Endpoint endpoint) { + calls.add("before:" + name); + if (answer == THROW) { + throw new IllegalStateException(name + " stopped the send"); + } + if (answer == SET_EXCEPTION) { + exchange.setException(new IllegalStateException(name + " stopped the send")); + return PROCEED; + } + return answer; + } + + @Override + public void afterSend(Exchange exchange, Endpoint endpoint) { + calls.add("after:" + name); + if (afterThrows) { + throw new IllegalStateException(name + " failed after the send"); + } + } + + @Override + public int getOrder() { + return order; + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/core/camel-core/src/test/java/org/apache/camel/support/SendHelperTest.java b/core/camel-core/src/test/java/org/apache/camel/support/SendHelperTest.java index 12e14d8c9fc7..dd2f99f599f2 100644 --- a/core/camel-core/src/test/java/org/apache/camel/support/SendHelperTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/support/SendHelperTest.java @@ -68,6 +68,7 @@ class SendHelperTest extends ContextTestSupport { private static final String PRODUCER_COUNT = "producerCount"; private static final String FIXED_DELAY = "fixedDelay"; + private final ModeHook modeHook = new ModeHook(); private ExecutorService producerThreads; private Endpoint endpoint; @@ -94,7 +95,7 @@ class SendHelperTest extends ContextTestSupport { super.setUp(); producerThreads = Executors.newFixedThreadPool(4); endpoint = context.getEndpoint("mock:target"); - context.getCamelContextExtension().setSendHook(new ModeHook()); + context.getCamelContextExtension().addSendHook(modeHook); } @Override @@ -229,7 +230,7 @@ class SendHelperTest extends ContextTestSupport { @Test void testNoHookCallsTheProducerOnly() { - context.getCamelContextExtension().setSendHook(null); + context.getCamelContextExtension().removeSendHook(modeHook); Exchange exchange = newExchange(Mode.STOP, Kind.SYNC); Completion completion = new Completion(); diff --git a/core/camel-core/src/test/java/org/apache/camel/support/StepHelperTest.java b/core/camel-core/src/test/java/org/apache/camel/support/StepHelperTest.java index 66ef28b2120b..a6feae743409 100644 --- a/core/camel-core/src/test/java/org/apache/camel/support/StepHelperTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/support/StepHelperTest.java @@ -65,6 +65,7 @@ class StepHelperTest extends ContextTestSupport { private static final String FIXED_DELAY = "fixedDelay"; private static final String RESERVED = "StepHook.beforeStep returned -5: negative values are reserved"; + private final ModeHook modeHook = new ModeHook(); private ExecutorService processorThreads; enum Mode { @@ -86,7 +87,7 @@ class StepHelperTest extends ContextTestSupport { public void setUp() throws Exception { super.setUp(); processorThreads = Executors.newFixedThreadPool(4); - context.getCamelContextExtension().setStepHook(new ModeHook()); + context.getCamelContextExtension().addStepHook(modeHook); } @Override @@ -282,7 +283,7 @@ class StepHelperTest extends ContextTestSupport { @Test void testNoHookCallsTheProcessorOnly() { - context.getCamelContextExtension().setStepHook(null); + context.getCamelContextExtension().removeStepHook(modeHook); Exchange exchange = newExchange(Mode.FAIL, Kind.SYNC); Completion completion = new Completion(); diff --git a/core/camel-management/src/test/java/org/apache/camel/management/ManagedStepHookTest.java b/core/camel-management/src/test/java/org/apache/camel/management/ManagedStepHookTest.java index c5ee80a84bd6..0fef39db794a 100644 --- a/core/camel-management/src/test/java/org/apache/camel/management/ManagedStepHookTest.java +++ b/core/camel-management/src/test/java/org/apache/camel/management/ManagedStepHookTest.java @@ -53,7 +53,7 @@ class ManagedStepHookTest extends ManagementTestSupport { @Override protected CamelContext createCamelContext() throws Exception { CamelContext context = super.createCamelContext(); - context.getCamelContextExtension().setStepHook(this::beforeStep); + context.getCamelContextExtension().addStepHook(this::beforeStep); return context; } diff --git a/core/camel-support/src/main/java/org/apache/camel/support/CompositeSendHook.java b/core/camel-support/src/main/java/org/apache/camel/support/CompositeSendHook.java new file mode 100644 index 000000000000..33847a8832b6 --- /dev/null +++ b/core/camel-support/src/main/java/org/apache/camel/support/CompositeSendHook.java @@ -0,0 +1,136 @@ +/* + * 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.support; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.camel.Endpoint; +import org.apache.camel.Exchange; +import org.apache.camel.Ordered; +import org.apache.camel.spi.SendHook; + +/** + * A {@link SendHook} that runs several hooks, in their {@link Ordered order} (lower first; a hook that does not + * implement {@link Ordered} has order 0, and hooks with the same order keep the order they were given in). + * <p/> + * {@link SendHelper} runs the hooks of a composite one by one, as follows: + * <ul> + * <li>{@link #beforeSend(Exchange, Endpoint)} is called on the hooks in their order. A hook that throws an exception, + * sets one on the exchange, returns {@link SendHook#SKIP} or a reserved negative value ends it: the hooks after it are + * not asked. So the first {@link SendHook#SKIP} wins.</li> + * <li>When all the hooks proceed, the longest of their delays wins: the send waits for it, once.</li> + * <li>{@link #afterSend(Exchange, Endpoint)} is called only on the hooks whose {@code beforeSend} returned, in the + * reverse order, so the hook that was asked first is called last, as for nested hooks.</li> + * </ul> + * <p/> + * Code that calls a composite itself does not know which hooks were asked: its {@code beforeSend} combines the hooks as + * above, and its {@code afterSend} calls every hook, in the reverse order. + * <p/> + * It is immutable: the {@link org.apache.camel.CamelContext} creates a new one when a hook is added or removed (see + * {@link org.apache.camel.ExtendedCamelContext#addSendHook(SendHook)}). + * + * @since 4.24 + */ +public final class CompositeSendHook implements SendHook { + + private final SendHook[] hooks; + + private CompositeSendHook(SendHook[] hooks) { + this.hooks = hooks; + } + + /** + * The hook to install for the given hooks: {@code null} for none, the hook itself for one, or else a composite that + * runs them in their order. + * + * @param hooks the hooks, in the order they were added + * @return the hook, or {@code null} + */ + public static SendHook of(Collection<? extends SendHook> hooks) { + if (hooks.isEmpty()) { + return null; + } + SendHook[] sorted = hooks.toArray(new SendHook[0]); + if (sorted.length == 1) { + return sorted[0]; + } + // a stable sort, so hooks with the same order keep the order they were added + Arrays.sort(sorted, OrderedComparator.get()); + return new CompositeSendHook(sorted); + } + + /** + * The hooks, in the order {@code beforeSend} is called. + */ + public List<SendHook> getHooks() { + return List.of(hooks); + } + + /** + * The hooks for {@link SendHelper}, which must not change the array. + */ + SendHook[] hooks() { + return hooks; + } + + @Override + public long beforeSend(Exchange exchange, Endpoint endpoint) { + final Exception existing = exchange.getException(); + long answer = PROCEED; + for (SendHook hook : hooks) { + long value = hook.beforeSend(exchange, endpoint); + Exception exception = exchange.getException(); + if (exception != null && exception != existing) { + // the hook stopped the send: the return value is ignored + return PROCEED; + } + if (value < 0) { + // SKIP, or a reserved value that fails the send + return value; + } + answer = Math.max(answer, value); + } + return answer; + } + + @Override + public void afterSend(Exchange exchange, Endpoint endpoint) { + RuntimeException failure = null; + for (int i = hooks.length - 1; i >= 0; i--) { + try { + hooks[i].afterSend(exchange, endpoint); + } catch (RuntimeException e) { + // the other hooks are still called + if (failure == null) { + failure = e; + } else if (failure != e) { + failure.addSuppressed(e); + } + } + } + if (failure != null) { + throw failure; + } + } + + @Override + public String toString() { + return "CompositeSendHook" + Arrays.toString(hooks); + } +} diff --git a/core/camel-support/src/main/java/org/apache/camel/support/CompositeStepHook.java b/core/camel-support/src/main/java/org/apache/camel/support/CompositeStepHook.java new file mode 100644 index 000000000000..af0d3bd424d6 --- /dev/null +++ b/core/camel-support/src/main/java/org/apache/camel/support/CompositeStepHook.java @@ -0,0 +1,101 @@ +/* + * 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.support; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.apache.camel.Exchange; +import org.apache.camel.Ordered; +import org.apache.camel.spi.StepHook; + +/** + * A {@link StepHook} that asks several hooks, in their {@link Ordered order} (lower first; a hook that does not + * implement {@link Ordered} has order 0, and hooks with the same order keep the order they were given in). + * <p/> + * The hooks are asked one after the other: a hook that throws an exception, or sets one on the exchange, fails the + * step, and the hooks after it are not asked. A hook that returns a reserved negative value fails the step as well. The + * delays of the hooks add up: the step waits for their sum, once, after all the hooks were asked. + * <p/> + * It is immutable: the {@link org.apache.camel.CamelContext} creates a new one when a hook is added or removed (see + * {@link org.apache.camel.ExtendedCamelContext#addStepHook(StepHook)}). + * + * @since 4.24 + */ +public final class CompositeStepHook implements StepHook { + + private final StepHook[] hooks; + + private CompositeStepHook(StepHook[] hooks) { + this.hooks = hooks; + } + + /** + * The hook to install for the given hooks: {@code null} for none, the hook itself for one, or else a composite that + * asks them in their order. + * + * @param hooks the hooks, in the order they were added + * @return the hook, or {@code null} + */ + public static StepHook of(Collection<? extends StepHook> hooks) { + if (hooks.isEmpty()) { + return null; + } + StepHook[] sorted = hooks.toArray(new StepHook[0]); + if (sorted.length == 1) { + return sorted[0]; + } + // a stable sort, so hooks with the same order keep the order they were added + Arrays.sort(sorted, OrderedComparator.get()); + return new CompositeStepHook(sorted); + } + + /** + * The hooks, in the order they are asked. + */ + public List<StepHook> getHooks() { + return List.of(hooks); + } + + @Override + public long beforeStep(Exchange exchange, String nodeId) { + // an exchange can reach a step with an exception on it already, which does not fail the step + final Exception existing = exchange.getException(); + long total = PROCEED; + for (StepHook hook : hooks) { + // a hook that throws fails the step (the caller sets the exception), and the later hooks are not asked + long delay = hook.beforeStep(exchange, nodeId); + Exception exception = exchange.getException(); + if (exception != null && exception != existing) { + // the hook failed the step: the return value is ignored + return PROCEED; + } + if (delay < 0) { + // reserved: the caller fails the step + return delay; + } + total = delay > Long.MAX_VALUE - total ? Long.MAX_VALUE : total + delay; + } + return total; + } + + @Override + public String toString() { + return "CompositeStepHook" + Arrays.toString(hooks); + } +} diff --git a/core/camel-support/src/main/java/org/apache/camel/support/SendHelper.java b/core/camel-support/src/main/java/org/apache/camel/support/SendHelper.java index 8964c0dcc63a..cefce803acb4 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/SendHelper.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/SendHelper.java @@ -167,18 +167,32 @@ public final class SendHelper { * One send with a hook. The hook is called before the producer, the producer is called right away, after a delay * (that this thread waits for), or not at all, and then the hook is called after it and the callback is called, * each exactly once. + * <p/> + * With several hooks (a {@link CompositeSendHook}) the hooks are asked one by one, see {@link CompositeSendHook}: + * the send keeps how many were asked, so only those are called after it, in the reverse order. */ private static final class HookedSend implements AsyncCallback { + // the one hook, or null when there are several private final SendHook hook; + // the hooks of a composite, in the order they are asked, or null when there is one + private final SendHook[] hooks; private final Exchange exchange; private final Endpoint endpoint; private final AsyncProcessor producer; private final AsyncCallback callback; + // how many hooks returned from beforeSend: they, and only they, are called after the send + private int asked; private HookedSend(SendHook hook, Exchange exchange, Endpoint endpoint, AsyncProcessor producer, AsyncCallback callback) { - this.hook = hook; + if (hook instanceof CompositeSendHook composite) { + this.hook = null; + this.hooks = composite.hooks(); + } else { + this.hook = hook; + this.hooks = null; + } this.exchange = exchange; this.endpoint = endpoint; this.producer = producer; @@ -190,10 +204,12 @@ public final class SendHelper { final Exception existing = exchange.getException(); final long delay; try { - delay = hook.beforeSend(exchange, endpoint); + delay = beforeSend(existing); } catch (Exception e) { - // the hook failed: the producer is not called, and there is no after + // a hook failed: the producer is not called, and there is no after for that hook (the hooks asked + // before it are called after the send, as for any send they proceeded with) exchange.setException(e); + afterSend(); callback.done(true); return true; } @@ -239,6 +255,38 @@ public final class SendHelper { } } + /** + * Asks the hooks before the send. + * + * @param existing the exception the exchange had before the send + * @return what the hooks decided: {@link SendHook#PROCEED}, a delay, {@link SendHook#SKIP} or a + * reserved value (ignored when a hook set an exception on the exchange) + */ + private long beforeSend(Exception existing) { + if (hooks == null) { + long answer = hook.beforeSend(exchange, endpoint); + asked = 1; + return answer; + } + long answer = SendHook.PROCEED; + for (SendHook h : hooks) { + long value = h.beforeSend(exchange, endpoint); + asked++; + Exception exception = exchange.getException(); + if (exception != null && exception != existing) { + // this hook stopped the send: the later hooks are not asked + return SendHook.PROCEED; + } + if (value < 0) { + // the first SKIP wins (or a reserved value fails the send): the later hooks are not asked + return value; + } + // the longest delay wins + answer = Math.max(answer, value); + } + return answer; + } + private boolean completeWithoutProducer() { afterSend(); callback.done(true); @@ -254,9 +302,24 @@ public final class SendHelper { callback.done(doneSync); } + /** + * Calls the hooks that were asked before the send, in the reverse order. + */ private void afterSend() { + if (hooks == null) { + if (asked > 0) { + afterSend(hook); + } + return; + } + for (int i = asked - 1; i >= 0; i--) { + afterSend(hooks[i]); + } + } + + private void afterSend(SendHook h) { try { - hook.afterSend(exchange, endpoint); + h.afterSend(exchange, endpoint); } catch (Exception e) { Exception existing = exchange.getException(); if (existing == null) { diff --git a/design/exchange-sanitizer.adoc b/design/exchange-sanitizer.adoc index 660871616c78..11346fd0c622 100644 --- a/design/exchange-sanitizer.adoc +++ b/design/exchange-sanitizer.adoc @@ -507,19 +507,18 @@ of the cached view it gets from the context extension, and goes on. The JMH run === Relation to StepHook -`StepHook` (CAMEL-26338) acts: it can delay or fail a step, before the processor is called. There is one per -`CamelContext`, because two hooks that act on the same step need a rule for how they combine (chaos logs a WARN and does -not fire at steps when another hook is installed). The step listener only observes, also after the call (R1 and R3 are +`StepHook` (CAMEL-26338) acts: it can delay or fail a step, before the processor is called. Two hooks that act on +the same step need a rule for how they combine (see below). The step listener only observes, also after the call (R1 and R3 are about what happens after it), so any number can be installed with no rule for combining them. The hook is asked first, outside the `InterceptStrategy` wrappers; if it fails the step, the processor is not called and the listeners see no call (`StepHelper` calls the callback, which R1 does not check). The `StepHook` stays as it is: it cannot be expressed on a listener, which cannot act. A later version could let chaos count its faults through a listener; that is not proposed here. -Making the `StepHook` composable is future work, outside this design. Acting hooks can compose with a fixed rule: ask -them in the order they were added, stop at the first that fails the step, and wait for the sum of the delays. The -Javadoc of `StepHook` already tells users to combine their hooks in one; a composite in camel-support, installed in the -one slot, would do that for them, and chaos would then no longer need its WARN. The sanitizer does not need it. +Since CAMEL-26607 more than one `StepHook` (and `SendHook`) can be installed, with `addStepHook`: the hooks compose +with a fixed rule, ask them in their `Ordered` order, stop at the first that fails the step, and wait for the sum of +the delays (a `CompositeStepHook` of camel-support, kept in the one field the steps read). Chaos takes the lowest +order, so it is asked last, and no longer needs a WARN. The sanitizer does not need a hook. === The sanitizer module @@ -713,7 +712,7 @@ Each step is its own pull request after this document: attempt, and R2 and R4 are not about steps, so nothing would be left for the advice to do; the wrapper is needed at every step either way. Two mechanisms for the same steps would cost more than one. The hybrid becomes worth it if a later rule needs one event per step only. -* *The `StepHook`.* One per context, and before the call only (see Relation to StepHook). +* *The `StepHook`.* It acts, and before the call only (see Relation to StepHook). * *Listener calls in `StepHelper`, next to the hook.* `StepHelper` is called just outside the chosen wrapper in most cases: by the error handler for each attempt, or by the `StepHook` processor or the channel. Every step already reads the `stepHook` field there, so holding the hook and the listeners in one field would add no read per diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index 2e2652c1fa71..b0057373f13b 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -7175,7 +7175,7 @@ an older offset, so the consumer can process again the messages after that offse === camel-core - a hook before each step of a route -A new SPI `org.apache.camel.spi.StepHook`, set with `ExtendedCamelContext.setStepHook`, can fail or delay each step of +A new SPI `org.apache.camel.spi.StepHook`, added with `ExtendedCamelContext.addStepHook`, can fail or delay each step of a route. `ErrorHandlerRedeliveryCustomizer` has a new default method `useStepHook(String)`, so existing implementations need no change. A custom error handler that wants a failure from the hook handled like a failure of the step's processor (with `onException` and redelivery) can override it.
