AMQP-431: Add RabbitTemplate.recoveryCallback

JIRA: https://jira.spring.io/browse/AMQP-431

AMQP-431: Address PR comments

Doc Polishing
This commit is contained in:
Artem Bilan
2014-10-09 14:03:14 +03:00
committed by Gary Russell
parent fdd5ac9b20
commit 421152d2de
8 changed files with 165 additions and 43 deletions

View File

@@ -63,6 +63,8 @@ class TemplateParser extends AbstractSingleBeanDefinitionParser {
private static final String RETRY_TEMPLATE = "retry-template";
private static final String RECOVERY_CALLBACK = "recovery-callback";
@Override
protected Class<?> getBeanClass(Element element) {
return RabbitTemplate.class;
@@ -104,6 +106,7 @@ class TemplateParser extends AbstractSingleBeanDefinitionParser {
NamespaceUtils.setReferenceIfAttributeDefined(builder, element, CONFIRM_CALLBACK_ATTRIBUTE);
NamespaceUtils.setValueIfAttributeDefined(builder, element, CORRELATION_KEY);
NamespaceUtils.setReferenceIfAttributeDefined(builder, element, RETRY_TEMPLATE);
NamespaceUtils.setReferenceIfAttributeDefined(builder, element, RECOVERY_CALLBACK);
BeanDefinition expressionDef =
NamespaceUtils.createExpressionDefinitionFromValueOrExpression(MANDATORY_ATTRIBUTE,

View File

@@ -63,6 +63,7 @@ import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.support.RetryTemplate;
@@ -113,8 +114,8 @@ import com.rabbitmq.client.GetResponse;
* @author Artem Bilan
* @since 1.0
*/
public class RabbitTemplate extends RabbitAccessor implements BeanFactoryAware, RabbitOperations, MessageListener,
PublisherCallbackChannel.Listener {
public class RabbitTemplate extends RabbitAccessor
implements BeanFactoryAware, RabbitOperations, MessageListener, PublisherCallbackChannel.Listener {
/** Alias for amq.direct default exchange */
private static final String DEFAULT_EXCHANGE = "";
@@ -125,6 +126,24 @@ public class RabbitTemplate extends RabbitAccessor implements BeanFactoryAware,
private static final String DEFAULT_ENCODING = "UTF-8";
private final ConcurrentHashMap<Object, SortedMap<Long, PendingConfirm>> pendingConfirms =
new ConcurrentHashMap<Object, SortedMap<Long, PendingConfirm>>();
private final Map<String, PendingReply> replyHolder = new ConcurrentHashMap<String, PendingReply>();
private final String uuid = UUID.randomUUID().toString();
private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
private final ReplyToAddressCallback<?> defaultReplyToAddressCallback = new ReplyToAddressCallback<Object>() {
@Override
public Address getReplyToAddress(Message request, Object reply) {
return RabbitTemplate.this.getReplyToAddress(request);
}
};
private volatile String exchange = DEFAULT_EXCHANGE;
private volatile String routingKey = DEFAULT_ROUTING_KEY;
@@ -142,38 +161,22 @@ public class RabbitTemplate extends RabbitAccessor implements BeanFactoryAware,
private volatile Queue replyQueue;
private final Map<String, PendingReply> replyHolder = new ConcurrentHashMap<String, PendingReply>();
private volatile ConfirmCallback confirmCallback;
private volatile ReturnCallback returnCallback;
private final ConcurrentHashMap<Object, SortedMap<Long, PendingConfirm>> pendingConfirms =
new ConcurrentHashMap<Object, SortedMap<Long, PendingConfirm>>();
private volatile Expression mandatoryExpression = new ValueExpression<Boolean>(false);
private final String uuid = UUID.randomUUID().toString();
private volatile String correlationKey = null;
private volatile RetryTemplate retryTemplate;
private volatile RecoveryCallback<?> recoveryCallback;
private volatile Expression sendConnectionFactorySelectorExpression;
private volatile Expression receiveConnectionFactorySelectorExpression;
private final StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
private final ReplyToAddressCallback<?> defaultReplyToAddressCallback = new ReplyToAddressCallback<Object>() {
@Override
public Address getReplyToAddress(Message request, Object reply) {
return RabbitTemplate.this.getReplyToAddress(request);
}
};
/**
* Convenient constructor for use with setter injection. Don't forget to set the connection factory.
*/
@@ -399,6 +402,18 @@ public class RabbitTemplate extends RabbitAccessor implements BeanFactoryAware,
this.retryTemplate = retryTemplate;
}
/**
* Add a {@link RecoveryCallback} which is used for the {@code retryTemplate.execute}.
* If {@link #retryTemplate} isn't provided {@link #recoveryCallback} is ignored.
* {@link RecoveryCallback} should produce result compatible with
* {@link #execute(ChannelCallback, ConnectionFactory)} return type.
* @param recoveryCallback The retry recoveryCallback.
* @since 1.4
*/
public void setRecoveryCallback(RecoveryCallback<?> recoveryCallback) {
this.recoveryCallback = recoveryCallback;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
@@ -406,8 +421,8 @@ public class RabbitTemplate extends RabbitAccessor implements BeanFactoryAware,
}
/**
* Gets unconfirmed correlatiom data older than age and removes them.
* @param age in millseconds
* Gets unconfirmed correlation data older than age and removes them.
* @param age in milliseconds
* @return the collection of correlation data for which confirms have
* not been received.
*/
@@ -906,6 +921,7 @@ public class RabbitTemplate extends RabbitAccessor implements BeanFactoryAware,
return execute(action, null);
}
@SuppressWarnings("unchecked")
private <T> T execute(final ChannelCallback<T> action, final ConnectionFactory connectionFactory) {
if (this.retryTemplate != null) {
try {
@@ -916,7 +932,7 @@ public class RabbitTemplate extends RabbitAccessor implements BeanFactoryAware,
return RabbitTemplate.this.doExecute(action, connectionFactory);
}
});
}, (RecoveryCallback<T>) this.recoveryCallback);
}
catch (Exception e) {
if (e instanceof RuntimeException) {

View File

@@ -1048,6 +1048,18 @@
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="recovery-callback" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[
Reference to a RecoveryCallback - used in the 'retryTemplate.execute()'. Ignored if 'retry-template' isn't provided.
]]></xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.retry.RecoveryCallback" />
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="send-connection-factory-selector-expression" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[

View File

@@ -34,6 +34,7 @@ import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.core.io.ClassPathResource;
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.support.RetryTemplate;
/**
@@ -90,6 +91,7 @@ public final class TemplateParserTests {
DirectFieldAccessor accessor = new DirectFieldAccessor(template);
assertEquals("foo", accessor.getPropertyValue("correlationKey"));
assertSame(beanFactory.getBean(RetryTemplate.class), accessor.getPropertyValue("retryTemplate"));
assertSame(beanFactory.getBean(RecoveryCallback.class), accessor.getPropertyValue("recoveryCallback"));
}
@Test

View File

@@ -20,6 +20,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -28,6 +29,7 @@ import static org.mockito.Mockito.when;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
@@ -48,6 +50,8 @@ import org.springframework.amqp.support.converter.SimpleMessageConverter;
import org.springframework.amqp.utils.SerializationUtils;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.retry.RecoveryCallback;
import org.springframework.retry.RetryContext;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionException;
@@ -215,6 +219,38 @@ public class RabbitTemplateTests {
assertEquals(3, count.get());
}
@Test
public void testRecovery() throws Exception {
ConnectionFactory mockConnectionFactory = mock(ConnectionFactory.class);
final AtomicInteger count = new AtomicInteger();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
count.incrementAndGet();
throw new AuthenticationFailureException("foo");
}
}).when(mockConnectionFactory).newConnection((ExecutorService) null);
RabbitTemplate template = new RabbitTemplate(new SingleConnectionFactory(mockConnectionFactory));
template.setRetryTemplate(new RetryTemplate());
final AtomicBoolean recoverInvoked = new AtomicBoolean();
template.setRecoveryCallback(new RecoveryCallback<Object>() {
@Override
public Object recover(RetryContext context) throws Exception {
recoverInvoked.set(true);
return null;
}
});
template.convertAndSend("foo", "bar", "baz");
assertEquals(3, count.get());
assertTrue(recoverInvoked.get());
}
public final static AtomicInteger LOOKUP_KEY_COUNT = new AtomicInteger();
@Test

View File

@@ -1,44 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<rabbit:template id="template" connection-factory="connectionFactory" />
<rabbit:template id="template" connection-factory="connectionFactory"/>
<rabbit:template id="kitchenSink" connection-factory="connectionFactory" channel-transacted="true" correlation-key="foo"
encoding="UTF-8" exchange="foo" queue="bar" routing-key="spam" message-converter="converter" reply-timeout="1000"
retry-template="retrier" />
<rabbit:template id="kitchenSink" connection-factory="connectionFactory" channel-transacted="true"
correlation-key="foo"
encoding="UTF-8" exchange="foo" queue="bar" routing-key="spam" message-converter="converter"
reply-timeout="1000"
retry-template="retrier"
recovery-callback="recoverer"/>
<rabbit:connection-factory id="connectionFactory" />
<rabbit:connection-factory id="connectionFactory"/>
<bean id="converter" class="org.springframework.amqp.support.converter.SerializerMessageConverter"/>
<rabbit:template id="withReplyQ" connection-factory="connectionFactory" reply-queue="replyQId" correlation-key="correlationId">
<rabbit:reply-listener />
<rabbit:template id="withReplyQ" connection-factory="connectionFactory" reply-queue="replyQId"
correlation-key="correlationId">
<rabbit:reply-listener/>
</rabbit:template>
<rabbit:queue id="replyQId" name="reply.queue" queue-arguments="args" />
<rabbit:queue id="replyQId" name="reply.queue" queue-arguments="args"/>
<rabbit:queue-arguments id="args">
<entry key="foo" value="bar" />
<entry key="foo" value="bar"/>
</rabbit:queue-arguments>
<rabbit:template id="withCallbacks" connection-factory="connectionFactory"
mandatory="true" return-callback="rcb" confirm-callback="ccb" />
mandatory="true" return-callback="rcb" confirm-callback="ccb"/>
<rabbit:template id="withMandatoryExpression" connection-factory="connectionFactory"
mandatory-expression="'true'"
send-connection-factory-selector-expression="'foo'"
receive-connection-factory-selector-expression="'foo'"/>
<beans:bean id="rcb" class="org.mockito.Mockito" factory-method="mock">
<beans:constructor-arg value="org.springframework.amqp.rabbit.core.RabbitTemplate$ReturnCallback" />
</beans:bean>
<bean id="rcb" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.amqp.rabbit.core.RabbitTemplate$ReturnCallback"/>
</bean>
<beans:bean id="ccb" class="org.mockito.Mockito" factory-method="mock">
<beans:constructor-arg value="org.springframework.amqp.rabbit.core.RabbitTemplate$ConfirmCallback" />
</beans:bean>
<bean id="ccb" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.amqp.rabbit.core.RabbitTemplate$ConfirmCallback"/>
</bean>
<bean id="retrier" class="org.springframework.retry.support.RetryTemplate"/>
<bean id="recoverer" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.retry.RecoveryCallback"/>
</bean>
<beans:bean id="retrier" class="org.springframework.retry.support.RetryTemplate" />
</beans>

View File

@@ -562,6 +562,42 @@ public AmqpTemplate rabbitTemplate();
template.setRetryTemplate(retryTemplate);
return template;
}]]></programlisting>
<para>
Starting with <emphasis>version 1.4</emphasis>, in addition to the <code>retryTemplate</code> property,
the <code>recoveryCallback</code> option is supported on the <classname>RabbitTemplate</classname>.
It is used as a second argument for the
<code>RetryTemplate.execute(RetryCallback&lt;T, E&gt; retryCallback,
RecoveryCallback&lt;T&gt;recoveryCallback)</code>.
</para>
<note>
The <interfacename>RecoveryCallback</interfacename> is somewhat limited in that the retry context
only contains the <code>lastThrowable</code> field. For more sophisticated use cases, you should
use an external <classname>RetryTemplate</classname> so that you can convey additional information
to the <interfacename>RecoveryCallback</interfacename> via the context's attributes:
</note>
<programlisting language="java"><![CDATA[retryTemplate.execute(
new RetryCallback<Object, Exception>() {
@Override
public Object doWithRetry(RetryContext context) throws Exception {
context.setAttribute("message", message);
return rabbitTemplate.convertAndSend(exchange, routingKey, message);
}
}, new RecoveryCallback<Object>() {
@Override
public Object recover(RetryContext context) throws Exception {
Object message = context.getAttribute("message");
Throwable t = context.getLastThrowable();
// Do something with message
return null;
}
});
}]]></programlisting>
<para>
In this case, you would <emphasis role="bold">not</emphasis> inject a <classname>RetryTemplate</classname>
into the <classname>RabbitTemplate</classname>.
</para>
</section>
<section id="template-confirms">
<title>Publisher Confirms and Returns</title>

View File

@@ -112,6 +112,14 @@
See <xref linkend="routing-connection-factory"/>.
</para>
</section>
<section>
<title>RabbitTemplate: RecoveryCallback option</title>
<para>
The <code>recoveryCallback</code> property has been added to be used in the
<code>retryTemplate.execute()</code>.
See <xref linkend="template-retry"/>.
</para>
</section>
</section>
<section>