RabbitMQ + Spring RabbitTemplate: timeout for "convertAndSend" - java

I've got a fairly simple code that uses Spring's RabbitTemplate to send messages to RabbitMQ. The code is not interested in receiving messages, it is a simple fire and forget scenario.
rabbitTemplate.convertAndSend(exchange, routingKey, payload);
The template is created like this (please note that I'm using a transacted channel):
#Bean
public RabbitTemplate rabbitTemplate() {
val rabbitTemplate = new RabbitTemplate(connectionFactory());
rabbitTemplate.setChannelTransacted(true);
rabbitTemplate.setMessageConverter(jsonConverter());
return rabbitTemplate;
}
I've faced an issue when the RabbitMQ server was overloaded and this call hanged for a long time and never timed out. The connection itself did not die, but RabbitMQ server had nearly full RAM and 100% CPU usage, so it wasn't responsive.
Is there a way to configure either Spring's RabbitTemplate or the underlying AmqpTemplate to time out on a simple send if it blocks for too long?

Related

Rabbit MQ + Spring Boot: delay between resend broken messages

I'm creating application using Spring Boot with RabbitMQ.
I've created configuration for Rabbit like this:
#Configuration
public class RabbitConfiguration {
public static final String RESEND_DISPOSAL_QUEUE = "RESEND_DISPOSAL";
#Bean
public Queue resendDisposalQueue() {
return new Queue(RESEND_DISPOSAL_QUEUE, true);
}
#Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory (ConnectionFactory connectionFactoryr) {
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
return factory;
}
#Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory){
return new RabbitTemplate(connectionFactory);
}
}
Also I've created listener for Rabbit messages like this:
#RabbitListener(queues = RESEND_DISPOSAL_QUEUE)
public void getResendDisposalPayload(String messageBody){
LOGGER.info("[getResendDisposalPayload] message = {}", messageBody);
// And there is some business logic
}
All works pretty good, but there is one problem.
When I got exception in method getResendDisposalPayload which listens RESEND_DISPOSAL_QUEUE queue (for example temporary problem with database) Rabbit starts resend last not processed message without any delay. It produces a big amount of log and for some reason uncomfortable for my system.
As I've read in this article https://www.baeldung.com/spring-amqp-exponential-backoff "While using a Dead Letter Queue is a standard way to deal with failed messages".
In order to use this pattern I've to create RetryOperationsInterceptor which defines count attempt to deliver message and delay between attempts.
For example:
#Bean
public RetryOperationsInterceptor retryInterceptor() {
return RetryInterceptorBuilder.stateless()
.backOffOptions(1000, 3.0, 10000)
.maxAttempts(3)
.recoverer(messageRecoverer)
.build();
}
It sounds very good but only one problem: I can't define infinity attempt amount in options maxAttempts.
After maxAttempts I have to save somewhere broken message and deal with it in the future. It demands some extra code.
The question is: Is there any way to configure Rabbit to infinity resend broken messages with some delay, say with one second delay?
Rabbit starts resend last not processed message without any delay
That's how redelivery works: it re-push the same message again and again, until you ack it manually or drop altogether. There is no delay in between redeliveries just because an new message is not pulled from the queue until something is done with this one.
I can't define infinity attempt amount in options maxAttempts
Have you tried an Integer.MAX_VALUE? Pretty decent number of attempts.
The other way is to use a Delayed Exchange: https://docs.spring.io/spring-amqp/docs/current/reference/html/#delayed-message-exchange.
You can configure that retry with a RepublishMessageRecoverer to publish into a your original queue back after some attempts are exhausted: https://docs.spring.io/spring-amqp/docs/current/reference/html/#async-listeners

How to fix ActiveMQ durable subscription throwing "durable consumer already in use" error

