fix: Slow RabbitMQ message polling

Configurable RabbitMQ polling in order that more messages can be obtained from the queue during a poll.

Fixes #8445
This commit is contained in:
driesva 2023-06-02 09:11:49 +02:00
parent 733dcea36a
commit ee45763ceb
4 changed files with 156 additions and 7 deletions

View File

@ -1117,6 +1117,7 @@ queue:
automatic_recovery_enabled: "${TB_QUEUE_RABBIT_MQ_AUTOMATIC_RECOVERY_ENABLED:false}" automatic_recovery_enabled: "${TB_QUEUE_RABBIT_MQ_AUTOMATIC_RECOVERY_ENABLED:false}"
connection_timeout: "${TB_QUEUE_RABBIT_MQ_CONNECTION_TIMEOUT:60000}" connection_timeout: "${TB_QUEUE_RABBIT_MQ_CONNECTION_TIMEOUT:60000}"
handshake_timeout: "${TB_QUEUE_RABBIT_MQ_HANDSHAKE_TIMEOUT:10000}" handshake_timeout: "${TB_QUEUE_RABBIT_MQ_HANDSHAKE_TIMEOUT:10000}"
max_poll_messages: "${TB_QUEUE_RABBIT_MQ_MAX_POLL_MESSAGES:1}"
queue-properties: queue-properties:
rule-engine: "${TB_QUEUE_RABBIT_MQ_RE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" rule-engine: "${TB_QUEUE_RABBIT_MQ_RE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}"
core: "${TB_QUEUE_RABBIT_MQ_CORE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}" core: "${TB_QUEUE_RABBIT_MQ_CORE_QUEUE_PROPERTIES:x-max-length-bytes:1048576000;x-message-ttl:604800000}"

View File

