INT-2285 Implement max-subscribers on Channels

Add a subscriber limit on both unicast and publish-subscribe
channels. This permits detection of inadvertent channel wiring
during context initialization.

Also add a mechanism to globally set these defaults.

Two properties are added to ChannelInitializer. If the
'channelInitializer' bean is declared before a channel, and
these properties are set, it will globally override the
default (Integer.MAX_VALUE) for these properties.

For example:

    <bean id="channelInitializer" class="org.springframework.integration.config.xml.ChannelInitializer">
        <property name="autoCreate" value="true" />
        <property name="defaultMaxUnicastSubscribers" value="1" />
        <property name="defaultMaxMulticastSubscribers" value="2" />
    </bean>

will make the default max-subscribers 1 and 2 for <channel/> and
<publish-subscribe/> channels respectively.

Also applies to module channels (jms, amqp, redis).
This commit is contained in:
Gary Russell
2012-07-10 18:06:16 -04:00
committed by Gunnar Hillert
parent a90a7e8e0b
commit 2ecb14de2a
35 changed files with 606 additions and 78 deletions

View File

@@ -36,8 +36,8 @@ import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.channel.MessagePublishingErrorHandler;
import org.springframework.integration.core.MessageHandler;
import org.springframework.integration.core.SubscribableChannel;
import org.springframework.integration.dispatcher.AbstractDispatcher;
import org.springframework.integration.dispatcher.BroadcastingDispatcher;
import org.springframework.integration.dispatcher.MessageDispatcher;
import org.springframework.integration.support.channel.BeanFactoryChannelResolver;
import org.springframework.integration.support.converter.MessageConverter;
import org.springframework.integration.support.converter.SimpleMessageConverter;
@@ -58,16 +58,16 @@ public class SubscribableRedisChannel extends AbstractMessageChannel implements
private final RedisConnectionFactory connectionFactory;
private final RedisTemplate redisTemplate;
private final String topicName;
private final MessageDispatcher dispatcher = new BroadcastingDispatcher(true);
private final AbstractDispatcher dispatcher = new BroadcastingDispatcher(true);
private volatile boolean initialized;
// defaults
private volatile Executor taskExecutor = new SimpleAsyncTaskExecutor();
private volatile RedisSerializer<?> serializer = new StringRedisSerializer();
private volatile MessageConverter messageConverter = new SimpleMessageConverter();
public SubscribableRedisChannel(RedisConnectionFactory connectionFactory, String topicName) {
Assert.notNull(connectionFactory, "'connectionFactory' must not be null");
Assert.hasText(topicName, "'topicName' must not be empty");
@@ -80,17 +80,26 @@ public class SubscribableRedisChannel extends AbstractMessageChannel implements
Assert.notNull(taskExecutor, "'taskExecutor' must not be null");
this.taskExecutor = taskExecutor;
}
public void setMessageConverter(MessageConverter messageConverter) {
Assert.notNull(messageConverter, "'messageConverter' must not be null");
this.messageConverter = messageConverter;
}
public void setSerializer(RedisSerializer<?> serializer) {
Assert.notNull(serializer, "'serializer' must not be null");
this.serializer = serializer;
}
/**
* Specify the maximum number of subscribers supported by the
* channel's dispatcher.
* @param maxSubscribers
*/
public void setMaxSubscribers(int maxSubscribers) {
this.dispatcher.setMaxSubscribers(maxSubscribers);
}
public boolean subscribe(MessageHandler handler) {
return this.dispatcher.addHandler(handler);
}
@@ -98,7 +107,7 @@ public class SubscribableRedisChannel extends AbstractMessageChannel implements
public boolean unsubscribe(MessageHandler handler) {
return this.dispatcher.removeHandler(handler);
}
@Override
protected boolean doSend(Message<?> message, long arg1) {
this.redisTemplate.convertAndSend(this.topicName, this.messageConverter.fromMessage(message));
@@ -169,7 +178,7 @@ public class SubscribableRedisChannel extends AbstractMessageChannel implements
this.container.destroy();
}
}
private class MessageListenerDelegate {
@SuppressWarnings("unused")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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,21 +16,21 @@
package org.springframework.integration.redis.config;
import org.w3c.dom.Element;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.integration.config.xml.AbstractChannelParser;
import org.springframework.integration.config.xml.IntegrationNamespaceUtils;
import org.springframework.integration.redis.channel.SubscribableRedisChannel;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
/**
* Parser for the 'channel' and 'publish-subscribe-channel' elements of the
* Spring Integration Redis namespace.
*
*
* @author Oleg Zhurakusky
* @author Artem Bilan
* @author Gary Russell
* @since 2.1
*/
public class RedisChannelParser extends AbstractChannelParser {
@@ -45,13 +45,14 @@ public class RedisChannelParser extends AbstractChannelParser {
builder.addConstructorArgReference(connectionFactory);
String topicName = element.getAttribute("topic-name");
builder.addConstructorArgValue(topicName);
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "task-executor");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "message-converter");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "serializer");
// The following 2 attributes should be added once configurable on the RedisMessageListenerContainer
// IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "phase");
// IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "auto-startup");
this.setMaxSubscribersProperty(parserContext, builder, element, IntegrationNamespaceUtils.DEFAULT_MAX_BROADCAST_SUBSCRIBERS_PROPERTY_NAME);
return builder;
}

