GH-3450: Expose PubSub requireSubscribers option (#3459)

* GH-3450: Expose PubSub requireSubscribers option

Fixes https://github.com/spring-projects/spring-integration/issues/3450

* Add `PublishSubscribeChannel` ctors with the `requireSubscribers` option
with a direct delegation to the same option of underlying `BroadcastingDispatcher`
* Add factory methods to the `Channels` & `MessageChannels` to expose
this new `requireSubscribers` option
* Expose an XML `require-subscribers` attribute for the `<publish-subscribe-channel`
* Document this new `requireSubscribers` option
* Introduce a global `spring.integration.channels.error.requireSubscribers` property
for a default `errorChannel`
* Document this property and explain its default `true` in the `error-handling.adoc`
* Remove docs for `spring.integration.postProcessDynamicBeans` since it purpose was
removed since version `5.1` in favor of "always post-process behavior" as it was
always with all the `BeanPostProcessor`s

* * Fix language in docs according review

* * Add more docs about `requireSubscribers` and cross-links between chapters
This commit is contained in:
Artem Bilan
2021-01-12 13:35:41 -05:00
committed by GitHub
parent f294331945
commit 478a79c74b
15 changed files with 181 additions and 76 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -39,6 +39,8 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel implements
private ErrorHandler errorHandler;
private final boolean requireSubscribers;
private boolean ignoreFailures;
private boolean applySequence;
@@ -50,7 +52,18 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel implements
* message sender's thread.
*/
public PublishSubscribeChannel() {
this(null);
this(false);
}
/**
* Create a PublishSubscribeChannel that will invoke the handlers in the
* message sender's thread considering the provided {@code requireSubscribers} flag.
* @param requireSubscribers if set to true, the sent message is considered as non-dispatched
* and rejected to the caller with the {@code "Dispatcher has no subscribers"}.
* @since 5.4.3
*/
public PublishSubscribeChannel(boolean requireSubscribers) {
this(null, requireSubscribers);
}
/**
@@ -60,8 +73,22 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel implements
* @param executor The executor.
*/
public PublishSubscribeChannel(@Nullable Executor executor) {
this(executor, false);
}
/**
* Create a PublishSubscribeChannel that will use an {@link Executor}
* to invoke the handlers. If this is null, each invocation will occur in
* the message sender's thread.
* @param executor The executor.
* @param requireSubscribers if set to true, the sent message is considered as non-dispatched
* and rejected to the caller with the {@code "Dispatcher has no subscribers"}.
* @since 5.4.3
*/
public PublishSubscribeChannel(@Nullable Executor executor, boolean requireSubscribers) {
super(executor);
this.dispatcher = new BroadcastingDispatcher(executor);
this.requireSubscribers = requireSubscribers;
this.dispatcher = new BroadcastingDispatcher(executor, requireSubscribers);
}
@@ -77,7 +104,7 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel implements
/**
* Provide an {@link ErrorHandler} strategy for handling Exceptions that
* occur downstream from this channel. This will <i>only</i> be applied if
* occur downstream from this channel. This will only be applied if
* an Executor has been configured to dispatch the Messages for this
* channel. Otherwise, Exceptions will be thrown directly within the
* sending Thread. If no ErrorHandler is provided, and this channel does
@@ -94,9 +121,9 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel implements
/**
* Specify whether failures for one or more of the handlers should be
* ignored. By default this is <code>false</code> meaning that an Exception
* ignored. By default this is false meaning that an Exception
* will be thrown whenever a handler fails. To override this and suppress
* Exceptions, set the value to <code>true</code>.
* Exceptions, set the value to true.
* @param ignoreFailures true if failures should be ignored.
*/
public void setIgnoreFailures(boolean ignoreFailures) {
@@ -107,10 +134,10 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel implements
/**
* Specify whether to apply the sequence number and size headers to the
* messages prior to invoking the subscribed handlers. By default, this
* value is <code>false</code> meaning that sequence headers will
* <em>not</em> be applied. If planning to use an Aggregator downstream
* value is false meaning that sequence headers will
* not be applied. If planning to use an Aggregator downstream
* with the default correlation and completion strategies, you should set
* this flag to <code>true</code>.
* this flag to true.
* @param applySequence true if the sequence information should be applied.
*/
public void setApplySequence(boolean applySequence) {
@@ -147,7 +174,7 @@ public class PublishSubscribeChannel extends AbstractExecutorChannel implements
}
this.executor = new ErrorHandlingTaskExecutor(this.executor, this.errorHandler);
}
dispatcherToUse = new BroadcastingDispatcher(this.executor);
dispatcherToUse = new BroadcastingDispatcher(this.executor, this.requireSubscribers);
dispatcherToUse.setIgnoreFailures(this.ignoreFailures);
dispatcherToUse.setApplySequence(this.applySequence);
dispatcherToUse.setMinSubscribers(this.minSubscribers);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 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.
@@ -215,7 +215,10 @@ class DefaultConfiguringBeanFactoryPostProcessor
"Therefore, a default PublishSubscribeChannel will be created.");
}
this.registry.registerBeanDefinition(IntegrationContextUtils.ERROR_CHANNEL_BEAN_NAME,
new RootBeanDefinition(PublishSubscribeChannel.class));
BeanDefinitionBuilder.rootBeanDefinition(PublishSubscribeChannel.class)
.addConstructorArgValue(IntegrationProperties.getExpressionFor(
IntegrationProperties.ERROR_CHANNEL_REQUIRE_SUBSCRIBERS))
.getBeanDefinition());
BeanDefinition loggingHandler =
BeanDefinitionBuilder.genericBeanDefinition(LoggingHandler.class)

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 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.
@@ -40,6 +40,7 @@ public class PublishSubscribeChannelParser extends AbstractChannelParser {
if (StringUtils.hasText(taskExecutorRef)) {
builder.addConstructorArgReference(taskExecutorRef);
}
builder.addConstructorArgValue(element.getAttribute("require-subscribers"));
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-handler");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "ignore-failures");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "apply-sequence");

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2019 the original author or authors.
* Copyright 2014-2021 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.
@@ -47,14 +47,24 @@ public final class IntegrationProperties {
* in case of point-to-point channels (e.g. {@link org.springframework.integration.channel.ExecutorChannel}),
* if the attribute {@code max-subscribers} isn't configured on the channel component.
*/
public static final String CHANNELS_MAX_UNICAST_SUBSCRIBERS = INTEGRATION_PROPERTIES_PREFIX + "channels.maxUnicastSubscribers";
public static final String CHANNELS_MAX_UNICAST_SUBSCRIBERS =
INTEGRATION_PROPERTIES_PREFIX + "channels.maxUnicastSubscribers";
/**
* Specifies the value for {@link org.springframework.integration.dispatcher.BroadcastingDispatcher#maxSubscribers}
* in case of point-to-point channels (e.g. {@link org.springframework.integration.channel.PublishSubscribeChannel}),
* if the attribute {@code max-subscribers} isn't configured on the channel component.
*/
public static final String CHANNELS_MAX_BROADCAST_SUBSCRIBERS = INTEGRATION_PROPERTIES_PREFIX + "channels.maxBroadcastSubscribers";
public static final String CHANNELS_MAX_BROADCAST_SUBSCRIBERS =
INTEGRATION_PROPERTIES_PREFIX + "channels.maxBroadcastSubscribers";
/**
* Specifies the value for {@link org.springframework.integration.channel.PublishSubscribeChannel#requireSubscribers}
* on a global default {@link IntegrationContextUtils#ERROR_CHANNEL_BEAN_NAME}.
*/
public static final String ERROR_CHANNEL_REQUIRE_SUBSCRIBERS =
INTEGRATION_PROPERTIES_PREFIX + "channels.error.requireSubscribers";
/**
* Specifies the value of {@link org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler#poolSize}
@@ -65,7 +75,8 @@ public final class IntegrationProperties {
/**
* Specifies the value of {@link org.springframework.messaging.core.GenericMessagingTemplate#throwExceptionOnLateReply}.
*/
public static final String THROW_EXCEPTION_ON_LATE_REPLY = INTEGRATION_PROPERTIES_PREFIX + "messagingTemplate.throwExceptionOnLateReply";
public static final String THROW_EXCEPTION_ON_LATE_REPLY =
INTEGRATION_PROPERTIES_PREFIX + "messagingTemplate.throwExceptionOnLateReply";
/**
* Specifies the value of {@link org.springframework.integration.support.DefaultMessageBuilderFactory#readOnlyHeaders}.
@@ -77,18 +88,19 @@ public final class IntegrationProperties {
*/
public static final String ENDPOINTS_NO_AUTO_STARTUP = INTEGRATION_PROPERTIES_PREFIX + "endpoints.noAutoStartup";
private static Properties defaults;
private static final Properties DEFAULTS;
static {
String resourcePattern = "classpath*:META-INF/spring.integration.default.properties";
try {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver(IntegrationProperties.class.getClassLoader());
ResourcePatternResolver resourceResolver =
new PathMatchingResourcePatternResolver(IntegrationProperties.class.getClassLoader());
Resource[] defaultResources = resourceResolver.getResources(resourcePattern);
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocations(defaultResources);
propertiesFactoryBean.afterPropertiesSet();
defaults = propertiesFactoryBean.getObject();
DEFAULTS = propertiesFactoryBean.getObject();
}
catch (IOException e) {
throw new IllegalStateException("Can't load '" + resourcePattern + "' resources.", e);
@@ -100,7 +112,7 @@ public final class IntegrationProperties {
* from resources 'META-INF/spring.integration.default.properties'.
*/
public static Properties defaults() {
return defaults;
return DEFAULTS;
}
/**
@@ -112,11 +124,13 @@ public final class IntegrationProperties {
* @throws IllegalArgumentException if provided {@code key} isn't an Integration property.
*/
public static String getExpressionFor(String key) {
if (defaults.containsKey(key)) {
return "#{T(org.springframework.integration.context.IntegrationContextUtils).getIntegrationProperties(beanFactory).getProperty('" + key + "')}";
if (DEFAULTS.containsKey(key)) {
return "#{T(org.springframework.integration.context.IntegrationContextUtils)" +
".getIntegrationProperties(beanFactory).getProperty('" + key + "')}";
}
else {
throw new IllegalArgumentException("The provided key [" + key + "] isn't the one of Integration properties: " + defaults.keySet());
throw new IllegalArgumentException("The provided key [" + key +
"] isn't the one of Integration properties: " + DEFAULTS.keySet());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2019 the original author or authors.
* Copyright 2016-2021 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.
@@ -98,22 +98,38 @@ public final class Channels {
return MessageChannels.rendezvous(id);
}
public PublishSubscribeChannelSpec<?> publishSubscribe() {
public PublishSubscribeChannelSpec<?> publishSubscribe() {
return MessageChannels.publishSubscribe();
}
public PublishSubscribeChannelSpec<?> publishSubscribe(boolean requireSubscribers) {
return MessageChannels.publishSubscribe(requireSubscribers);
}
public PublishSubscribeChannelSpec<?> publishSubscribe(Executor executor) {
return MessageChannels.publishSubscribe(executor);
}
public PublishSubscribeChannelSpec<?> publishSubscribe(Executor executor, boolean requireSubscribers) {
return MessageChannels.publishSubscribe(executor, requireSubscribers);
}
public PublishSubscribeChannelSpec<?> publishSubscribe(String id, Executor executor) {
return MessageChannels.publishSubscribe(id, executor);
}
public PublishSubscribeChannelSpec<?> publishSubscribe(String id, Executor executor, boolean requireSubscribers) {
return MessageChannels.publishSubscribe(id, executor, requireSubscribers);
}
public PublishSubscribeChannelSpec<?> publishSubscribe(String id) {
return MessageChannels.publishSubscribe(id);
}
public PublishSubscribeChannelSpec<?> publishSubscribe(String id, boolean requireSubscribers) {
return MessageChannels.publishSubscribe(id, requireSubscribers);
}
public ExecutorChannelSpec executor(Executor executor) {
return MessageChannels.executor(executor);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2019 the original author or authors.
* Copyright 2016-2021 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.
@@ -108,19 +108,37 @@ public final class MessageChannels {
}
public static PublishSubscribeChannelSpec<?> publishSubscribe() {
return new PublishSubscribeChannelSpec<>();
return publishSubscribe(false);
}
public static PublishSubscribeChannelSpec<?> publishSubscribe(boolean requireSubscribers) {
return new PublishSubscribeChannelSpec<>(requireSubscribers);
}
public static PublishSubscribeChannelSpec<?> publishSubscribe(String id) {
return MessageChannels.publishSubscribe().id(id);
return publishSubscribe(id, false);
}
public static PublishSubscribeChannelSpec<?> publishSubscribe(String id, boolean requireSubscribers) {
return publishSubscribe(requireSubscribers).id(id);
}
public static PublishSubscribeChannelSpec<?> publishSubscribe(Executor executor) {
return new PublishSubscribeChannelSpec<>(executor);
return publishSubscribe(executor, false);
}
public static PublishSubscribeChannelSpec<?> publishSubscribe(Executor executor, boolean requireSubscribers) {
return new PublishSubscribeChannelSpec<>(executor, requireSubscribers);
}
public static PublishSubscribeChannelSpec<?> publishSubscribe(String id, Executor executor) {
return MessageChannels.publishSubscribe(executor).id(id);
return publishSubscribe(id, executor, false);
}
public static PublishSubscribeChannelSpec<?> publishSubscribe(String id, Executor executor,
boolean requireSubscribers) {
return publishSubscribe(executor, requireSubscribers).id(id);
}
public static FluxMessageChannelSpec flux() {
@@ -128,8 +146,7 @@ public final class MessageChannels {
}
public static FluxMessageChannelSpec flux(String id) {
return flux()
.id(id);
return flux().id(id);
}
private MessageChannels() {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2019 the original author or authors.
* Copyright 2016-2021 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.
@@ -34,11 +34,19 @@ public class PublishSubscribeChannelSpec<S extends PublishSubscribeChannelSpec<S
extends MessageChannelSpec<S, PublishSubscribeChannel> {
protected PublishSubscribeChannelSpec() {
this(null);
this(false);
}
protected PublishSubscribeChannelSpec(boolean requireSubscribers) {
this(null, requireSubscribers);
}
protected PublishSubscribeChannelSpec(@Nullable Executor executor) {
this.channel = new PublishSubscribeChannel(executor);
this(executor, false);
}
protected PublishSubscribeChannelSpec(@Nullable Executor executor, boolean requireSubscribers) {
this.channel = new PublishSubscribeChannel(executor, requireSubscribers);
}
public S errorHandler(ErrorHandler errorHandler) {

View File

@@ -1,6 +1,7 @@
spring.integration.channels.autoCreate=true
spring.integration.channels.maxUnicastSubscribers=0x7fffffff
spring.integration.channels.maxBroadcastSubscribers=0x7fffffff
spring.integration.channels.error.requireSubscribers=true
spring.integration.taskScheduler.poolSize=10
spring.integration.messagingTemplate.throwExceptionOnLateReply=false
# Defaults to MessageHeaders.ID and MessageHeaders.TIMESTAMP

View File

@@ -537,13 +537,23 @@
</xsd:attribute>
<xsd:attribute name="min-subscribers" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
<xsd:documentation>
Specifies the minimum subscribers required to be subscribed to this channel; if the minimum number
of subscribers receive the message, the send is deemed to be successful (returns true).
Defaults to 0.
]]></xsd:documentation>
</xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="require-subscribers" default="false">
<xsd:annotation>
<xsd:documentation>
Indicates if this channel may ignore or not a message when there are no subscribers.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="xsd:boolean xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attributeGroup ref="subscribersAttributeGroup"/>
</xsd:extension>
</xsd:complexContent>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2016-2019 the original author or authors.
* Copyright 2016-2021 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,18 +16,23 @@
package org.springframework.integration.channel;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.Mockito.mock;
import java.util.concurrent.Executor;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.integration.MessageDispatchingException;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.support.GenericMessage;
/**
* @author Gary Russell
* @author Artem Bilan
*
* @since 5.0
*
*/
@@ -36,17 +41,21 @@ public class PublishSubscribeChannelTests {
@Test
public void testEarlySubscribe() {
PublishSubscribeChannel channel = new PublishSubscribeChannel(mock(Executor.class));
try {
channel.subscribe(m -> { });
channel.setBeanFactory(mock(BeanFactory.class));
channel.afterPropertiesSet();
fail("expected Exception");
}
catch (IllegalStateException e) {
assertThat(e.getMessage()).isEqualTo("When providing an Executor, you cannot subscribe() until the " +
"channel "
+ "bean is fully initialized by the framework. Do not subscribe in a @Bean definition");
}
channel.subscribe(m -> { });
channel.setBeanFactory(mock(BeanFactory.class));
assertThatIllegalStateException()
.isThrownBy(channel::afterPropertiesSet)
.withMessage("When providing an Executor, you cannot subscribe() until the channel "
+ "bean is fully initialized by the framework. Do not subscribe in a @Bean definition");
}
@Test
public void testRequireSubscribers() {
PublishSubscribeChannel channel = new PublishSubscribeChannel(true);
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> channel.send(new GenericMessage<>("test")))
.withCauseInstanceOf(MessageDispatchingException.class)
.withMessageContaining("Dispatcher has no subscribers");
}
}

View File

@@ -3,16 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:int="http://www.springframework.org/schema/integration"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:util="http://www.springframework.org/schema/util"
xmlns:beans="http://www.springframework.org/schema/c"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/integration https://www.springframework.org/schema/integration/spring-integration.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
<util:properties id="integrationGlobalProperties">
<prop key="spring.integration.postProcessDynamicBeans">true</prop>
</util:properties>
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<int:channel id="inputA">
<int:interceptors>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 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,30 +18,29 @@ package org.springframework.integration.config.xml;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.channel.PublishSubscribeChannel;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Oleg Zhurakousky
* @author Artem Bilan
*/
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
@SpringJUnitConfig
public class ErrorChannelAutoCreationTests {
@Autowired
private MessageChannel errorChannel;
// see INT-1899
@Test
public void testErrorChannelIsPubSub() {
assertThat(errorChannel.getClass()).isEqualTo(PublishSubscribeChannel.class);
assertThat(this.errorChannel).isInstanceOf(PublishSubscribeChannel.class);
assertThat(TestUtils.getPropertyValue(this.errorChannel, "dispatcher.requireSubscribers", Boolean.class))
.isTrue();
}
}