Skip to content

Commit c6f3e63

Browse files
marijarinm.zharinovamfvanek
authored
Add example of logging retries (#200)
* add base classes to perform and log retries to public api * add app config and wiremock test * add larger timeout to TimeControllerTest * add proper wiremock dependencies for both wiremock modules * add proper wiremock dependency to spring boot 3 module * refactor happy path in test * complete tests for spring boot 2 module * add tests for spring boot 3 module * refactor * refactor * refactoring * refactor wiremock test * Configure WireMock * BOM * Simplify tests * Simplify retries --------- Co-authored-by: m.zharinova <[email protected]> Co-authored-by: Ivan Vakhrushev <[email protected]>
1 parent 98791a2 commit c6f3e63

File tree

25 files changed

+499
-5
lines changed

25 files changed

+499
-5
lines changed

buildSrc/src/main/kotlin/sb-ot-demo.java-conventions.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ tasks {
5656
useJUnitPlatform()
5757
dependsOn(pmdMain, pmdTest)
5858
finalizedBy(jacocoTestReport, jacocoTestCoverageVerification)
59+
maxParallelForks = 1
5960
}
6061

6162
jacocoTestCoverageVerification {
@@ -79,7 +80,7 @@ tasks {
7980
limit {
8081
counter = "LINE"
8182
value = "MISSEDCOUNT"
82-
maximum = "8.0".toBigDecimal()
83+
maximum = "10.0".toBigDecimal()
8384
}
8485
}
8586
rule {

config/pmd/pmd.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@
6565
</properties>
6666
</rule>
6767

68+
<rule ref="category/java/codestyle.xml/TooManyStaticImports">
69+
<properties>
70+
<property name="maximumStaticImports" value="8" />
71+
</properties>
72+
</rule>
73+
6874
<rule ref="category/java/design.xml">
6975
<exclude name="LawOfDemeter"/>
7076
<exclude name="LoosePackageCoupling"/>

gradle.properties

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
org.gradle.parallel=true
2+
org.gradle.daemon=true

spring-boot-2-demo-app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ dependencies {
1616
implementation(platform("org.springframework.cloud:spring-cloud-sleuth-otel-dependencies:1.1.4"))
1717

1818
implementation("org.springframework.boot:spring-boot-starter-web")
19+
implementation("org.springframework.boot:spring-boot-starter-webflux")
1920
implementation("org.springframework.boot:spring-boot-starter-actuator")
2021
implementation("io.micrometer:micrometer-registry-prometheus")
2122
implementation("org.springdoc:springdoc-openapi-ui")
@@ -38,6 +39,7 @@ dependencies {
3839
because("https://github.com/jdbc-observations/datasource-proxy/issues/111")
3940
}
4041

42+
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-stub-runner")
4143
testImplementation("org.springframework.boot:spring-boot-starter-test")
4244
testImplementation("org.springframework.boot:spring-boot-starter-webflux")
4345
testImplementation("org.testcontainers:postgresql")
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package io.github.mfvanek.spring.boot2.test.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.web.reactive.function.client.WebClient;
7+
8+
@Configuration
9+
public class WebClientConfig {
10+
11+
@Value("${app.external-base-url}")
12+
private String external;
13+
14+
@Bean
15+
public WebClient webClient() {
16+
return WebClient.builder().baseUrl(external).build();
17+
}
18+
}

spring-boot-2-demo-app/src/main/java/io/github/mfvanek/spring/boot2/test/controllers/TimeController.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.github.mfvanek.spring.boot2.test.controllers;
22

33
import io.github.mfvanek.spring.boot2.test.service.KafkaSendingService;
4+
import io.github.mfvanek.spring.boot2.test.service.PublicApiService;
45
import lombok.RequiredArgsConstructor;
56
import lombok.SneakyThrows;
67
import lombok.extern.slf4j.Slf4j;
@@ -22,6 +23,7 @@ public class TimeController {
2223
private final Tracer tracer;
2324
private final Clock clock;
2425
private final KafkaSendingService kafkaSendingService;
26+
private final PublicApiService publicApiService;
2527

2628
// http://localhost:8090/current-time
2729
@SneakyThrows
@@ -32,7 +34,8 @@ public LocalDateTime getNow() {
3234
.map(TraceContext::traceId)
3335
.orElse(null);
3436
log.info("Called method getNow. TraceId = {}", traceId);
35-
final LocalDateTime now = LocalDateTime.now(clock);
37+
final LocalDateTime nowFromRemote = publicApiService.getZonedTime();
38+
final LocalDateTime now = nowFromRemote == null ? LocalDateTime.now(clock) : nowFromRemote;
3639
kafkaSendingService.sendNotification("Current time = " + now)
3740
.thenRun(() -> log.info("Awaiting acknowledgement from Kafka"))
3841
.get();
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package io.github.mfvanek.spring.boot2.test.service;
2+
3+
4+
import com.fasterxml.jackson.core.JsonProcessingException;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import io.github.mfvanek.spring.boot2.test.service.dto.CurrentTime;
7+
import io.github.mfvanek.spring.boot2.test.service.dto.ParsedDateTime;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.beans.factory.annotation.Value;
11+
import org.springframework.http.MediaType;
12+
import org.springframework.lang.Nullable;
13+
import org.springframework.retry.ExhaustedRetryException;
14+
import org.springframework.stereotype.Service;
15+
import org.springframework.web.reactive.function.client.WebClient;
16+
import reactor.core.publisher.Mono;
17+
import reactor.util.retry.Retry;
18+
19+
import java.time.Duration;
20+
import java.time.LocalDateTime;
21+
import java.util.TimeZone;
22+
23+
@Slf4j
24+
@Service
25+
@RequiredArgsConstructor
26+
public class PublicApiService {
27+
28+
@Value("${app.retries}")
29+
private int retries;
30+
31+
private final ObjectMapper mapper;
32+
private final WebClient webClient;
33+
34+
@Nullable
35+
public LocalDateTime getZonedTime() {
36+
try {
37+
final ParsedDateTime result = getZonedTimeFromWorldTimeApi().getDatetime();
38+
return result.toLocalDateTime();
39+
} catch (ExhaustedRetryException e) {
40+
log.warn("Failed to get response", e);
41+
} catch (JsonProcessingException e) {
42+
log.warn("Failed to convert response", e);
43+
}
44+
return null;
45+
}
46+
47+
private CurrentTime getZonedTimeFromWorldTimeApi() throws JsonProcessingException {
48+
final String zoneNames = TimeZone.getDefault().getID();
49+
final Mono<String> response = webClient.get()
50+
.uri(String.join("/", zoneNames))
51+
.accept(MediaType.APPLICATION_JSON)
52+
.retrieve()
53+
.bodyToMono(String.class)
54+
.retryWhen(Retry.fixedDelay(retries, Duration.ofSeconds(2))
55+
.doBeforeRetry(retrySignal -> log.info("Retrying request to {}, attempt {}/{} due to error:",
56+
webClient.options().uri(String.join("", zoneNames)), retries, retrySignal.totalRetries() + 1, retrySignal.failure()))
57+
.onRetryExhaustedThrow((retryBackoffSpec, retrySignal) -> {
58+
log.error("Request to {} failed after {} attempts.", webClient.options().uri(String.join("", zoneNames)), retrySignal.totalRetries() + 1);
59+
return new ExhaustedRetryException("Retries exhausted", retrySignal.failure());
60+
})
61+
);
62+
return mapper.readValue(response.block(), CurrentTime.class);
63+
}
64+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package io.github.mfvanek.spring.boot2.test.service.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
5+
import lombok.AllArgsConstructor;
6+
import lombok.Builder;
7+
import lombok.Getter;
8+
import lombok.extern.jackson.Jacksonized;
9+
10+
@Jacksonized
11+
@Builder
12+
@AllArgsConstructor
13+
@JsonIgnoreProperties(ignoreUnknown = true)
14+
@Getter
15+
public class CurrentTime {
16+
private final ParsedDateTime datetime;
17+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.github.mfvanek.spring.boot2.test.service.dto;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
import java.time.LocalDateTime;
8+
import javax.annotation.Nonnull;
9+
import javax.annotation.concurrent.Immutable;
10+
11+
@AllArgsConstructor
12+
@JsonIgnoreProperties(ignoreUnknown = true)
13+
@Getter
14+
@Immutable
15+
public class ParsedDateTime {
16+
17+
private final int year;
18+
private final int monthValue;
19+
private final int dayOfMonth;
20+
private final int hour;
21+
private final int minute;
22+
23+
@Nonnull
24+
public LocalDateTime toLocalDateTime() {
25+
return LocalDateTime.of(year, monthValue, dayOfMonth, hour, minute);
26+
}
27+
28+
@Nonnull
29+
public static ParsedDateTime from(final LocalDateTime localDateTime) {
30+
return new ParsedDateTime(
31+
localDateTime.getYear(),
32+
localDateTime.getMonthValue(),
33+
localDateTime.getDayOfMonth(),
34+
localDateTime.getHour(),
35+
localDateTime.getMinute()
36+
);
37+
}
38+
}

spring-boot-2-demo-app/src/main/resources/application.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
app:
2+
external-base-url: "http://worldtimeapi.org/api/timezone/"
3+
retries: 3
4+
15
server:
26
port: 8090
37
# See also https://docs.spring.io/spring-boot/docs/2.7.9/reference/html/application-properties.html#appendix.application-properties.server

0 commit comments

Comments
 (0)