View File

@@ -137,6 +137,7 @@
<!-- ]]></xsd:documentation> -->
<!-- </xsd:annotation> -->
<!-- </xsd:attribute> -->
<xsd:attributeGroup ref="integration:subscribersAttributeGroup" />
</xsd:complexType>
<xsd:element name="inbound-channel-adapter">

View File

@@ -18,4 +18,7 @@
<bean id="redisSerializer" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
<int-redis:publish-subscribe-channel id="redisChannelWithSubLimit" topic-name="si.test.topic"
serializer="redisSerializer" max-subscribers="1" />
</beans>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2012 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,9 +16,10 @@
package org.springframework.integration.redis.config;
import static junit.framework.Assert.assertEquals;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializer;
@@ -31,24 +32,28 @@ import org.springframework.integration.redis.rules.RedisAvailable;
import org.springframework.integration.redis.rules.RedisAvailableTests;
import org.springframework.integration.test.util.TestUtils;
import static junit.framework.Assert.assertEquals;
/**
* @author Oleg Zhurakousky
* @author Gary Russell
*/
public class RedisChannelParserTests extends RedisAvailableTests{
@Test
@Test
@RedisAvailable
public void testPubSubChannelConfig(){
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("RedisChannelParserTests-context.xml", this.getClass());
SubscribableChannel redisChannel = context.getBean("redisChannel", SubscribableChannel.class);
JedisConnectionFactory connectionFactory =
JedisConnectionFactory connectionFactory =
TestUtils.getPropertyValue(redisChannel, "connectionFactory", JedisConnectionFactory.class);
RedisSerializer<?> redisSerializer = TestUtils.getPropertyValue(redisChannel, "serializer", RedisSerializer.class);
assertEquals(connectionFactory, context.getBean("redisConnectionFactory"));
assertEquals(redisSerializer, context.getBean("redisSerializer"));
assertEquals("si.test.topic", TestUtils.getPropertyValue(redisChannel, "topicName"));
assertEquals(Integer.MAX_VALUE, TestUtils.getPropertyValue(
TestUtils.getPropertyValue(redisChannel, "dispatcher"), "maxSubscribers", Integer.class).intValue());
redisChannel = context.getBean("redisChannelWithSubLimit", SubscribableChannel.class);
assertEquals(1, TestUtils.getPropertyValue(
TestUtils.getPropertyValue(redisChannel, "dispatcher"), "maxSubscribers", Integer.class).intValue());
context.stop();
}
@@ -58,9 +63,9 @@ public class RedisChannelParserTests extends RedisAvailableTests{
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("RedisChannelParserTests-context.xml", this.getClass());
SubscribableChannel redisChannel = context.getBean("redisChannel", SubscribableChannel.class);
final Message<?> m = new GenericMessage<String>("Hello Redis");
final Marker marker = Mockito.mock(Marker.class);
redisChannel.subscribe(new MessageHandler() {
redisChannel.subscribe(new MessageHandler() {
public void handleMessage(Message<?> message) throws MessagingException {
assertEquals(m.getPayload(), message.getPayload());
marker.mark();
@@ -71,7 +76,7 @@ public class RedisChannelParserTests extends RedisAvailableTests{
Mockito.verify(marker, Mockito.times(1)).mark();
context.stop();
}
interface Marker {
void mark();
}