How to test MessageChannel in Spring Integrtion? - spring

I'm trying to know if the message passed through specific channel for test or i'd like to get the message from specific channel
So my flow is: controller -> gateway -> ServiceActivator
private final Gateway gateway;
public ResponseEntity<Map<String,String>> submit(String applicationId, ApplicationDto applicationDto) {
applicationDto.setApplicationId(applicationId);
gateway.submitApplication(applicationDto);
return ResponseEntity.ok(Map.of(MESSAGE, "Accepted submit"));
}
the gateway
#Gateway(requestChannel = "submitApplicationChannel", replyChannel = "replySubmitApplicationChannel")
WorkflowPayload submitApplication(ApplicationDto applicationDto);
pipeline
#Bean
MessageChannel submitApplicationChannel() {
return new DirectChannel();
}
So my test is sending a request to start the flow
#Test
#DisplayName("Application Submission")
void submissionTest() throws Exception {
mockMvc.perform(MockMvcRequestBuilders
.post("/api/v1/applications/contract-validation/" + APPLICATION_ID)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(payload)))
.andExpect(status().isAccepted())
.andReturn();
//Check HERE if the message passed through the channel
}
Can you give me a hand??

In your test, add a ChannelInterceptor to the submitApplicationChannel before calling the gateway.
public interface ChannelInterceptor {
/**
* Invoked before the Message is actually sent to the channel.
* This allows for modification of the Message if necessary.
* If this method returns {#code null} then the actual
* send invocation will not occur.
*/
#Nullable
default Message<?> preSend(Message<?> message, MessageChannel channel) {
return message;
}
/**
* Invoked immediately after the send invocation. The boolean
* value argument represents the return value of that invocation.
*/
default void postSend(Message<?> message, MessageChannel channel, boolean sent) {
}
/**
* Invoked after the completion of a send regardless of any exception that
* have been raised thus allowing for proper resource cleanup.
* <p>Note that this will be invoked only if {#link #preSend} successfully
* completed and returned a Message, i.e. it did not return {#code null}.
* #since 4.1
*/
default void afterSendCompletion(
Message<?> message, MessageChannel channel, boolean sent, #Nullable Exception ex) {
}
/**
* Invoked as soon as receive is called and before a Message is
* actually retrieved. If the return value is 'false', then no
* Message will be retrieved. This only applies to PollableChannels.
*/
default boolean preReceive(MessageChannel channel) {
return true;
}
/**
* Invoked immediately after a Message has been retrieved but before
* it is returned to the caller. The Message may be modified if
* necessary; {#code null} aborts further interceptor invocations.
* This only applies to PollableChannels.
*/
#Nullable
default Message<?> postReceive(Message<?> message, MessageChannel channel) {
return message;
}
/**
* Invoked after the completion of a receive regardless of any exception that
* have been raised thus allowing for proper resource cleanup.
* <p>Note that this will be invoked only if {#link #preReceive} successfully
* completed and returned {#code true}.
* #since 4.1
*/
default void afterReceiveCompletion(#Nullable Message<?> message, MessageChannel channel,
#Nullable Exception ex) {
}
}

Related

In Spring RabbitMQ I throw AmqpRejectAndDontRequeueException but message still requeue