@ -20,6 +20,8 @@ import com.google.protobuf.InvalidProtocolBufferException;
import com.rabbitmq.client.Channel; import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection; import com.rabbitmq.client.Connection;
import com.rabbitmq.client.GetResponse; import com.rabbitmq.client.GetResponse;
import java.util.ArrayList;
import java.util.Collection;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin; import org.thingsboard.server.queue.TbQueueAdmin;
@ -31,7 +33,6 @@ import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -44,6 +45,7 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
private final TbQueueMsgDecoder<T> decoder; private final TbQueueMsgDecoder<T> decoder;
private final Channel channel; private final Channel channel;
private final Connection connection; private final Connection connection;
private final int maxPollMessages;
private volatile Set<String> queues; private volatile Set<String> queues;
@ -51,6 +53,7 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
super(topic); super(topic);
this.admin = admin; this.admin = admin;
this.decoder = decoder; this.decoder = decoder;
this.maxPollMessages = rabbitMqSettings.getMaxPollMessages();
try { try {
connection = rabbitMqSettings.getConnectionFactory().newConnection(); connection = rabbitMqSettings.getConnectionFactory().newConnection();
} catch (IOException | TimeoutException e) { } catch (IOException | TimeoutException e) {
@ -70,18 +73,33 @@ public class TbRabbitMqConsumerTemplate<T extends TbQueueMsg> extends AbstractTb
protected List<GetResponse> doPoll(long durationInMillis) { protected List<GetResponse> doPoll(long durationInMillis) {
List<GetResponse> result = queues.stream() List<GetResponse> result = queues.stream()
.map(queue -> { .map(queue -> {
List<GetResponse> messages = new ArrayList<>();
for (int i = 0; i < maxPollMessages; i++) {
GetResponse response = doQueuePoll(queue);
if (response == null) {
break;
}
messages.add(response);
}
return messages;
})
.filter(r -> !r.isEmpty())
.flatMap(Collection::stream)
.collect(Collectors.toList());
if (result.size() > 0) {
return result;
} else {
return Collections.emptyList();
}
}
protected GetResponse doQueuePoll(String queue) {
try { try {
return channel.basicGet(queue, false); return channel.basicGet(queue, false);
} catch (IOException e) { } catch (IOException e) {
log.error("Failed to get messages from queue: [{}]", queue); log.error("Failed to get messages from queue: [{}]", queue);
throw new RuntimeException("Failed to get messages from queue.", e); throw new RuntimeException("Failed to get messages from queue.", e);
} }
}).filter(Objects::nonNull).collect(Collectors.toList());
if (result.size() > 0) {
return result;
} else {
return Collections.emptyList();
}
} }
@Override @Override

View File

@ -47,6 +47,8 @@ public class TbRabbitMqSettings {
private int connectionTimeout; private int connectionTimeout;
@Value("${queue.rabbitmq.handshake_timeout:}") @Value("${queue.rabbitmq.handshake_timeout:}")
private int handshakeTimeout; private int handshakeTimeout;
@Value("${queue.rabbitmq.max_poll_messages:1}")
private int maxPollMessages;
private ConnectionFactory connectionFactory; private ConnectionFactory connectionFactory;

View File

@ -0,0 +1,128 @@
/**
* Copyright © 2016-2023 The Thingsboard 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.thingsboard.server.queue.rabbitmq;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.GetResponse;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.queue.TbQueueAdmin;
import org.thingsboard.server.queue.TbQueueMsgDecoder;
import org.thingsboard.server.queue.common.DefaultTbQueueMsg;
@ExtendWith(MockitoExtension.class)
class TbRabbitMqConsumerTemplateTest {
private static final String TOPIC = "some-topic";
@Mock
private TbQueueAdmin admin;
@Mock
private ConnectionFactory connectionFactory;
@Mock
private TbQueueMsgDecoder<DefaultTbQueueMsg> decoder;
@Mock
private Connection connection;
@Mock
private Channel channel;
@Mock
private TopicPartitionInfo partition;
@Mock
private GetResponse getResponse;
private TbRabbitMqConsumerTemplate<DefaultTbQueueMsg> consumer;
private void setUpConsumerWithMaxPollMessages(int maxPollMessages) throws Exception {
when(connectionFactory.newConnection()).thenReturn(connection);
when(connection.createChannel()).thenReturn(channel);
TbRabbitMqSettings settings = new TbRabbitMqSettings();
settings.setMaxPollMessages(maxPollMessages);
settings.setConnectionFactory(connectionFactory);
consumer = new TbRabbitMqConsumerTemplate<>(admin, settings, TOPIC, decoder);
when(partition.getFullTopicName()).thenReturn(TOPIC);
consumer.subscribe(Set.of(partition));
}
@Test
void pollWithMax5PollMessagesReturnsEmptyListIfNoMessages() throws Exception {
setUpConsumerWithMaxPollMessages(5);
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(null);
assertThat(consumer.poll(0L)).isEmpty();
verify(channel).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax5PollMessagesReturns5MessagesIfQueueContains5() throws Exception {
setUpConsumerWithMaxPollMessages(5);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse);
assertThat(consumer.poll(0L)).hasSize(5);
verify(channel, times(5)).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax1PollMessageReturns1MessageIfQueueContainsMore() throws Exception {
setUpConsumerWithMaxPollMessages(1);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse);
assertThat(consumer.poll(0L)).hasSize(1);
verify(channel).basicGet(anyString(), anyBoolean());
}
@Test
void pollWithMax3PollMessagesReturns2MessagesIfQueueContains2() throws Exception {
setUpConsumerWithMaxPollMessages(3);
when(getResponse.getBody()).thenReturn(newMessageBody());
when(channel.basicGet(anyString(), anyBoolean())).thenReturn(getResponse, getResponse, null);
assertThat(consumer.poll(0L)).hasSize(2);
verify(channel, times(3)).basicGet(anyString(), anyBoolean());
}
private byte[] newMessageBody() {
return ("{\"key\": \"" + UUID.randomUUID() + "\"}").getBytes(StandardCharsets.UTF_8);
}
}