-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
10 changed files
with
274 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
src/main/java/io/jaconi/spring/rabbitmq/retry/RetryService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package io.jaconi.spring.rabbitmq.retry; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import lombok.extern.slf4j.Slf4j; | ||
import org.springframework.amqp.core.AmqpTemplate; | ||
import org.springframework.amqp.support.AmqpHeaders; | ||
import org.springframework.amqp.support.converter.MessagingMessageConverter; | ||
import org.springframework.messaging.Message; | ||
import org.springframework.stereotype.Component; | ||
|
||
/** | ||
* Retry AMQP messages as configured in the {@link RetryProperties}. | ||
*/ | ||
@Slf4j | ||
@Component | ||
@RequiredArgsConstructor | ||
public class RetryService { | ||
private static final MessagingMessageConverter CONVERTER = new MessagingMessageConverter(); | ||
|
||
private final AmqpTemplate amqpTemplate; | ||
|
||
/** | ||
* Retry a {@link org.springframework.amqp.core.Message} by increasing the retry attempt in the message header and | ||
* sending the message to the retry exchange. | ||
* | ||
* @param message the {@link org.springframework.amqp.core.Message} | ||
*/ | ||
public void retryMessage(org.springframework.amqp.core.Message message) { | ||
retryMessage((Message<?>) CONVERTER.fromMessage(message)); | ||
} | ||
|
||
/** | ||
* Retry a {@link Message} by increasing the retry attempt in the message header and sending the message to the | ||
* retry exchange. | ||
* | ||
* @param message the {@link Message} | ||
*/ | ||
public void retryMessage(Message<?> message) { | ||
var retry = getRetry(message); | ||
log.info("retrying message (attempt {}): {}", retry, message); | ||
|
||
var routingKey = message.getHeaders().get(AmqpHeaders.RECEIVED_ROUTING_KEY, String.class); | ||
amqpTemplate.convertAndSend(getRetryExchange(message), routingKey, message.getPayload(), m -> { | ||
m.getMessageProperties().setHeader(RetryProperties.RETRY_HEADER, retry); | ||
TechnicalHeadersFilter.filterHeaders(message.getHeaders()) | ||
.forEach(h -> m.getMessageProperties().setHeader(h, message.getHeaders().get(h))); | ||
return m; | ||
}); | ||
} | ||
|
||
/** | ||
* Determine the retry attempt for the {@link Message}. | ||
* | ||
* @param message the {@link Message} | ||
* @return {@literal 1L} for the first retry, {@literal 2L} for the second, and so on | ||
*/ | ||
private long getRetry(Message<?> message) { | ||
Long previousRetryAttempt = message.getHeaders().get(RetryProperties.RETRY_HEADER, Long.class); | ||
if (previousRetryAttempt == null) { | ||
previousRetryAttempt = 0L; | ||
} | ||
|
||
return previousRetryAttempt + 1; | ||
} | ||
|
||
/** | ||
* Determine the retry exchange for a {@link Message}. | ||
* | ||
* @param message the {@link Message} | ||
* @return the retry exchange | ||
*/ | ||
private String getRetryExchange(Message<?> message) { | ||
return RetryProperties.RETRY_EXCHANGE_PATTERN.formatted(message.getHeaders().get(AmqpHeaders.CONSUMER_QUEUE, String.class)); | ||
} | ||
} |
71 changes: 71 additions & 0 deletions
71
src/test/java/io/jaconi/spring/rabbitmq/retry/RabbitMQTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
package io.jaconi.spring.rabbitmq.retry; | ||
|
||
import org.junit.jupiter.api.AfterAll; | ||
import org.junit.jupiter.api.BeforeAll; | ||
import org.springframework.amqp.core.*; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.beans.factory.annotation.Qualifier; | ||
import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
import org.springframework.boot.test.context.SpringBootTest; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.test.context.DynamicPropertyRegistry; | ||
import org.springframework.test.context.DynamicPropertySource; | ||
import org.testcontainers.containers.RabbitMQContainer; | ||
|
||
@SpringBootTest(properties = { | ||
"jaconi.rabbitmq.listener.retry.enabled=true", | ||
"jaconi.rabbitmq.listener.retry.create-resources=true" | ||
}) | ||
abstract class RabbitMQTest { | ||
protected static final String EXCHANGE = "test-exchange"; | ||
protected static final String QUEUE = "test-queue"; | ||
protected static final String ROUTING_KEY = "foo"; | ||
|
||
static final RabbitMQContainer rabbit = new RabbitMQContainer("rabbitmq:3.13-management-alpine"); | ||
|
||
@Autowired | ||
@SuppressWarnings("unused") | ||
protected AmqpTemplate amqpTemplate; | ||
|
||
@BeforeAll | ||
static void beforeAll() { | ||
rabbit.start(); | ||
} | ||
|
||
@AfterAll | ||
static void afterAll() { | ||
rabbit.stop(); | ||
} | ||
|
||
@DynamicPropertySource | ||
@SuppressWarnings("unused") | ||
static void configureProperties(DynamicPropertyRegistry registry) { | ||
registry.add("spring.rabbitmq.addresses", rabbit::getAmqpUrl); | ||
|
||
registry.add("jaconi.rabbitmq.listener.retry.queues.%s.max-attempts".formatted(QUEUE), () -> 2); | ||
registry.add("jaconi.rabbitmq.listener.retry.queues.%s.durations[0]".formatted(QUEUE), () -> "5s"); | ||
registry.add("jaconi.rabbitmq.listener.retry.queues.%s.durations[1]".formatted(QUEUE), () -> "10s"); | ||
} | ||
|
||
@SpringBootApplication | ||
protected abstract static class RabbitMQTestApplication { | ||
|
||
@Bean(QUEUE) | ||
@SuppressWarnings("unused") | ||
Queue queue() { | ||
return new Queue(QUEUE); | ||
} | ||
|
||
@Bean(EXCHANGE) | ||
@SuppressWarnings("unused") | ||
DirectExchange exchange() { | ||
return new DirectExchange(EXCHANGE); | ||
} | ||
|
||
@Bean | ||
@SuppressWarnings("unused") | ||
Binding binding(@Qualifier(QUEUE) Queue queue, @Qualifier(EXCHANGE) Exchange exchange) { | ||
return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY).noargs(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.