I'm trying to write a basic ActiveMQ client to listen to a topic. I'm using Spring Boot ActiveMQ. I have an implementation built off of various tutorials that uses DefaultJmsListenerContainerFactory, but I am having some issues getting it working properly.
#Configuration
#EnableJms
public class JmsConfig {
#Bean
public DefaultJmsListenerContainerFactory jmsContainerFactory(ConnectionFactory connectionFactory,
DefaultJmsListenerContainerFactoryConfigurer configurer) {
DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
factory.setConcurrency("3-10");
factory.setConnectionFactory(connectionFactory);
configurer.configure(factory, connectionFactory);
factory.setSubscriptionDurable(true);
factory.setClientId("someUniqueClientId");
return factory;
}
}
#JmsListener(destination="someTopic", containerFactory="jmsContainerFactory", subscription="someUniqueSubscription")
public void onMessage(String msg) {
...
}
Everything works fine, until I try to get a durable subscription going. When I do that, I'm finding that with the client id set on the container factory, I get an error about how the client id cannot be set on a shared connection.
Cause: setClientID call not supported on proxy for shared Connection. Set the 'clientId' property on the SingleConnectionFactory instead.
When I change the code to set the client id on the connection factory instead (it's a CachingConnectionFactory wrapping an ActiveMQConnectionFactory), the service starts up successfully, reads a couple messages and then starts consistently outputting this error:
Setup of JMS message listener invoker failed for destination 'someTopic' - trying to recover. Cause: Durable consumer is in use for client: someUniqueClientId and subscriptionName: someUniqueSubscription
I continue to receive messages, but also this error inter-mingled in the logs. This seems like it is probably a problem, but I'm really not clear on how to fix it.
I do have a naive implementation of this going without any spring code, using ActiveMQConnectionFactory directly and it seems happy to use a durable consumer (but it has its own different issues). In any case, I don't think it's a lack of support for durable connections on the other side.
I'm hoping someone with more experience in this area can help me figure out if this error is something I can ignore, or alternatively what I need to do to address it.
Thanks!
JMS 1.1 (which is what you're using since you're using ActiveMQ 5.x) doesn't support shared durable subscriptions. Therefore, when you use setConcurrency("3-10") and Spring tries to create > 1 subscription you receive an error. I see two main ways to solve this problem:
Use setConcurrency("1") which will limit the number of subscribers/consumers to 1. Depending on your requirements this could have a severe negative performance impact.
Switch to ActiveMQ Artemis which does support JMS 2.0 and invoke setSubscriptionShared(true).

Does JMSTemplate producer open a thread for each message?

I'm building a REST API application with Spring Boot 2.1.6. I want to use JMS messaging in my app with Active MQ package (org.apache.activemq). I have MyEventController class which receives all kinds of events through http requests. I then want to send the information about the events as a JMS message to a topic so that the message consumer will update the database with the information from the events.
The reason I want to use JMS here is to not hold the Spring thread which handle http request and have the consumer open a separate thread to do potentially a lot of time consuming updates to the database. However I'm wondering if JMSTemplate stays always one thread. Because if a new thread is opened for each http request then the solution is not so scalable.
This is my code for producer:
#RestController
public class MyEventController {
#Autowired
private DBHandler db;
#Autowired
private JmsTemplate jmsTemplate;
#RequestMapping(method=GET, path=trackingEventPath)
public ResponseEntity<Object> handleTrackingEvent(
#RequestParam(name = Routes.pubId) String pubId,
#RequestParam(name = Routes.event) String event) {
jmsTemplate.convertAndSend("topic1", "info#example.com");
return new ResponseEntity<>(null, new HttpHeaders(), HttpStatus.OK);
}
consumer:
#Component
public class JSMListener {
#JmsListener(destination = "topic1", containerFactory = "topicListenerFactory")
public void receiveTopicMessage(String event) {
// do something...
}
}
JmsTemplate has no concept of background threads or async sending. It's a class design for simplifying usage of java.jms.Session and embedding it with usual Spring concepts e.g. declarative transaction management with #Transactional.
In your example convertAndSend() will execute as part of the request processing thread. The method will block until the JMS broker responds to the application that the message was added to the destination queue or throw an exception if there was a problem e.g. queue was full.

How to return an error message on socket timeout with Spring-Integration

I'm using Spring (Integration) to offer a socket tcp connection. I'm having a specific timeout for this socket.
When the timeout is exceeded, I not only like to shut down the connection (which already works), but also return custom error message (or not closing the connection, but just return the error message). Is that possible?
#Bean
public TcpConnectionFactoryFactoryBean tcpFactory(Converter converter) {
TcpConnectionFactoryFactoryBean factory = new TcpConnectionFactoryFactoryBean();
factory.setType("server");
factory.setPort("8080");
factory.setSingleUse(true);
factory.setSoTimeout(10000); //10s; how to return error message?
return factory;
}
Update:
#Bean
public ApplicationListener<TcpConnectionEvent> tcpErrorListener() {
TcpConnectionEventListeningMessageProducer producer = new TcpConnectionEventListeningMessageProducer();
producer.setEventTypes(new Class[] {TcpConnectionCloseEvent.class});
producer.setOutputChannel(tcpErrorChannel()); //what to do here?
return producer;
}
Actually any closed connection emits TcpConnectionCloseEvent and you can get deal with it using:
<int-event:inbound-channel-adapter channel="connectionClosedChannel"
event-types="org.springframework.integration.ip.tcp.connection.TcpConnectionCloseEvent"/>
Having that you not only close connection, but can do any desired logic with that event-flow.
UPDATE
To use it from JavaConfig:
#Bean
public SmartApplicationListener tcpErrorListener() {
ApplicationEventListeningMessageProducer producer = new ApplicationEventListeningMessageProducer();
producer.setEventTypes(TcpConnectionCloseEvent.class);
producer.setOutputChannel(tcpErrorChannel());
return producer;
}
#Bean
public MessageChannel tcpErrorChannel() {
return new DirectChannel();
}
The requirement is a little unusual - what will the client do when it gets the custom error message?
There is currently no easy way to intercept the timeout; you could do it, but you'd have to subclass the connection factory and the connection objects it creates, and override the handleReadException() method.
It would likely be much easier to just handle the sockets directly in your own component and send messages to your flow using a Messaging Gateway. Or, simply make your component a subclass of MessagingGatewaySupport.
Alternatively you could use something in your downstream flow (and don't set a timeout). When a message is received, schedule a task to send the error message in 10 seconds. When the next message arrives, cancel the scheduled task and schedule a new one.

Is it possible to stop and restart a durable subscription using spring jms?

I'm using the following DefaultMessageListenerContainer to create a durable subscription to get messages even in downtimes which really works well.
#Bean
ConnectionFactory connectionFactory() {
SingleConnectionFactory connectionFactory = new SingleConnectionFactory(new ActiveMQConnectionFactory(
AMQ_BROKER_URL));
connectionFactory.setClientId(CLIENT_ID);
return connectionFactory;
}
#Bean
DefaultMessageListenerContainer container(final MessageListener messageListener,
final ConnectionFactory connectionFactory) {
return new DefaultMessageListenerContainer() {
{
setMessageListener(messageListener);
setConnectionFactory(connectionFactory);
setDestinationName(JMS_TARGET);
setPubSubDomain(true);
setSessionTransacted(true);
setSubscriptionDurable(true);
setDurableSubscriptionName(SUBSCRIPTION_ID);
setConcurrentConsumers(1);
}
};
}
The question is: What is the best way to remove the subscription when I don't need it anymore? Would it even be possible to temporarily remove the subscription (and miss some messages), but enable it later again?
The only way which worked so far, was to shutdown the DMLC and call unsubscribe afterwards.
dmlc.shutdown();
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
session.unsubscribe(SUBSCRIPTION_ID);
Is that a sensible solution? How could the subscription be reinitiated?
I've already seen this answer but I really don't know how to do this? Or would it be even better to subscribe and unsubscribe in a total different way?
Before I suggest my answer, allow me a word of caution: temporarily removing a durable subscription sounds a little bit like you don't really need a durable subscription. Durable subscriptions are for Consumers which need to receive messages that were sent while they are offline. If you need the messages only sometimes (say, when your consumer is connected), a non-durable topic is the item of choice.
Note that this only makes sense for Topics (1:n communication, pub/sub). Queues work slightly different as they are used for 1:1 communication (advanced techniques like load balancing set aside for now).
Durable topics incur a notable resource overhead on the message broker and removing and recreating subscriptions might lead to expensive initialization over and over again.
Now if you really want to (temporarily) unsubscribe from your durable topic (also works for "normal" topics), you'll have to extend DefaultMessageListenerContainer:
public class MyContainer extends DefaultMessageListenerContainer {
public Session retrieveSession() {
return getSession(); // this is protected, so we wrap it (could also make getSession public)
}
}
When you instantiate MyContainer (instead of DefaultMessageListenerContainer) in the container method, make sure to store the reference:
protected MyContainer listenerContainer;
...
listenerContainer = new MyContainer(...);
return listenerContainer;
You can then just call listenerContainer.retrieveSession().unsubscribe(...) to unsubscribe.
Please also note that you may only call this after all the session's consumers to that topic are closed and there are no messages in transit (direct quote from the documentation: "It is erroneous for a client to delete a durable subscription while there is an active (not closed) consumer for the subscription, or while a consumed message is part of a pending transaction or has not been acknowledged in the session."

Categories