spring amqp (rabbitmq) and sending to DLQ when exception occurs - spring

I am using org.springframework.boot:spring-boot-starter-amqp:2.6.6 .
According to the documentation, I set up #RabbitListener - I use SimpleRabbitListenerContainerFactory and the configuration looks like this:
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(ObjectMapper om) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory());
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
factory.setConcurrentConsumers(rabbitProperties.getUpdater().getConcurrentConsumers());
factory.setMaxConcurrentConsumers(rabbitProperties.getUpdater().getMaxConcurrentConsumers());
factory.setMessageConverter(new Jackson2JsonMessageConverter(om));
factory.setAutoStartup(rabbitProperties.getUpdater().getAutoStartup());
factory.setDefaultRequeueRejected(false);
return factory;
}
The logic of the service is to receive messages from rabbitmq, contact an external service via the rest API (using rest template) and put some information into the database based on the results of the response (using spring data jpa). The service implemented it successfully, but during testing it ran into problems that if any exceptions occur during the work of those thrown up the stack, the message is not sent to the configured dlq, but simply hangs in the broker as unacked. Can you please tell me how you can tell spring amqp that if any error occurs, you need to redirect the message to dlq?
The listener itself looks something like this:
#RabbitListener(
queues = {"${rabbit.updater.consuming.queue.name}"},
containerFactory = "rabbitListenerContainerFactory"
)
#Override
public void listen(
#Valid #Payload MessageDTO message,
Channel channel,
#Header(AmqpHeaders.DELIVERY_TAG) Long deliveryTag
) {
log.debug(DebugMessagesConstants.RECEIVED_MESSAGE_FROM_QUEUE, message, deliveryTag);
messageUpdater.process(message);
channel.basicAck(deliveryTag, false);
log.debug(DebugMessagesConstants.PROCESSED_MESSAGE_FROM_QUEUE, message, deliveryTag);
}
In rabbit managment it look something like this:
enter image description here
and unacked will hang until the queue consuming application stops

See error handling documentation: https://docs.spring.io/spring-amqp/docs/current/reference/html/#annotation-error-handling.
So, you just don't do an AcknowledgeMode.MANUAL and rely on the Dead Letter Exchange configuration for those messages which are rejected in case of error.
Or try to use a this.channel.basicNack(deliveryTag, false, false) in case of messageUpdater.process(message); exception...

Related

Handling validation error of a JSON payload in Spring RabbitListener

Let's take the following consumer method for a RabbitMQ queue. Ths payload received from the queue is in JSON format, so I register a bean returning a Jackson2JsonMessageConverter. This basically works fine.
Now I'd like to add a validation of the QueueResponse object, similar to when using Jackson in a #RestController, e.g. if the JSON field does not exist or contains an invalid value. In this case, I'd like the code to execute the catch block, i.e. throwing an AmqpRejectAndDontRequeueException.
Thus, I added #Payload #Valid as described in the documentation. But I don't know what to do in the validationErrorHandler method. I don't understand the return statement from the documentation. What would I need to do there to reach the catch block?
#RabbitListener(queues = QUEUE_NAME, messageConverter = "jackson2MessageConverter", errorHandler="validationErrorHandler")
public void consume(#Payload #Valid QueueResponse queueResponse) {
try {
processMessage(queueResponse);
} catch (Exception e) {
throw new AmqpRejectAndDontRequeueException(e.getMessage());
}
}
#Bean
public MessageConverter jackson2MessageConverter() {
return new Jackson2JsonMessageConverter(objectMapper);
}
// Not sure what to do here...
#Bean
public RabbitListenerErrorHandler validationErrorHandler() {
return (m, e) -> {
...
};
}
If the error handler exits normally, the message will be acknowledged (discarded).
If the error handler throws an exception, the message will either be requeued (and redelivered) or discarded (and optionally sent to a dead letter queue), depending on the exception type, container properties, and queue arguments.
Basically what I do in the RabbitListenerErrorHandler is the following:
check how many times I requeued a message by looking into the count property in the x-death header
then decide to either requeue the message by throwing an AmqpRejectAndDontRequeueException or not. In this last case, instead of just discard the message, send it to a parking-lot exchange (bound to a queue with no consumer, in my case these queues are monitored externally) with additional information (for instance the stack trace of the last failure)

AWS SQS Consumer not consuming messages

I am using the spring-cloud-starter-aws-messaging in my Spring Boot application and #sqslistener to consume SQS messages. my consumer weirdly stops getting messages out of nowhere and the ApproximateNumberOfMessagesVisible gradually increases triggering a CloudWatch alarm. I could see no error logs that are generated before it stops getting any more messages. Am I missing something?
#SqsListener(value = "${sqs.queue.url.indexSavedSetQueue}",
deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS)
public void listenIndexSavedSetEvent(#NonNull String message) throws IOException {
log.info("Index saved set event received, message: {}", message);
IndexSavedSetPayloadDto indexSavedSetPayloadDto = objectMapper
.readValue(message, IndexSavedSetPayloadDto.class);
String setName = indexSavedSetPayloadDto.getSetName();
indexerService.indexSet(setName);
}

Spring integration (Manual Acking)

