Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ This is meant to be used with Spring Boot. In order to get this running just add
|-----------------|---------------------|
| 2.x.x | 2.7+ |
| 3.x.x | 3.1+ |

| 4.x.x | 4.0+ |

#### Configuration

Expand Down Expand Up @@ -114,25 +114,27 @@ options.

#### Mandatory Logging of Command Execution (since version 3.1)

Commands have the potential to alter the state of the system (in contrast to queries, which should not). This is why
it makes sense to log attempted command executions (regardless of their outcome, may it be success or any kind of
Commands have the potential to alter the state of the system (in contrast to queries, which should not). This is why
it makes sense to log attempted command executions (regardless of their outcome, may it be success or any kind of
failure). The aspect will take care of that automatically.

When logging command executions, an extra attribute is added to the Log-Event with the name of 'cqs.command' and the value
is the so-called LogString of the command. By default this is generated reflectively as a best-effort similar to a
When logging command executions, an extra attribute is added to the Log-Event with the name of 'cqs.command' and the
value
is the so-called LogString of the command. By default this is generated reflectively as a best-effort similar to a
toString implementation. As it makes sense to exclude certain fields from logging (for GDPR reasons for example)
you can annotate fields of your Commands using '@LogExclude' in order to skip those when rendering a command for logging.
you can annotate fields of your Commands using '@LogExclude' in order to skip those when rendering a command for
logging.

Also, if your using returning Command handlers, the result will be added as 'cqs.result' with the same rules as
Also, if your using returning Command handlers, the result will be added as 'cqs.result' with the same rules as
for the command.

Failures will go to WARN loglevel while success go to INFO.

#### Tips for use

* Create one handler per use-case.
* Do not call other handlers inside of yours. If that leads to code duplication, consider refactoring common code into a service that will be used by the two handlers.

* Do not call other handlers inside of yours. If that leads to code duplication, consider refactoring common code into a
service that will be used by the two handlers.

## Migration

Expand Down
18 changes: 7 additions & 11 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<version>0.2.19</version>
</parent>
<artifactId>spring-cqs</artifactId>
<version>3.1.2-SNAPSHOT</version>
<version>4.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>CQS spring lib</name>
<description>TODO</description>
Expand Down Expand Up @@ -48,9 +48,9 @@
<maven.version>3.3.9</maven.version>
<maven-gpg-plugin.version>1.6</maven-gpg-plugin.version>
<nexus-staging-maven-plugin.version>1.6.8</nexus-staging-maven-plugin.version>
<spring-boot.version>3.5.13</spring-boot.version>
<spring-boot.version>4.0.5</spring-boot.version>
<spring.version>6.2.1</spring.version>
<spring-retry.version>2.0.11</spring-retry.version>
<junit.version>6.0.1</junit.version>
</properties>
<dependencyManagement>
<dependencies>
Expand All @@ -67,19 +67,19 @@
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.14.3</version>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.14.3</version>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-engine</artifactId>
<version>1.14.3</version>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
Expand All @@ -91,7 +91,7 @@
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-commons</artifactId>
<version>1.14.3</version>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -121,10 +121,6 @@
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
Expand Down
46 changes: 28 additions & 18 deletions src/main/java/eu/prismacapacity/spring/cqs/retry/RetryUtils.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2022 PRISMA European Capacity Platform GmbH
* Copyright © 2022-2026 PRISMA European Capacity Platform GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -15,16 +15,17 @@
*/
package eu.prismacapacity.spring.cqs.retry;

import java.util.Arrays;
import java.time.Duration;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.springframework.retry.RetryCallback;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.retry.support.RetryTemplateBuilder;
import org.springframework.core.retry.RetryException;
import org.springframework.core.retry.RetryPolicy;
import org.springframework.core.retry.RetryTemplate;

@UtilityClass
public class RetryUtils {
Expand All @@ -39,10 +40,21 @@ public <R> R withOptionalRetry(Class<?> handler, Function<Integer, R> fn) {
final Optional<RetryTemplate> template = from(handler);

if (template.isPresent()) {
return template
.get()
.execute(
(RetryCallback<R, Throwable>) retryContext -> fn.apply(retryContext.getRetryCount()));
final AtomicInteger counter = new AtomicInteger(0);
try {
return template
.get()
.execute(
() -> {
try {
return fn.apply(counter.get());
} finally {
counter.incrementAndGet();
}
});
} catch (RetryException e) {
throw e.getCause();
}
} else {
return fn.apply(0);
}
Expand All @@ -52,27 +64,25 @@ Optional<RetryTemplate> getRetryTemplate(Class<?> clazz) {
return Optional.ofNullable(clazz.getAnnotation(RetryConfiguration.class))
.map(
config -> {
final RetryTemplateBuilder tplBuilder = new RetryTemplateBuilder();
final RetryPolicy.Builder policyBuilder = RetryPolicy.builder();

tplBuilder.maxAttempts(config.maxAttempts());
policyBuilder.maxRetries(config.maxAttempts());

final long interval = config.interval();
final long maxInterval = config.exponentialBackoffMaxInterval();

policyBuilder.delay(Duration.ofMillis(interval));
if (maxInterval != 0) {
tplBuilder.exponentialBackoff(interval, 1.2, maxInterval);
} else {
tplBuilder.fixedBackoff(interval);
policyBuilder.multiplier(1.2);
policyBuilder.maxDelay(Duration.ofMillis(maxInterval));
}

final Class<? extends Throwable>[] notRetryOn = config.notRetryOn();

if (notRetryOn != null && notRetryOn.length > 0) {
Arrays.stream(notRetryOn).forEach(tplBuilder::notRetryOn);
tplBuilder.traversingCauses();
policyBuilder.excludes(notRetryOn);
}

return tplBuilder.build();
return new RetryTemplate(policyBuilder.build());
});
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2022-2024 PRISMA European Capacity Platform GmbH
* Copyright © 2022-2026 PRISMA European Capacity Platform GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -287,10 +287,10 @@ void withRetry() throws Throwable {

doThrow(new IllegalStateException("foo")).when(uut).process(joinPoint);

Assertions.assertThrows(RuntimeException.class, () -> uut.orchestrate(joinPoint));
Assertions.assertThrows(IllegalStateException.class, () -> uut.orchestrate(joinPoint));

verify(uut, times(3)).process(joinPoint);
verify(metrics, times(3)).timedCommand(any(), anyInt(), any());
verify(uut, times(4)).process(joinPoint);
verify(metrics, times(4)).timedCommand(any(), anyInt(), any());
}

class SimpleCommandHandler implements CommandHandler<SimpleCommand> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2022-2023 PRISMA European Capacity Platform GmbH
* Copyright © 2022-2026 PRISMA European Capacity Platform GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -297,8 +297,8 @@ void withRetry() throws Throwable {

Assertions.assertThrows(RuntimeException.class, () -> uut.orchestrate(joinPoint));

verify(uut, times(3)).process(joinPoint);
verify(metrics, times(3)).timedQuery(any(), anyInt(), any());
verify(uut, times(4)).process(joinPoint);
verify(metrics, times(4)).timedQuery(any(), anyInt(), any());
}

@RetryConfiguration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2022 PRISMA European Capacity Platform GmbH
* Copyright © 2022-2026 PRISMA European Capacity Platform GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -23,20 +23,16 @@
import eu.prismacapacity.spring.cqs.query.QueryHandlingException;
import eu.prismacapacity.spring.cqs.query.QueryValidationException;
import java.util.function.Function;
import nl.altindag.log.LogCaptor;
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.springframework.retry.backoff.ExponentialBackOffPolicy;

@ExtendWith(MockitoExtension.class)
class RetryUtilsTest {
@Mock private Function<Integer, String> fn;
@Mock private Function<Integer, String> fn2;

private LogCaptor logCaptor = LogCaptor.forClass(ExponentialBackOffPolicy.class);

@Test
void test_happyCase() {
when(fn.apply(any())).thenReturn("foo");
Expand Down Expand Up @@ -98,7 +94,7 @@ void test_customConfig() {
() -> RetryUtils.withOptionalRetry(RetryWithCustomConfig.class, fn2));

verify(fn).apply(0);
verify(fn2, times(2)).apply(any());
verify(fn2, times(3)).apply(any());
}

@Test
Expand All @@ -109,19 +105,7 @@ void test_backoff() {
IllegalStateException.class,
() -> RetryUtils.withOptionalRetry(RetryWithBackoff.class, fn));

verify(fn, times(5)).apply(any());

assertEquals(4, logCaptor.getDebugLogs().size());
assertTrue(
logCaptor
.getDebugLogs()
.contains("Sleeping for 20")); // first retry with default interval of 20
assertTrue(logCaptor.getDebugLogs().contains("Sleeping for 24")); // interval*1.2
assertEquals(
2L,
logCaptor.getDebugLogs().stream()
.filter(x -> x.equals("Sleeping for 25"))
.count()); // maxInterval
verify(fn, times(6)).apply(any());
}

static class NoRetries {}
Expand Down
Loading