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:
committed by
Artem Bilan
parent
8afdcb4893
commit
544de6bf5f
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user