I want to create a simple IntegrationFlow with Spring integration, and I am having difficulties.
I want to create an integration flow that takes messages from a queue in Rabbit Mq and posts the messages to an endpoint Rest. I want to ack manually depending on the results of the post that I will make.
A typical behavior of the integration Flow would be like this:
I receive a message in the queue.
Spring detects it, takes the message and posts it in the Rest endpoint.
The end point responds with a 200 code.
Spring integration ack the message.
If the endpoint responds with an error code I want to be able to nack or retry.
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
RestTemplate restTemplate = new RestTemplate();
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
container.setQueueNames(BOUTIQUE_QUEUE_NAME);
/* Get Message from RabbitMQ */
return IntegrationFlows.from(Amqp.inboundAdapter(container))
.handle(msg ->
{
String msgString = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
HttpEntity<String> requestBody = new HttpEntity<String>(msgString, headers);
restTemplate.postForObject(ENDPOINT_LOCAL_URL, requestBody, String.class);
System.out.println(msgString);
})
.get();
You don't need to use manual acknowledge mode for this use case; if he rest call returns normally, the container will ack the message; if an exception is thrown, the container will nack the message and it will be redelivered.
If you use manual acks, the Channel and deliveryTag are available in the AmqpHeaders.CHANNEL and AmqpHeaders.DELIVERY_TAG message headers and you can call basicAck or basicReject on the channel (you will have to add an error channel to the inbound adapter to handle errors.

Spring integration headers on message events

I have a simple TCP connection factory implemented in Spring Integration:
#Bean
#ServiceActivator(inputChannel = "toTcpChannel")
public TcpSendingMessageHandler tcpOutClient() throws Exception {
TcpSendingMessageHandler sender = new TcpSendingMessageHandler();
sender.setConnectionFactory(clientFactory());
sender.setClientMode(false);
sender.afterPropertiesSet();
return sender;
}
#Bean
public AbstractClientConnectionFactory clientFactory() {
final TcpNioClientConnectionFactory factory = new TcpNioClientConnectionFactory(tcpHost, tcpPort);
factory.setSingleUse(true);
return factory;
}
#EventListener
public void handleTcpConnectionOpenEvent(TcpConnectionOpenEvent event) throws Exception {
LOGGER.info("TCP connection OPEN event: {}", event.getConnectionId());
// HERE I would like to have "myCustomID" header here.
}
I am looking for getting the custom ID that I am providing via Gateway in the produced TcpConnectionOpenEvent (or similar via interceptors)
#Gateway(requestChannel="toTcpChannel")
public void sendToTcp(#Payload String message, #Header("myCustomID") Long myCustomID);
I know this is an event not a message but I do know how to get the Connection ID that I will receive in the input channel in any other way.
I am creating a type of hash map of my custom id – connection id.
I cannot use a custom correlation via aggregator because the response message will not contain any information about the previously sent message. Any suggestions will be welcome.
Oh! I see. Not sure what you are going to do from your custom TcpSendingMessageHandler, but as far as ApplicationEventPublisher is single-threaded, you can store the connectionId in the ThreadLocal variable and obtain it from there after send operation.

Spring send message to Websocket Message Broker

I want to send a message to websocket subscribers of a specific record - when an action takes place in one of my service class.
I'm trying to read the Spring Websocket documentation but it's kind of ambiguous to the point of how to get all these things working together.
Here are my setup files (this is extending jHipster btw):
WebsocketConfiguration.java
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableStompBrokerRelay("/queue/", "/topic/", "/exchange/");
config.setApplicationDestinationPrefixes("/app");
config.setPathMatcher(new AntPathMatcher("."));
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").withSockJS();
}
WebsocketSecurity.java
#Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
// message types other than MESSAGE and SUBSCRIBE
.nullDestMatcher().authenticated()
// matches any destination that starts with /rooms/
.simpDestMatchers("/topic/tracker").hasAuthority(AuthoritiesConstants.ADMIN)
.simpDestMatchers("/topic/**").authenticated()
// (i.e. cannot send messages directly to /topic/, /queue/)
// (i.e. cannot subscribe to /topic/messages/* to get messages sent to
// /topic/messages-user<id>)
.simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll()
// catch all
.anyMessage().denyAll();
}
Controller class (attempt at implementing a simple broker I can test subscribing to from sockjs and recieving messages generated elsewhere in the application:
#MessageMapping("/ws")
#SendTo("/topic/sendactivity.{id}")
public void activity(#DestinationVariable string id, #Payload String message){
log.debug("Sending command center: "+message);
}
#RequestMapping(value = "/updateactivity", method = RequestMethod.PUT)
public ResponseEntity<Membership> updateMembership(
#RequestBody Membership membership) throws URISyntaxException {
// ...
String testString = "test";
messagingTemplate.convertAndSend("/topic/commandcenter"+membership.getId().toString(), testString);
// ...
}
When I put a breakpoint on the public void activity method, I don't get anything?
Sending a message to "/topic/commandcenterID" using the messaging template will send that message to the message broker, which will dispatch that message to clients subscribed to that topic. So it won't flow through your activity method.
When using #MessageMapping annotated methods, you're declaring those as application destinations. So sending a message to "/app/ws" should map to that method. Note that in that case I doubt it'll work since the destination variable you're expecting as a method argument is missing from the path definition in the #MessageMapping annotation.
Also, the #SendTo annotation in fact tells Spring that the value returned by the method should be converted to a message and sent to the given destination.
It seems you're mixing things up here, and I think you should:
read carefully the flow of messages in Spring STOMP support
look at a few example apps like the websocket portfolio and websocket chat

Resources