GH-8981: Add UnicastingDispatcher.failoverStrategy option
Fixes: #8981 Sometime the simple `boolean failover` on the `MessageChannel` (default `true`) is not enough to be sure that we can dispatch to the next handler or not. Such a decision can be made using `ErrorMessageExceptionTypeRouter`, but that would require an overhaul for the whole integration flow * Introduce a simple `Predicate<Exception> failoverStrategy` into `UnicastingDispatcher` and all its `MessageChannel` implementation consumers to allow to make a decision about next failover according to a thrown exception from the current `MessageHandler` * Expose `failoverStrategy` on the `DirectChannel`, `ExecutorChannel` & `PartitionedChannel`, and add it into respective Java DSL specs * Fix involved tests to rely on the `failoverStrategy` property from now on * Document the new feature
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.integration.channel;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.integration.dispatcher.LoadBalancingStrategy;
|
||||
import org.springframework.integration.dispatcher.RoundRobinLoadBalancingStrategy;
|
||||
import org.springframework.integration.dispatcher.UnicastingDispatcher;
|
||||
@@ -60,12 +62,26 @@ public class DirectChannel extends AbstractSubscribableChannel {
|
||||
/**
|
||||
* Specify whether the channel's dispatcher should have failover enabled.
|
||||
* By default, it will. Set this value to 'false' to disable it.
|
||||
* Overrides {@link #setFailoverStrategy(Predicate)} option.
|
||||
* In other words: or this, or that option has to be set.
|
||||
* @param failover The failover boolean.
|
||||
*/
|
||||
public void setFailover(boolean failover) {
|
||||
this.dispatcher.setFailover(failover);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a strategy whether the channel's dispatcher should have failover enabled
|
||||
* for the exception thrown.
|
||||
* Overrides {@link #setFailover(boolean)} option.
|
||||
* In other words: or this, or that option has to be set.
|
||||
* @param failoverStrategy The failover boolean.
|
||||
* @since 6.3
|
||||
*/
|
||||
public void setFailoverStrategy(Predicate<Exception> failoverStrategy) {
|
||||
this.dispatcher.setFailoverStrategy(failoverStrategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the maximum number of subscribers supported by the
|
||||
* channel's dispatcher.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.integration.channel;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.integration.dispatcher.LoadBalancingStrategy;
|
||||
import org.springframework.integration.dispatcher.RoundRobinLoadBalancingStrategy;
|
||||
@@ -49,7 +50,7 @@ public class ExecutorChannel extends AbstractExecutorChannel {
|
||||
|
||||
private final LoadBalancingStrategy loadBalancingStrategy;
|
||||
|
||||
private boolean failover = true;
|
||||
private Predicate<Exception> failoverStrategy = (exception) -> true;
|
||||
|
||||
/**
|
||||
* Create an ExecutorChannel that delegates to the provided
|
||||
@@ -88,8 +89,20 @@ public class ExecutorChannel extends AbstractExecutorChannel {
|
||||
* @param failover The failover boolean.
|
||||
*/
|
||||
public void setFailover(boolean failover) {
|
||||
this.failover = failover;
|
||||
getDispatcher().setFailover(failover);
|
||||
setFailoverStrategy((exception) -> failover);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a strategy whether the channel's dispatcher should have failover enabled
|
||||
* for the exception thrown.
|
||||
* Overrides {@link #setFailover(boolean)} option.
|
||||
* In other words: or this, or that option has to be set.
|
||||
* @param failoverStrategy The failover boolean.
|
||||
* @since 6.3
|
||||
*/
|
||||
public void setFailoverStrategy(Predicate<Exception> failoverStrategy) {
|
||||
this.failoverStrategy = failoverStrategy;
|
||||
getDispatcher().setFailoverStrategy(failoverStrategy);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,7 +120,7 @@ public class ExecutorChannel extends AbstractExecutorChannel {
|
||||
this.executor = new ErrorHandlingTaskExecutor(this.executor, errorHandler);
|
||||
}
|
||||
UnicastingDispatcher unicastingDispatcher = new UnicastingDispatcher(this.executor);
|
||||
unicastingDispatcher.setFailover(this.failover);
|
||||
unicastingDispatcher.setFailoverStrategy(this.failoverStrategy);
|
||||
if (this.maxSubscribers == null) {
|
||||
this.maxSubscribers = getIntegrationProperties().getChannelsMaxUnicastSubscribers();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.integration.channel;
|
||||
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.integration.IntegrationMessageHeaderAccessor;
|
||||
import org.springframework.integration.dispatcher.LoadBalancingStrategy;
|
||||
@@ -99,6 +100,18 @@ public class PartitionedChannel extends AbstractExecutorChannel {
|
||||
getDispatcher().setFailover(failover);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a strategy whether the channel's dispatcher should have failover enabled
|
||||
* for the exception thrown.
|
||||
* Overrides {@link #setFailover(boolean)} option.
|
||||
* In other words: or this, or that option has to be set.
|
||||
* @param failoverStrategy The failover boolean.
|
||||
* @since 6.3
|
||||
*/
|
||||
public void setFailoverStrategy(Predicate<Exception> failoverStrategy) {
|
||||
getDispatcher().setFailoverStrategy(failoverStrategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a {@link LoadBalancingStrategy} for the {@link PartitionedDispatcher}.
|
||||
* @param loadBalancingStrategy The load balancing strategy implementation.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,6 +28,7 @@ import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.integration.util.ErrorHandlingTaskExecutor;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -67,7 +68,7 @@ public class PartitionedDispatcher extends AbstractDispatcher {
|
||||
|
||||
private ThreadFactory threadFactory = new CustomizableThreadFactory("partition-thread-");
|
||||
|
||||
private boolean failover = true;
|
||||
private Predicate<Exception> failoverStrategy = (exception) -> true;
|
||||
|
||||
@Nullable
|
||||
private LoadBalancingStrategy loadBalancingStrategy;
|
||||
@@ -108,7 +109,20 @@ public class PartitionedDispatcher extends AbstractDispatcher {
|
||||
* @param failover The failover boolean.
|
||||
*/
|
||||
public void setFailover(boolean failover) {
|
||||
this.failover = failover;
|
||||
setFailoverStrategy((exception) -> failover);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a strategy whether the channel's dispatcher should have failover enabled
|
||||
* for the exception thrown.
|
||||
* Overrides {@link #setFailover(boolean)} option.
|
||||
* In other words: or this, or that option has to be set.
|
||||
* @param failoverStrategy The failover boolean.
|
||||
* @since 6.3
|
||||
*/
|
||||
public void setFailoverStrategy(Predicate<Exception> failoverStrategy) {
|
||||
Assert.notNull(failoverStrategy, "'failoverStrategy' must not be null");
|
||||
this.failoverStrategy = failoverStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,7 +193,7 @@ public class PartitionedDispatcher extends AbstractDispatcher {
|
||||
this.executors.add(executor);
|
||||
DelegateDispatcher delegateDispatcher =
|
||||
new DelegateDispatcher(new ErrorHandlingTaskExecutor(executor, this.errorHandler));
|
||||
delegateDispatcher.setFailover(this.failover);
|
||||
delegateDispatcher.setFailoverStrategy(this.failoverStrategy);
|
||||
delegateDispatcher.setLoadBalancingStrategy(this.loadBalancingStrategy);
|
||||
delegateDispatcher.setMessageHandlingTaskDecorator(this.messageHandlingTaskDecorator);
|
||||
return delegateDispatcher;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,6 +21,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.integration.MessageDispatchingException;
|
||||
import org.springframework.integration.support.utils.IntegrationUtils;
|
||||
@@ -58,7 +59,7 @@ public class UnicastingDispatcher extends AbstractDispatcher {
|
||||
|
||||
private final Executor executor;
|
||||
|
||||
private boolean failover = true;
|
||||
private Predicate<Exception> failoverStrategy = (exception) -> true;
|
||||
|
||||
private LoadBalancingStrategy loadBalancingStrategy;
|
||||
|
||||
@@ -77,10 +78,25 @@ public class UnicastingDispatcher extends AbstractDispatcher {
|
||||
* Specify whether this dispatcher should failover when a single
|
||||
* {@link MessageHandler} throws an Exception. The default value is
|
||||
* <code>true</code>.
|
||||
* Overrides {@link #setFailoverStrategy(Predicate)} option.
|
||||
* In other words: or this, or that option has to be set.
|
||||
* @param failover The failover boolean.
|
||||
*/
|
||||
public void setFailover(boolean failover) {
|
||||
this.failover = failover;
|
||||
setFailoverStrategy((exception) -> failover);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a strategy whether the channel's dispatcher should have failover enabled
|
||||
* for the exception thrown.
|
||||
* Overrides {@link #setFailover(boolean)} option.
|
||||
* In other words: or this, or that option has to be set.
|
||||
* @param failoverStrategy The failover boolean.
|
||||
* @since 6.3
|
||||
*/
|
||||
public void setFailoverStrategy(Predicate<Exception> failoverStrategy) {
|
||||
Assert.notNull(failoverStrategy, "'failoverStrategy' must not be null");
|
||||
this.failoverStrategy = failoverStrategy;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,10 +170,15 @@ public class UnicastingDispatcher extends AbstractDispatcher {
|
||||
}
|
||||
exceptions.add(runtimeException);
|
||||
boolean isLast = !handlerIterator.hasNext();
|
||||
if (!isLast && this.failover) {
|
||||
boolean failover = this.failoverStrategy.test(ex);
|
||||
|
||||
if (!isLast && failover) {
|
||||
logExceptionBeforeFailOver(ex, handler, message);
|
||||
}
|
||||
handleExceptions(exceptions, message, isLast);
|
||||
|
||||
if (isLast || !failover) {
|
||||
handleExceptions(exceptions, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
return success;
|
||||
@@ -187,22 +208,12 @@ public class UnicastingDispatcher extends AbstractDispatcher {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles Exceptions that occur while dispatching. If this dispatcher has
|
||||
* failover enabled, it will only throw an Exception when the handler list
|
||||
* is exhausted. The 'isLast' flag will be <em>true</em> if the
|
||||
* Exception occurred during the final iteration of the MessageHandlers.
|
||||
* If failover is disabled for this dispatcher, it will re-throw any
|
||||
* Exception immediately.
|
||||
*/
|
||||
private void handleExceptions(List<RuntimeException> allExceptions, Message<?> message, boolean isLast) {
|
||||
if (isLast || !this.failover) {
|
||||
if (allExceptions.size() == 1) {
|
||||
throw allExceptions.get(0);
|
||||
}
|
||||
throw new AggregateMessageDeliveryException(message,
|
||||
"All attempts to deliver Message to MessageHandlers failed.", allExceptions);
|
||||
private void handleExceptions(List<RuntimeException> allExceptions, Message<?> message) {
|
||||
if (allExceptions.size() == 1) {
|
||||
throw allExceptions.get(0);
|
||||
}
|
||||
throw new AggregateMessageDeliveryException(message,
|
||||
"All attempts to deliver Message to MessageHandlers failed.", allExceptions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2020 the original author or authors.
|
||||
* Copyright 2016-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,8 +28,8 @@ public class DirectChannelSpec extends LoadBalancingChannelSpec<DirectChannelSpe
|
||||
@Override
|
||||
protected DirectChannel doGet() {
|
||||
this.channel = new DirectChannel(this.loadBalancingStrategy);
|
||||
if (this.failover != null) {
|
||||
this.channel.setFailover(this.failover);
|
||||
if (this.failoverStrategy != null) {
|
||||
this.channel.setFailoverStrategy(this.failoverStrategy);
|
||||
}
|
||||
if (this.maxSubscribers != null) {
|
||||
this.channel.setMaxSubscribers(this.maxSubscribers);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2020 the original author or authors.
|
||||
* Copyright 2016-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -36,8 +36,8 @@ public class ExecutorChannelSpec extends LoadBalancingChannelSpec<ExecutorChanne
|
||||
@Override
|
||||
protected ExecutorChannel doGet() {
|
||||
this.channel = new ExecutorChannel(this.executor, this.loadBalancingStrategy);
|
||||
if (this.failover != null) {
|
||||
this.channel.setFailover(this.failover);
|
||||
if (this.failoverStrategy != null) {
|
||||
this.channel.setFailoverStrategy(this.failoverStrategy);
|
||||
}
|
||||
if (this.maxSubscribers != null) {
|
||||
this.channel.setMaxSubscribers(this.maxSubscribers);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2019 the original author or authors.
|
||||
* Copyright 2016-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.integration.dsl;
|
||||
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.springframework.integration.channel.AbstractMessageChannel;
|
||||
import org.springframework.integration.dispatcher.LoadBalancingStrategy;
|
||||
import org.springframework.integration.dispatcher.RoundRobinLoadBalancingStrategy;
|
||||
@@ -34,7 +36,7 @@ public abstract class LoadBalancingChannelSpec<S extends MessageChannelSpec<S, C
|
||||
|
||||
protected LoadBalancingStrategy loadBalancingStrategy = new RoundRobinLoadBalancingStrategy(); // NOSONAR
|
||||
|
||||
protected Boolean failover; // NOSONAR
|
||||
protected Predicate<Exception> failoverStrategy; // NOSONAR
|
||||
|
||||
protected Integer maxSubscribers; // NOSONAR
|
||||
|
||||
@@ -46,8 +48,20 @@ public abstract class LoadBalancingChannelSpec<S extends MessageChannelSpec<S, C
|
||||
return _this();
|
||||
}
|
||||
|
||||
public S failover(Boolean failoverToSet) {
|
||||
this.failover = failoverToSet;
|
||||
public S failover(boolean failoverToSet) {
|
||||
return failoverStrategy((exception) -> failoverToSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a strategy whether the channel's dispatcher should have failover enabled
|
||||
* for the exception thrown.
|
||||
* Overrides {@link #failover(boolean)} option.
|
||||
* In other words: or this, or that option has to be set.
|
||||
* @param failoverStrategy The failover boolean.
|
||||
* @since 6.3
|
||||
*/
|
||||
public S failoverStrategy(Predicate<Exception> failoverStrategy) {
|
||||
this.failoverStrategy = failoverStrategy;
|
||||
return _this();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
* Copyright 2023-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -63,8 +63,8 @@ public class PartitionedChannelSpec extends LoadBalancingChannelSpec<Partitioned
|
||||
this.channel = new PartitionedChannel(this.partitionCount);
|
||||
}
|
||||
this.channel.setLoadBalancingStrategy(this.loadBalancingStrategy);
|
||||
if (this.failover != null) {
|
||||
this.channel.setFailover(this.failover);
|
||||
if (this.failoverStrategy != null) {
|
||||
this.channel.setFailoverStrategy(this.failoverStrategy);
|
||||
}
|
||||
if (this.maxSubscribers != null) {
|
||||
this.channel.setMaxSubscribers(this.maxSubscribers);
|
||||
|
||||
@@ -21,6 +21,8 @@ import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
@@ -76,7 +78,16 @@ public class PartitionedChannelTests {
|
||||
|
||||
MultiValueMap<String, Message<?>> partitionedMessages = new LinkedMultiValueMap<>();
|
||||
|
||||
partitionedChannel.subscribe((message) -> partitionedMessages.add(Thread.currentThread().getName(), message));
|
||||
Lock partitionsLock = new ReentrantLock();
|
||||
partitionedChannel.subscribe((message) -> {
|
||||
partitionsLock.lock();
|
||||
try {
|
||||
partitionedMessages.add(Thread.currentThread().getName(), message);
|
||||
}
|
||||
finally {
|
||||
partitionsLock.unlock();
|
||||
}
|
||||
});
|
||||
|
||||
partitionedChannel.send(MessageBuilder.withPayload("test1").setHeader("partitionKey", "1").build());
|
||||
partitionedChannel.send(MessageBuilder.withPayload("test2").setHeader("partitionKey", "2").build());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,9 +19,9 @@ package org.springframework.integration.channel.config;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -37,18 +37,20 @@ import org.springframework.integration.util.ErrorHandlingTaskExecutor;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Oleg Zhurakousky
|
||||
* @author Artem Bilan
|
||||
*
|
||||
* @since 1.0.3
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
@SpringJUnitConfig
|
||||
public class DispatchingChannelParserTests {
|
||||
|
||||
@Autowired
|
||||
@@ -60,50 +62,60 @@ public class DispatchingChannelParserTests {
|
||||
@Test
|
||||
public void taskExecutorOnly() {
|
||||
MessageChannel channel = channels.get("taskExecutorOnly");
|
||||
assertThat(channel.getClass()).isEqualTo(ExecutorChannel.class);
|
||||
assertThat(channel).isInstanceOf(ExecutorChannel.class);
|
||||
Object executor = getDispatcherProperty("executor", channel);
|
||||
assertThat(executor.getClass()).isEqualTo(ErrorHandlingTaskExecutor.class);
|
||||
assertThat(executor).isInstanceOf(ErrorHandlingTaskExecutor.class);
|
||||
assertThat(new DirectFieldAccessor(executor).getPropertyValue("executor"))
|
||||
.isSameAs(context.getBean("taskExecutor"));
|
||||
assertThat((Boolean) getDispatcherProperty("failover", channel)).isTrue();
|
||||
assertThat(getDispatcherProperty("loadBalancingStrategy", channel).getClass())
|
||||
.isEqualTo(RoundRobinLoadBalancingStrategy.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Predicate<Exception> failoverStrategy = (Predicate<Exception>) getDispatcherProperty("failoverStrategy", channel);
|
||||
assertThat(failoverStrategy.test(mock())).isTrue();
|
||||
assertThat(getDispatcherProperty("loadBalancingStrategy", channel))
|
||||
.isInstanceOf(RoundRobinLoadBalancingStrategy.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failoverFalse() {
|
||||
MessageChannel channel = channels.get("failoverFalse");
|
||||
assertThat(channel.getClass()).isEqualTo(DirectChannel.class);
|
||||
assertThat((Boolean) getDispatcherProperty("failover", channel)).isFalse();
|
||||
assertThat(getDispatcherProperty("loadBalancingStrategy", channel).getClass())
|
||||
.isEqualTo(RoundRobinLoadBalancingStrategy.class);
|
||||
assertThat(channel).isInstanceOf(DirectChannel.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Predicate<Exception> failoverStrategy = (Predicate<Exception>) getDispatcherProperty("failoverStrategy", channel);
|
||||
assertThat(failoverStrategy.test(mock())).isFalse();
|
||||
assertThat(getDispatcherProperty("loadBalancingStrategy", channel))
|
||||
.isInstanceOf(RoundRobinLoadBalancingStrategy.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failoverTrue() {
|
||||
MessageChannel channel = channels.get("failoverTrue");
|
||||
assertThat(channel.getClass()).isEqualTo(DirectChannel.class);
|
||||
assertThat((Boolean) getDispatcherProperty("failover", channel)).isTrue();
|
||||
assertThat(getDispatcherProperty("loadBalancingStrategy", channel).getClass())
|
||||
.isEqualTo(RoundRobinLoadBalancingStrategy.class);
|
||||
assertThat(channel).isInstanceOf(DirectChannel.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Predicate<Exception> failoverStrategy = (Predicate<Exception>) getDispatcherProperty("failoverStrategy", channel);
|
||||
assertThat(failoverStrategy.test(mock())).isTrue();
|
||||
assertThat(getDispatcherProperty("loadBalancingStrategy", channel))
|
||||
.isInstanceOf(RoundRobinLoadBalancingStrategy.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadBalancerDisabled() {
|
||||
MessageChannel channel = channels.get("loadBalancerDisabled");
|
||||
assertThat(channel.getClass()).isEqualTo(DirectChannel.class);
|
||||
assertThat((Boolean) getDispatcherProperty("failover", channel)).isTrue();
|
||||
assertThat(channel).isInstanceOf(DirectChannel.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Predicate<Exception> failoverStrategy = (Predicate<Exception>) getDispatcherProperty("failoverStrategy", channel);
|
||||
assertThat(failoverStrategy.test(mock())).isTrue();
|
||||
assertThat(getDispatcherProperty("loadBalancingStrategy", channel)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadBalancerDisabledAndTaskExecutor() {
|
||||
MessageChannel channel = channels.get("loadBalancerDisabledAndTaskExecutor");
|
||||
assertThat(channel.getClass()).isEqualTo(ExecutorChannel.class);
|
||||
assertThat((Boolean) getDispatcherProperty("failover", channel)).isTrue();
|
||||
assertThat(channel).isInstanceOf(ExecutorChannel.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Predicate<Exception> failoverStrategy = (Predicate<Exception>) getDispatcherProperty("failoverStrategy", channel);
|
||||
assertThat(failoverStrategy.test(mock())).isTrue();
|
||||
assertThat(getDispatcherProperty("loadBalancingStrategy", channel)).isNull();
|
||||
Object executor = getDispatcherProperty("executor", channel);
|
||||
assertThat(executor.getClass()).isEqualTo(ErrorHandlingTaskExecutor.class);
|
||||
assertThat(executor).isInstanceOf(ErrorHandlingTaskExecutor.class);
|
||||
assertThat(new DirectFieldAccessor(executor).getPropertyValue("executor"))
|
||||
.isSameAs(context.getBean("taskExecutor"));
|
||||
}
|
||||
@@ -111,12 +123,14 @@ public class DispatchingChannelParserTests {
|
||||
@Test
|
||||
public void roundRobinLoadBalancerAndTaskExecutor() {
|
||||
MessageChannel channel = channels.get("roundRobinLoadBalancerAndTaskExecutor");
|
||||
assertThat(channel.getClass()).isEqualTo(ExecutorChannel.class);
|
||||
assertThat((Boolean) getDispatcherProperty("failover", channel)).isTrue();
|
||||
assertThat(getDispatcherProperty("loadBalancingStrategy", channel).getClass())
|
||||
.isEqualTo(RoundRobinLoadBalancingStrategy.class);
|
||||
assertThat(channel).isInstanceOf(ExecutorChannel.class);
|
||||
@SuppressWarnings("unchecked")
|
||||
Predicate<Exception> failoverStrategy = (Predicate<Exception>) getDispatcherProperty("failoverStrategy", channel);
|
||||
assertThat(failoverStrategy.test(mock())).isTrue();
|
||||
assertThat(getDispatcherProperty("loadBalancingStrategy", channel))
|
||||
.isInstanceOf(RoundRobinLoadBalancingStrategy.class);
|
||||
Object executor = getDispatcherProperty("executor", channel);
|
||||
assertThat(executor.getClass()).isEqualTo(ErrorHandlingTaskExecutor.class);
|
||||
assertThat(executor).isInstanceOf(ErrorHandlingTaskExecutor.class);
|
||||
assertThat(new DirectFieldAccessor(executor).getPropertyValue("executor"))
|
||||
.isSameAs(context.getBean("taskExecutor"));
|
||||
}
|
||||
@@ -126,19 +140,15 @@ public class DispatchingChannelParserTests {
|
||||
MessageChannel channel = channels.get("lbRefChannel");
|
||||
LoadBalancingStrategy lbStrategy = TestUtils.getPropertyValue(channel, "dispatcher.loadBalancingStrategy",
|
||||
LoadBalancingStrategy.class);
|
||||
assertThat(lbStrategy instanceof SampleLoadBalancingStrategy).isTrue();
|
||||
assertThat(lbStrategy).isInstanceOf(SampleLoadBalancingStrategy.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loadBalancerRefFailWithLoadBalancer() {
|
||||
|
||||
try {
|
||||
new ClassPathXmlApplicationContext("ChannelWithLoadBalancerRef-fail-config.xml", this.getClass()).close();
|
||||
}
|
||||
catch (BeanDefinitionParsingException e) {
|
||||
assertThat(e.getMessage()).contains("'load-balancer' and 'load-balancer-ref' are mutually exclusive");
|
||||
}
|
||||
|
||||
assertThatExceptionOfType(BeanDefinitionParsingException.class)
|
||||
.isThrownBy(() -> new ClassPathXmlApplicationContext("ChannelWithLoadBalancerRef-fail-config.xml",
|
||||
getClass()))
|
||||
.withMessageContaining("'load-balancer' and 'load-balancer-ref' are mutually exclusive");
|
||||
}
|
||||
|
||||
private static Object getDispatcherProperty(String propertyName, MessageChannel channel) {
|
||||
@@ -153,5 +163,7 @@ public class DispatchingChannelParserTests {
|
||||
public Iterator<MessageHandler> getHandlerIterator(Message<?> message, Collection<MessageHandler> handlers) {
|
||||
return handlers.iterator();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,7 +20,7 @@ import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.integration.MessageRejectedException;
|
||||
@@ -32,6 +32,7 @@ import org.springframework.messaging.MessageHandler;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
@@ -69,12 +70,8 @@ public class FailOverDispatcherTests {
|
||||
MessageHandler target = new CountingTestEndpoint(counter, false);
|
||||
dispatcher.addHandler(target);
|
||||
dispatcher.addHandler(target);
|
||||
try {
|
||||
dispatcher.dispatch(new GenericMessage<>("test"));
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
assertThatExceptionOfType(MessageRejectedException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(new GenericMessage<>("test")));
|
||||
assertThat(counter.get()).as("target should not have duplicate subscriptions").isEqualTo(1);
|
||||
}
|
||||
|
||||
@@ -89,12 +86,8 @@ public class FailOverDispatcherTests {
|
||||
dispatcher.addHandler(target2);
|
||||
dispatcher.addHandler(target3);
|
||||
dispatcher.removeHandler(target2);
|
||||
try {
|
||||
dispatcher.dispatch(new GenericMessage<>("test"));
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
assertThatExceptionOfType(AggregateMessageDeliveryException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(new GenericMessage<>("test")));
|
||||
assertThat(counter.get()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@@ -108,46 +101,31 @@ public class FailOverDispatcherTests {
|
||||
dispatcher.addHandler(target1);
|
||||
dispatcher.addHandler(target2);
|
||||
dispatcher.addHandler(target3);
|
||||
try {
|
||||
dispatcher.dispatch(new GenericMessage<>("test1"));
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
assertThatExceptionOfType(AggregateMessageDeliveryException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(new GenericMessage<>("test1")));
|
||||
assertThat(counter.get()).isEqualTo(3);
|
||||
dispatcher.removeHandler(target2);
|
||||
try {
|
||||
dispatcher.dispatch(new GenericMessage<>("test2"));
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
assertThatExceptionOfType(AggregateMessageDeliveryException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(new GenericMessage<>("test2")));
|
||||
assertThat(counter.get()).isEqualTo(5);
|
||||
dispatcher.removeHandler(target1);
|
||||
try {
|
||||
dispatcher.dispatch(new GenericMessage<>("test3"));
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
assertThatExceptionOfType(MessageRejectedException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(new GenericMessage<>("test3")));
|
||||
assertThat(counter.get()).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test(expected = MessageDeliveryException.class)
|
||||
@Test
|
||||
public void removeConsumerLastTargetCausesDeliveryException() {
|
||||
UnicastingDispatcher dispatcher = new UnicastingDispatcher();
|
||||
final AtomicInteger counter = new AtomicInteger();
|
||||
MessageHandler target = new CountingTestEndpoint(counter, false);
|
||||
dispatcher.addHandler(target);
|
||||
try {
|
||||
dispatcher.dispatch(new GenericMessage<>("test1"));
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
assertThatExceptionOfType(MessageRejectedException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(new GenericMessage<>("test1")));
|
||||
assertThat(counter.get()).isEqualTo(1);
|
||||
dispatcher.removeHandler(target);
|
||||
dispatcher.dispatch(new GenericMessage<>("test2"));
|
||||
assertThatExceptionOfType(MessageDeliveryException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(new GenericMessage<>("test2")));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -188,15 +166,26 @@ public class FailOverDispatcherTests {
|
||||
dispatcher.addHandler(target1);
|
||||
dispatcher.addHandler(target2);
|
||||
dispatcher.addHandler(target3);
|
||||
try {
|
||||
assertThat(dispatcher.dispatch(new GenericMessage<>("test"))).isFalse();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// ignore
|
||||
}
|
||||
assertThatExceptionOfType(AggregateMessageDeliveryException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(new GenericMessage<>("test")));
|
||||
assertThat(counter.get()).as("each target should have been invoked").isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failoverStrategyRejects() {
|
||||
UnicastingDispatcher dispatcher = new UnicastingDispatcher();
|
||||
dispatcher.setFailoverStrategy((exception) -> !(exception instanceof MessageRejectedException));
|
||||
AtomicInteger counter = new AtomicInteger();
|
||||
MessageHandler target1 = new CountingTestEndpoint(counter, false);
|
||||
MessageHandler target2 = new CountingTestEndpoint(counter, true);
|
||||
MessageHandler target3 = new CountingTestEndpoint(counter, true);
|
||||
dispatcher.addHandler(target1);
|
||||
dispatcher.addHandler(target2);
|
||||
dispatcher.addHandler(target3);
|
||||
assertThatExceptionOfType(MessageRejectedException.class)
|
||||
.isThrownBy(() -> dispatcher.dispatch(new GenericMessage<>("test")));
|
||||
assertThat(counter.get()).as("only first should have been invoked").isEqualTo(1);
|
||||
}
|
||||
|
||||
private static ServiceActivatingHandler createConsumer(Object object) {
|
||||
ServiceActivatingHandler handler = new ServiceActivatingHandler(object);
|
||||
|
||||
@@ -112,6 +112,10 @@ XML::
|
||||
----
|
||||
======
|
||||
|
||||
Starting with version 6.3, all the `MessageChannel` implementations based on the `UnicastingDispatcher` can be configured with a `Predicate<Exception> failoverStrategy` instead of plain `failover` option.
|
||||
This predicate makes a decision to failover or not to the next `MessageHandler` based on an exception thrown from the current one.
|
||||
The more complex error analysis should be done using xref:router/implementations.adoc#router-implementations-exception-router[`ErrorMessageExceptionTypeRouter`].
|
||||
|
||||
[[channel-datatype-channel]]
|
||||
== Datatype Channel Configuration
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ The `MessageHistory` header is now mutable, append-only container.
|
||||
And all the subsequent tracks don't create new message - only their entry is added to existing message history header.
|
||||
See xref:message-history.adoc[Message History Chapter] for more information.
|
||||
|
||||
All the `MessageChannel` implementations based on the `UnicastingDispatcher` now can be configured with a `Predicate<Exception> failoverStrategy` for dynamic decision for the failover on the exception thrown from the current `MessageHandler`.
|
||||
See xref:channel/configuration.adoc[Message Channel Configuration] for more information.
|
||||
|
||||
[[x6.3-security-changes]]
|
||||
=== Security Support Changes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user