Add BoundRabbitChannelAdvice

Polishing and docs

Polishing - DEBUG log for confirms; add integration test

Polishing - PR Comments

Renamed Advice

Verify acks logged.

Polishing - more PR comments

Renamed to BoundRabbitChannelAdvice.

* Extract `ConfirmCallback`s instances for optimization
* Remove unused constant
This commit is contained in:
Gary Russell
2018-03-28 17:02:10 -04:00
committed by Artem Bilan
parent 8afdcb4893
commit 544de6bf5f
7 changed files with 438 additions and 1 deletions

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2018 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.
* 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.springframework.integration.amqp.support;
import java.lang.reflect.UndeclaredThrowableException;
import java.time.Duration;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.amqp.rabbit.core.RabbitOperations;
import org.springframework.integration.handler.advice.HandleMessageAdvice;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import com.rabbitmq.client.ConfirmCallback;
/**
* An advice that causes all downstream {@link RabbitOperations} operations to be executed
* on the same channel, as long as there are no thread handoffs, since the channel is
* bound to the thread. The same RabbitOperations must be used in this and all downstream
* components. Typically used with a splitter or some other mechanism that would cause
* multiple messages to be sent. Optionally waits for publisher confirms if the channel is
* so configured.
*
* @author Gary Russell
* @author Artem Bilan
*
* @since 5.1
*
*/
public class BoundRabbitChannelAdvice implements HandleMessageAdvice {
private final Log logger = LogFactory.getLog(getClass());
private final RabbitOperations operations;
private final Duration waitForConfirmsTimeout;
private final ConfirmCallback ackCallback = this::handleAcks;
private final ConfirmCallback nackCallback = this::handleNacks;
/**
* Construct an instance that doesn't wait for confirms.
* @param operations the operations.
*/
public BoundRabbitChannelAdvice(RabbitOperations operations) {
this(operations, null);
}
/**
* Construct an instance that waits for publisher confirms (if
* configured and waitForConfirmsTimeout is not null).
* @param operations the operations.
* @param waitForConfirmsTimeout the timeout.
*/
public BoundRabbitChannelAdvice(RabbitOperations operations, @Nullable Duration waitForConfirmsTimeout) {
Assert.notNull(operations, "'operations' cannot be null");
this.operations = operations;
this.waitForConfirmsTimeout = waitForConfirmsTimeout;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
return this.operations.invoke(operations -> {
try {
Object result = invocation.proceed();
if (this.waitForConfirmsTimeout != null) {
this.operations.waitForConfirmsOrDie(this.waitForConfirmsTimeout.toMillis());
}
return result;
}
catch (Throwable t) { // NOSONAR - rethrown below
ReflectionUtils.rethrowRuntimeException(t);
return null; // not reachable - satisfy compiler
}
}, this.ackCallback, this.nackCallback);
}
catch (UndeclaredThrowableException ute) {
throw ute.getCause();
}
}
private void handleAcks(long deliveryTag, boolean multiple) {
doHandleAcks(deliveryTag, multiple, true);
}
private void handleNacks(long deliveryTag, boolean multiple) {
doHandleAcks(deliveryTag, multiple, false);
}
private void doHandleAcks(long deliveryTag, boolean multiple, boolean ack) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Publisher confirm " + (!ack ? "n" : "") + "ack: " + deliveryTag + ", " +
"multiple: " + multiple);
}
}
}

View File