My service listens to RabbitMQ queue. I configure retry policy in consumer side. When I throw exception, all dead-letter messages requeue. But depend on my business logic, after throwing StopRequeueException (every exception except SmsException) I want to stop retry for this message. But the message still requeue.
Here is my configuration
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true
initial-interval: 3s
max-attempts: 10
max-interval: 12s
multiplier: 2
missing-queues-fatal: false
if (!checkMobileService.isMobileNumberAdmitted(mobileNumber())) {
throw new StopRequeueException("SMS_BIMTEK.MOBILE_NUMBER_IS_NOT_ADMITTED");
}
My error handler:
public class CustomErrorHandler implements ErrorHandler {
#Override
public void handleError(Throwable t) {
if (!(t.getCause() instanceof SmsException)) {
throw new AmqpRejectAndDontRequeueException("Error Handler converted exception to fatal", t);
}
}
}
Calling the error handler is outside the scope of retry; it is called after retries are exhausted.
You need to classify which exceptions are retryable at the retry level and do the conversion in the recoverer.
Here is an example:
#SpringBootApplication
public class So67406799Application {
public static void main(String[] args) {
SpringApplication.run(So67406799Application.class, args);
}
#Bean
public RabbitRetryTemplateCustomizer customizer(
#Value("${spring.rabbitmq.listener.simple.retry.max-attempts}") int attempts) {
return (target, template) -> template.setRetryPolicy(new SimpleRetryPolicy(attempts,
Map.of(StopRequeueException.class, false), true, true));
}
#Bean
MessageRecoverer recoverer() {
return (msg, cause) -> {
throw new AmqpRejectAndDontRequeueException("Stop requeue after " +
RetrySynchronizationManager.getContext().getRetryCount() + " attempts");
};
}
#RabbitListener(queues = "so67406799")
void listen(String in) {
System.out.println(in);
if (in.equals("dontRetry")) {
throw new StopRequeueException("test");
}
throw new RuntimeException("test");
}
#Bean
Queue queue() {
return new Queue("so67406799");
}
}
#SuppressWarnings("serial")
class StopRequeueException extends NestedRuntimeException {
public StopRequeueException(String msg) {
super(msg);
}
}
EDIT
The customizer is called once by Spring Boot; it is called after the retry policy and back off policy have been set up. See RetryTemplateFactory.
In this case, the customizer replaces the retry policy with a new one with an exception classifier (that's why we need the max attempts injected here).
See the SimpleRetryPolicy constructor.
/**
* Create a {#link SimpleRetryPolicy} with the specified number of retry attempts. If
* traverseCauses is true, the exception causes will be traversed until a match is
* found. The default value indicates whether to retry or not for exceptions (or super
* classes) are not found in the map.
* #param maxAttempts the maximum number of attempts
* #param retryableExceptions the map of exceptions that are retryable based on the
* map value (true/false).
* #param traverseCauses true to traverse the exception cause chain until a classified
* exception is found or the root cause is reached.
* #param defaultValue the default action.
*/
public SimpleRetryPolicy(int maxAttempts, Map<Class<? extends Throwable>, Boolean> retryableExceptions,
boolean traverseCauses, boolean defaultValue) {
The last boolean in the config above (true) is the default behavior (retry exceptions that are not in the map), the third (true) tells the policy to follow the cause chain to look for the exception (like your getCause() in the error handler). The map <Exception, Boolean> says don't retry for this one.
You can also configure it the other way around (default false and true in the map values), explicitly stating which exceptions you want to retry and don't for all others.
The MessageRecoverer is called for all exceptions, either immediately for the classified exception or when retries are exhausted for the others.

How to aggregate messages with the identical headers in Spring Integration

public static void main(String[] args) throws InterruptedException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(Main.class);
ctx.refresh();
DirectChannel channel1 = ctx.getBean("channel1", DirectChannel.class);
ctx.getBean("channel2", PublishSubscribeChannel.class).subscribe(message ->
System.out.println("Output: " + message));
channel1.send(MessageBuilder.withPayload("p1")
.setHeader(CORRELATION_ID, 1)
.setHeader(SEQUENCE_SIZE,2)
.setHeader(SEQUENCE_NUMBER,1)
.setHeader("a", 1)
.build());
channel1.send(MessageBuilder.withPayload("p2")
.setHeader(CORRELATION_ID, 1)
.setHeader(SEQUENCE_SIZE,2)
.setHeader(SEQUENCE_NUMBER,2)
.setHeader("a", 2)
.build());
}
#Bean
public MessageChannel channel1() {
return MessageChannels.direct().get();
}
#Bean
public MessageChannel channel2() {
return MessageChannels.publishSubscribe().get();
}
#Bean
public IntegrationFlow flow1() {
return IntegrationFlows
.from("channel1")
.aggregate(a -> a
.releaseStrategy(new SequenceSizeReleaseStrategy())
.expireGroupsUponCompletion(true)
.sendPartialResultOnExpiry(true))
.channel("channel2")
.get();
}
Output: GenericMessage [payload=[p1, p2], headers={sequenceNumber=2, a=2, correlationId=1, id=b5e51041-c967-1bb4-1601-7e468ae28527, sequenceSize=2, timestamp=1580475773518}]
Headers "a" and "sequenceNumber" were overwritten.
How to aggregate messages with the identical headers?
It must be so
Output: GenericMessage [payload=[p1, p2], headers={sequenceNumber=[1,2], a=[1, 2], correlationId=1, id=b5e51041-c967-1bb4-1601-7e468ae28527, sequenceSize=2, timestamp=1580475773518}]
See AbstractAggregatingMessageGroupProcessor:
/**
* Specify a {#link Function} to map {#link MessageGroup} into composed headers for output message.
* #param headersFunction the {#link Function} to use.
* #since 5.2
*/
public void setHeadersFunction(Function<MessageGroup, Map<String, Object>> headersFunction) {
and also:
/**
* The {#link Function} implementation for a default headers merging in the aggregator
* component. It takes all the unique headers from all the messages in group and removes
* those which are conflicted: have different values from different messages.
*
* #author Artem Bilan
*
* #since 5.2
*
* #see AbstractAggregatingMessageGroupProcessor
*/
public class DefaultAggregateHeadersFunction implements Function<MessageGroup, Map<String, Object>> {
Or just long existing:
/**
* This default implementation simply returns all headers that have no conflicts among the group. An absent header
* on one or more Messages within the group is not considered a conflict. Subclasses may override this method with
* more advanced conflict-resolution strategies if necessary.
* #param group The message group.
* #return The aggregated headers.
*/
protected Map<String, Object> aggregateHeaders(MessageGroup group) {
So, what you need in your aggregate() configuration is an outputProcessor(MessageGroupProcessor outputProcessor) option.
See docs for more info: https://docs.spring.io/spring-integration/docs/5.2.3.RELEASE/reference/html/message-routing.html#aggregatingmessagehandler

Overriding errorChannel configured in #MessagingGateway

I have configured #MessagingGateway as below to use an error channel, which works as expected.
#MessagingGateway(errorChannel = "DefaultInboundErrorHandlerChannel")
public interface InboundMessagingGateway {
#Gateway(requestChannel = "InboundEntryChannel")
void receive(XferRes response);
}
Within the flow I am passing the object to a transformer as below:
Step 1:
#Transformer(inputChannel = "InboundEntryChannel", outputChannel = "TransmissionLogChannel")
public CassandraEntity createEntity(
org.springframework.messaging.Message<XferRes> message) throws ParseException {
XferRes response = message.getPayload();
CassandraEntity entity = new CassandraEntity();
// ... getters & setter ommitted for brevity
return entity;
}
Next, I update the entity as below:
Step 2:
#ServiceActivator(inputChannel = "TransmissionLogChannel", outputChannel="PublishChannel")
public XferRes updateCassandraEntity(
org.springframework.messaging.Message<XferRes> message) {
XferRes response = message.getPayload();
this.cassandraServiceImpl.update(response);
return response;
}
And last, I post to a Kafka topic as below:
Step 3:
#ServiceActivator(inputChannel = "PublishChannel")
public void publish(org.springframework.messaging.Message<XferRes> message){
XferRes response = message.getPayload();
publisher.post(response);
}
In case of an error I post the message to a service which publishes the error object to log ingestion:
#ServiceActivator(inputChannel="defaultInboundErrorHandlerChannel")
public void handleInvalidRequest(org.springframework.messaging.Message<MessageHandlingException> message) throws ParseException {
XferRes originalRequest = (XferRes) message.getPayload().getFailedMessage().getPayload();
this.postToErrorBoard(originalRequest)
}
If an error occurs at Step 2: in updating the DB, then also I want to invoke Step 3. A trivial way is to remove the Step 2 & make the call to update database from Step 1.
Is there any other way in Spring Integration where I can invoke Step 3 irrespective if an error occurs or not.
This technique called PublishSubscribeChannel. Since I see that you reuse a payload on the second step to send to the third step, then it is definitely a use-case for the PublishSubscribeChannel and two sequential subscribers to it.
I mean you create a PublishSubscribeChannel #Bean and those #ServiceActivators are use the name to this channel.
More info is in the Reference Manual. Pay attention to the ignoreFailures property:
/**
* Specify whether failures for one or more of the handlers should be
* ignored. By default this is <code>false</code> meaning that an Exception
* will be thrown whenever a handler fails. To override this and suppress
* Exceptions, set the value to <code>true</code>.
* #param ignoreFailures true if failures should be ignored.
*/
public void setIgnoreFailures(boolean ignoreFailures) {

GRPC client onNext does not fail if there is no server

I have a simple gRPC client as follows:
/**
* Client that calls gRPC.
*/
public class Client {
private static final Context.Key<String> URI_CONTEXT_KEY =
Context.key(Constants.URI_HEADER_KEY);
private final ManagedChannel channel;
private final DoloresRPCStub asyncStub;
/**
* Construct client for accessing gRPC server at {#code host:port}.
* #param host
* #param port
*/
public Client(String host, int port) {
this(ManagedChannelBuilder.forAddress(host, port).usePlaintext(true));
}
/**
* Construct client for accessing gRPC server using the existing channel.
* #param channelBuilder {#link ManagedChannelBuilder} instance
*/
public Client(ManagedChannelBuilder<?> channelBuilder) {
channel = channelBuilder.build();
asyncStub = DoloresRPCGrpc.newStub(channel);
}
/**
* Closes the client
* #throws InterruptedException
*/
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
/**
* Main async method for communication between client and server
* #param responseObserver user's {#link StreamObserver} implementation to handle
* responses received from the server.
* #return {#link StreamObserver} instance to provide requests into
*/
public StreamObserver<Request> downloading(StreamObserver<Response> responseObserver) {
return asyncStub.downloading(responseObserver);
}
public static void main(String[] args) {
Client cl = new Client("localhost", 8999); // fail??
StreamObserver<Request> requester = cl.downloading(new StreamObserver<Response>() {
#Override
public void onNext(Response value) {
System.out.println("On Next");
}
#Override
public void onError(Throwable t) {
System.out.println("Error");
}
#Override
public void onCompleted() {
System.out.println("Completed");
}
}); // fail ??
System.out.println("Start");
requester.onNext(Request.newBuilder().setUrl("http://my-url").build()); // fail?
requester.onNext(Request.newBuilder().setUrl("http://my-url").build());
requester.onNext(Request.newBuilder().setUrl("http://my-url").build());
requester.onNext(Request.newBuilder().setUrl("http://my-url").build());
System.out.println("Finish");
}
}
I don't start any server and run the main method. I would suppose that the program fails on:
client creation
client.downloading call
or observer.onNext
but suprisingly (for me), the code runs successfully, only messages got lost. The output is:
Start
Finish
Error
Because of the asynchronnous nature, the finish can be called even before an error is propagated at least through the response observer. Is that a desired behavior? I can't lose any messages. Am I missing something?
Thank you, Adam
This is the intended behavior. As you mentioned the API is asynchronous and so errors must generally be asynchronous as well. gRPC does not guarantee message delivery and in the case of a streaming RPC failure does not indicate which messages were received by the remote side. The advanced ClientCall API calls this out.
If you need stronger guarantees it must be added at the application-level, such as with replies or with a Status of OK. As an example, in gRPC + Image Upload I mention using a bidirectional stream for acknowledgements.
Creating a ManagedChannelBuilder does not error because the channel is lazy: it only creates a TCP connection when necessary (and reconnects when necessary). Also since most failures are transient, we wouldn't want to prevent all future RPCs on the channel just because your client happened to start when the network was broken.
Since the API is asynchronous already, grpc-java can purposefully throw away messages when sending even when it knows an error has occurred (i.e., it chooses not to throw). Thus almost all errors are delivered to the application via onError().

Testing JMS and Spring Integration

I'm trying to write down a test class in order to test that a message driven channel adapter listening on a JMS Queue is forwarding the message to the right channel (ref. Advanced Spring Integration Testing). Following is the test context xml:
<!-- MockRunner configuration -->
<bean id="destinationManager" class="com.mockrunner.jms.DestinationManager"/>
<bean id="outgoingDestination" factory-bean="destinationManager" factory-method="createQueue">
<constructor-arg index="0" value="demoMockRunnerQueue"/>
</bean>
<bean id="configurationManager" class="com.mockrunner.jms.ConfigurationManager"/>
<bean id="connectionFactory" class="com.mockrunner.mock.jms.MockQueueConnectionFactory">
<constructor-arg index="0" ref="destinationManager"/>
<constructor-arg index="1" ref="configurationManager"/>
</bean>
<!-- Spring JMS Template -->
<bean id="jmsTemplate" class="org.mockito.Mockito" factory-method="mock">
<constructor-arg value="org.springframework.jms.core.JmsTemplate" />
</bean>
Here is the spring integration configuration with the message driver channel:
<int:channel id="inbound"/>
<int-jms:message-driven-channel-adapter id="jmsIn"
channel="inbound"
destination="outgoingDestination"
connection-factory="connectionFactory"
acknowledge="transacted"/>
<int:service-activator input-channel="inbound"
ref="messageQueueConsumer"
method="consumeMessage"/>
<bean id="messageQueueConsumer" class="uk.co.example.consumer.SimpleMessageConsumer">
</bean>
And following there's the java class containing the test:
#Resource
JmsTemplate jmsTemplate;
/**
* "inbound" is the channel used to trigger the service activator (i.e. the message consumer)
* */
#Resource
#Qualifier("inbound")
SubscribableChannel inbound;
private static final Logger LOGGER = Logger.getLogger(InboundChannelFlowUnitTest.class);
/**
* This test verifies that a message received on a polling JMS inbound channel adapter is
* routed to the designated channel and that the message payload is as expected
*
* #throws JMSException
* #throws InterruptedException
* #throws IOException
*/
#Test
public void testReceiveMessage() throws JMSException, InterruptedException, IOException {
String msg = "hello";
boolean sent = verifyJmsMessageReceivedOnChannel(msg, inbound, new CountDownHandler() {
#Override
protected void verifyMessage(Message<?> message) {
assertEquals("hello", message.getPayload());
}
}
);
assertTrue("message not sent to expected output channel", sent);
}
/**
* Provide a message via a mock JMS template and wait for the default timeout to receive the message on the expected channel
* #param obj The message provided to the poller (currently must be a String)
* #param expectedOutputChannel The expected output channel
* #param handler An instance of CountDownHandler to handle (verify) the output message
* #return true if the message was received on the expected channel
* #throws JMSException
* #throws InterruptedException
*/
protected boolean verifyJmsMessageReceivedOnChannel(Object obj, SubscribableChannel expectedOutputChannel, CountDownHandler handler) throws JMSException, InterruptedException{
return verifyJmsMessageOnOutputChannel(obj, expectedOutputChannel, handler, 2000);
}
/**
* Provide a message via a mock JMS template and wait for the specified timeout to receive the message on the expected channel
* #param obj The message provided to the poller (currently must be a String)
* #param expectedOutputChannel The expected output channel
* #param handler An instance of CountDownHandler to handle (verify) the output message
* #param timeoutMillisec The timeout period. Note that this must allow at least enough time to process the entire flow. Only set if the default is
* not long enough
* #return true if the message was received on the expected channel
* #throws JMSException
* #throws InterruptedException
*/
protected boolean verifyJmsMessageOnOutputChannel(Object obj, SubscribableChannel expectedOutputChannel, CountDownHandler handler,int timeoutMillisec) throws JMSException,
InterruptedException {
if (!(obj instanceof String)) {
throw new IllegalArgumentException("Only TextMessage is currently supported");
}
/*
* Use mocks to create a message returned to the JMS inbound adapter. It is assumed that the JmsTemplate
* is also a mock.
*/
TextMessage message = mock(TextMessage.class);
doReturn(new SimpleMessageConverter()).when(jmsTemplate).getMessageConverter();
doReturn(message).when(jmsTemplate).receiveSelected(anyString());
String text = (String) obj;
CountDownLatch latch = new CountDownLatch(1);
handler.setLatch(latch);
doReturn(text).when(message).getText();
expectedOutputChannel.subscribe(handler);
boolean latchCountedToZero = latch.await(timeoutMillisec, TimeUnit.MILLISECONDS);
if (!latchCountedToZero) {
LOGGER.warn(String.format("The specified waiting time of the latch (%s ms) elapsed.", timeoutMillisec));
}
return latchCountedToZero;
}
/*
* A MessageHandler that uses a CountDownLatch to synchronize with the calling thread
*/
private abstract class CountDownHandler implements MessageHandler {
CountDownLatch latch;
public final void setLatch(CountDownLatch latch){
this.latch = latch;
}
protected abstract void verifyMessage(Message<?> message);
/*
* (non-Javadoc)
*
* #see
* org.springframework.integration.core.MessageHandler#handleMessage
* (org.springframework.integration.Message)
*/
public void handleMessage(Message<?> message) throws MessagingException {
verifyMessage(message);
latch.countDown();
}
}
But I get the following exception:
[0;33mWARN [main] [InboundChannelFlowUnitTest] The specified waiting time of the latch (2000 ms) elapsed.
[m
java.lang.AssertionError: message not sent to expected output channel
Any hint on that?
EDIT:
I added the following test:
#SuppressWarnings("unchecked")
#Test
public void testMessageDriven() throws Exception {
TextMessage message = mock(TextMessage.class);
when(message.getText()).thenReturn("foo");
Session session = mock(Session.class);
((SessionAwareMessageListener<TextMessage>) this.messageListenerContainer.getMessageListener()).onMessage(message, session);
CountDownHandler myCountDownHandler = new CountDownHandler() {
#Override
protected void verifyMessage(Message<?> message) {
assertNotNull(message);
assertEquals("hello", message.getPayload());
}
};
CountDownLatch myLatch = new CountDownLatch(2);
myCountDownHandler.setLatch(myLatch);
this.inbound.subscribe(myCountDownHandler);
boolean receivedBeforeZero = myLatch.await(3, TimeUnit.SECONDS);
assertTrue(receivedBeforeZero);
}
And changed the message-driven adapter to:
<int-jms:message-driven-channel-adapter id="jmsIn"
channel="inbound"
container="messageListenerContainer"
acknowledge="transacted"/>
But still get the following error:
[0;33mWARN [main] [InboundChannelFlowUnitTest] The specified waiting time of the latch (3 sec) elapsed.
[m
java.lang.AssertionError
at org.junit.Assert.fail(Assert.java:92)
at org.junit.Assert.assertTrue(Assert.java:43)
at org.junit.Assert.assertTrue(Assert.java:54)
The message-driven adapter doesn't use a JmsTemplate so mocking it and its receive methods won't do anything.
You would have to mock/stub a message listener container and invoke its MessageListener. You can provide your mock container to the adapter via the 'container' attribute.
EDIT:
It's not entirely clear why you need to mock/test framework components; you can simply inject a test message into your flow by sending it to the channel.
However, if you are using custom message converters, and you want to test it in-place, you could mock the container.
Here's how to do it.

Resources