@@ -0,0 +1,136 @@
/*
* Copyright 2018 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.
* 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.springframework.integration.amqp.support;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.spy;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.junit.RabbitAvailable;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.amqp.dsl.Amqp;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.integration.test.util.TestUtils;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Gary Russell
* @since 5.1
*
*/
@SpringJUnitConfig
@RabbitAvailable(queues = BoundRabbitChannelAdviceIntegrationTests.QUEUE)
public class BoundRabbitChannelAdviceIntegrationTests {
public static final String QUEUE = "dedicated.advice";
@Autowired
private Config.Gate gate;
@Autowired
private Config config;
@Test
public void testAdvice() throws Exception {
BoundRabbitChannelAdvice advice = this.config.advice(this.config.template());
Log logger = spy(TestUtils.getPropertyValue(advice, "logger", Log.class));
new DirectFieldAccessor(advice).setPropertyValue("logger", logger);
willReturn(true).given(logger).isDebugEnabled();
final CountDownLatch latch = new CountDownLatch(1);
willAnswer(i -> {
latch.countDown();
return i.callRealMethod();
}).given(logger).debug(anyString());
this.gate.send("a,b,c");
assertTrue(this.config.latch.await(10, TimeUnit.SECONDS));
assertTrue(latch.await(10, TimeUnit.SECONDS));
assertThat(this.config.received).containsExactly("A", "B", "C");
}
@Configuration
@EnableIntegration
public static class Config {
private final CountDownLatch latch = new CountDownLatch(3);
private final List<String> received = new ArrayList<>();
@Bean
public CachingConnectionFactory cf() throws Exception {
CachingConnectionFactory ccf = new CachingConnectionFactory("localhost");
ccf.setSimplePublisherConfirms(true);
return ccf;
}
@Bean
public RabbitTemplate template() throws Exception {
return new RabbitTemplate(cf());
}
@Bean
public BoundRabbitChannelAdvice advice(RabbitTemplate template) {
return new BoundRabbitChannelAdvice(template, Duration.ofSeconds(10));
}
@Bean
public IntegrationFlow flow(RabbitTemplate template, BoundRabbitChannelAdvice advice) {
return IntegrationFlows.from(Gate.class)
.split(s -> s.delimiters(",")
.advice(advice))
.<String, String>transform(String::toUpperCase)
.handle(Amqp.outboundAdapter(template).routingKey(QUEUE))
.get();
}
@Bean
public IntegrationFlow listener(CachingConnectionFactory ccf) {
return IntegrationFlows.from(Amqp.inboundAdapter(ccf, QUEUE))
.handle(m -> {
received.add((String) m.getPayload());
this.latch.countDown();
})
.get();
}
public interface Gate {
void send(String out);
}
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2018 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.
* 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.springframework.integration.amqp.support;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.amqp.dsl.Amqp;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.integration.dsl.IntegrationFlows;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* @author Gary Russell
* @since 5.1
*
*/
@SpringJUnitConfig
public class BoundRabbitChannelAdviceTests {
@Autowired
private Config.Gate gate;
@Autowired
private Config config;
@Test
public void testAdvice() throws Exception {
this.gate.send("a,b,c");
verify(this.config.connection, times(1)).createChannel();
verify(this.config.channel).confirmSelect();
verify(this.config.channel).basicPublish(eq(""), eq("rk"), anyBoolean(), any(), eq("A".getBytes()));
verify(this.config.channel).basicPublish(eq(""), eq("rk"), anyBoolean(), any(), eq("B".getBytes()));
verify(this.config.channel).basicPublish(eq(""), eq("rk"), anyBoolean(), any(), eq("C".getBytes()));
verify(this.config.channel).waitForConfirmsOrDie(10_000L);
}
@Configuration
@EnableIntegration
public static class Config {
private Connection connection;
private Channel channel;
@Bean
public CachingConnectionFactory cf() throws Exception {
ConnectionFactory cf = mock(ConnectionFactory.class);
cf.setHost("localhost");
cf = spy(cf);
willAnswer(i -> {
this.connection = mock(Connection.class);
willAnswer(ii -> {
this.channel = mock(Channel.class);
given(this.channel.isOpen()).willReturn(true);
return this.channel;
}).given(this.connection).createChannel();
return this.connection;
}).given(cf).newConnection((ExecutorService) isNull(), anyString());
cf.setAutomaticRecoveryEnabled(false);
CachingConnectionFactory ccf = new CachingConnectionFactory(cf);
ccf.setSimplePublisherConfirms(true);
return ccf;
}
@Bean
public RabbitTemplate template() throws Exception {
return new RabbitTemplate(cf());
}
@Bean
public IntegrationFlow flow(RabbitTemplate template) {
return IntegrationFlows.from(Gate.class)
.split(s -> s.delimiters(",")
.advice(new BoundRabbitChannelAdvice(template, Duration.ofSeconds(10))))
.<String, String>transform(String::toUpperCase)
.handle(Amqp.outboundAdapter(template).routingKey("rk"))
.get();
}
public interface Gate {
void send(String out);
}
}
}