Skip to content

Commit ba1b2f1

Browse files
authored
Merge pull request #139 from bsara/retry
feat: added retry options
2 parents 09a818a + 7b1432c commit ba1b2f1

File tree

8 files changed

+287
-25
lines changed

8 files changed

+287
-25
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,15 @@ should be defined as `graphql.client.url` in your Spring Boot configuration file
7373
| `oauth2.client-secret` | OAuth2 client secret |
7474
| `oauth2.token-uri` | Token URI of the identity provider |
7575
| `oauth2.authorization-grant-type` | By default the grant type `client_credentials` is used |
76-
76+
| `retry.strategy` | The retry strategy to auto configure for the `WebClient` _(possible values are `none`, `backoff`, `fixed_delay`, `indefinitely`, `max` and `max_in_row`)_. Default is `none`. |
77+
| `retry.backoff.max-attempts` | The maximum number of retry attempts to allow _(only used when `retry.strategy` = `backoff`)_. |
78+
| `retry.backoff.min-backoff` | The minimum duration for the first backoff _(only used when `retry.strategy` = `backoff`)_. Default is `0`. |
79+
| `retry.backoff.max-backoff` | The maximum duration for the exponential backoffs _(only used when `retry.strategy` = `backoff`)_. Default is `Duration.ofMillis(Long.MAX_VALUE)`. |
80+
| `retry.fixed-delay.max-attempts` | The maximum number of retry attempts to allow _(only used when `retry.strategy` = `fixed_delay`)_. |
81+
| `retry.fixed-delay.delay` | The duration of the fixed delays between attempts _(only used when `retry.strategy` = `fixed_delay`)_. |
82+
| `retry.max.max-attempts` | The maximum number of retry attempts to allow _(only used when `retry.strategy` = `max`)_. |
83+
| `retry.max-in-row.max-attempts` | The maximum number of retry attempts to allow in a row _(only used when `retry.strategy` = `max_in_row`)_. |
84+
7785
### Max in memory size
7886

7987
In case you need to work with large responses you might run into the following error:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package graphql.kickstart.spring.webclient.boot;
2+
3+
import java.time.Duration;
4+
5+
import org.springframework.boot.context.properties.ConfigurationProperties;
6+
import org.springframework.context.annotation.Primary;
7+
8+
import lombok.Data;
9+
10+
@Data
11+
@Primary
12+
@ConfigurationProperties("graphql.client.retry")
13+
public class GraphQLClientRetryProperties {
14+
15+
private RetryStrategy strategy = RetryStrategy.NONE;
16+
private RetryBackoff backoff = new RetryBackoff();
17+
private RetryFixedDelay fixedDelay = new RetryFixedDelay();
18+
private RetryMax max = new RetryMax();
19+
private RetryMaxInRow maxInRow = new RetryMaxInRow();
20+
21+
@Data
22+
static class RetryBackoff {
23+
private long maxAttempts = -1;
24+
private Duration minBackoff = Duration.ofMillis(0);
25+
private Duration maxBackoff = Duration.ofMillis(Long.MAX_VALUE);
26+
}
27+
28+
@Data
29+
static class RetryFixedDelay {
30+
private long maxAttempts = -1;
31+
private Duration delay = Duration.ofMillis(0);
32+
}
33+
34+
@Data
35+
static class RetryMax {
36+
private long maxAttempts = -1;
37+
}
38+
39+
@Data
40+
static class RetryMaxInRow {
41+
private long maxAttempts = -1;
42+
}
43+
44+
enum RetryStrategy {
45+
BACKOFF,
46+
FIXED_DELAY,
47+
INDEFINITELY,
48+
MAX,
49+
MAX_IN_ROW,
50+
NONE
51+
}
52+
}

graphql-webclient-spring-boot-autoconfigure/src/main/java/graphql/kickstart/spring/webclient/boot/GraphQLWebClientAutoConfiguration.java

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.NonNull;
55
import lombok.RequiredArgsConstructor;
66
import lombok.extern.slf4j.Slf4j;
7+
import reactor.util.retry.Retry;
78
import org.springframework.beans.factory.annotation.Autowired;
89
import org.springframework.boot.autoconfigure.AutoConfiguration;
910
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@@ -28,11 +29,12 @@
2829
OAuth2ClientAutoConfiguration.class,
2930
WebFluxAutoConfiguration.class
3031
})
31-
@EnableConfigurationProperties(GraphQLClientProperties.class)
32+
@EnableConfigurationProperties({ GraphQLClientProperties.class, GraphQLClientRetryProperties.class })
3233
@ComponentScan(basePackageClasses = GraphQLWebClientImpl.class)
3334
public class GraphQLWebClientAutoConfiguration {
3435

3536
private final GraphQLClientProperties graphqlClientProperties;
37+
private final GraphQLClientRetryProperties graphqlClientRetryProperties;
3638

3739
@Bean
3840
@ConditionalOnMissingBean
@@ -76,7 +78,42 @@ public ObjectMapper objectMapper() {
7678

7779
@Bean
7880
@ConditionalOnMissingBean
79-
public GraphQLWebClient graphQLWebClient(WebClient webClient, ObjectMapper objectMapper) {
80-
return new GraphQLWebClientImpl(webClient, objectMapper);
81+
public GraphQLWebClient graphQLWebClient(WebClient webClient, ObjectMapper objectMapper, GraphQLWebClientRetryProvider retryProvider) {
82+
return new GraphQLWebClientImpl(webClient, objectMapper, retryProvider.get());
83+
}
84+
85+
@Bean
86+
@ConditionalOnMissingBean
87+
public GraphQLWebClientRetryProvider graphQLWebClientRetryProvider(GraphQLWebClientRetryErrorFilterPredicate errorFilterPredicate) {
88+
return () -> switch(graphqlClientRetryProperties.getStrategy()) {
89+
case BACKOFF ->
90+
Retry.backoff(graphqlClientRetryProperties.getBackoff().getMaxAttempts(), graphqlClientRetryProperties.getBackoff().getMinBackoff())
91+
.maxBackoff(graphqlClientRetryProperties.getBackoff().getMaxBackoff())
92+
.modifyErrorFilter(p -> p.and(errorFilterPredicate));
93+
94+
case FIXED_DELAY ->
95+
Retry.fixedDelay(graphqlClientRetryProperties.getFixedDelay().getMaxAttempts(), graphqlClientRetryProperties.getFixedDelay().getDelay())
96+
.modifyErrorFilter(p -> p.and(errorFilterPredicate));
97+
98+
case INDEFINITELY ->
99+
Retry.indefinitely()
100+
.modifyErrorFilter(p -> p.and(errorFilterPredicate));
101+
102+
case MAX ->
103+
Retry.max(graphqlClientRetryProperties.getMax().getMaxAttempts())
104+
.modifyErrorFilter(p -> p.and(errorFilterPredicate));
105+
106+
case MAX_IN_ROW ->
107+
Retry.maxInARow(graphqlClientRetryProperties.getMaxInRow().getMaxAttempts())
108+
.modifyErrorFilter(p -> p.and(errorFilterPredicate));
109+
110+
default -> null;
111+
};
112+
}
113+
114+
@Bean
115+
@ConditionalOnMissingBean
116+
public GraphQLWebClientRetryErrorFilterPredicate graphQLWebClientRetryErrorFilterPredicate() {
117+
return t -> true;
81118
}
82119
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package graphql.kickstart.spring.webclient.boot;
2+
3+
import java.util.function.Predicate;
4+
5+
public interface GraphQLWebClientRetryErrorFilterPredicate extends Predicate<Throwable> {
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package graphql.kickstart.spring.webclient.boot;
2+
3+
import reactor.util.retry.Retry;
4+
5+
public interface GraphQLWebClientRetryProvider {
6+
Retry get();
7+
}

graphql-webclient-spring-boot-autoconfigure/src/test/java/graphql/kickstart/spring/webclient/boot/GraphQLWebClientAutoConfigurationTest.java

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
package graphql.kickstart.spring.webclient.boot;
22

3+
import java.time.Duration;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
37
import static org.junit.jupiter.api.Assertions.assertNotNull;
8+
import static org.junit.jupiter.api.Assertions.assertNull;
9+
import static org.junit.jupiter.api.Assertions.assertTrue;
410
import static org.mockito.ArgumentMatchers.isA;
511
import static org.mockito.Mockito.mock;
612
import static org.mockito.Mockito.times;
@@ -16,16 +22,20 @@
1622
import org.springframework.security.oauth2.core.AuthorizationGrantType;
1723
import org.springframework.web.reactive.function.client.WebClient;
1824
import reactor.core.publisher.Mono;
25+
import reactor.util.retry.RetryBackoffSpec;
26+
import reactor.util.retry.RetrySpec;
1927

2028
class GraphQLWebClientAutoConfigurationTest {
2129

2230
private GraphQLWebClientAutoConfiguration configuration;
31+
private GraphQLClientRetryProperties graphQLClientRetryProperties;
2332
private WebClient.Builder mockClientBuilder;
2433

2534
@BeforeEach
2635
void setup() {
2736
GraphQLClientProperties graphQLClientProperties = new GraphQLClientProperties();
28-
configuration = new GraphQLWebClientAutoConfiguration(graphQLClientProperties);
37+
graphQLClientRetryProperties = new GraphQLClientRetryProperties();
38+
configuration = new GraphQLWebClientAutoConfiguration(graphQLClientProperties, graphQLClientRetryProperties);
2939

3040
mockClientBuilder = mock(WebClient.Builder.class);
3141
}
@@ -68,7 +78,126 @@ void webClient_getObjectMapper_returns() {
6878

6979
@Test
7080
void webClient_graphQLWebClient_returns() {
71-
assertNotNull(configuration.graphQLWebClient(WebClient.builder().build(), new ObjectMapper()));
81+
assertNotNull(configuration.graphQLWebClient(WebClient.builder().build(), new ObjectMapper(), () -> null));
82+
}
83+
84+
@Test
85+
void webClient_graphQLWebClientRetryErrorFilterPredicate_returns() {
86+
var predicate = configuration.graphQLWebClientRetryErrorFilterPredicate();
87+
assertNotNull(predicate);
88+
assertTrue(predicate.test(mock()));
89+
}
90+
91+
@Test
92+
void webClient_graphQLWebClientRetryProvider_noneStrategy() {
93+
graphQLClientRetryProperties.setStrategy(GraphQLClientRetryProperties.RetryStrategy.NONE);
94+
var provider = configuration.graphQLWebClientRetryProvider(mock());
95+
assertNotNull(provider);
96+
assertNull(provider.get());
97+
}
98+
99+
@Test
100+
void webClient_graphQLWebClientRetryProvider_backoffStrategy() {
101+
graphQLClientRetryProperties.setStrategy(GraphQLClientRetryProperties.RetryStrategy.BACKOFF);
102+
graphQLClientRetryProperties.getBackoff().setMaxAttempts(42);
103+
graphQLClientRetryProperties.getBackoff().setMinBackoff(Duration.ofMillis(50));
104+
graphQLClientRetryProperties.getBackoff().setMaxBackoff(Duration.ofMinutes(30));
105+
106+
var mockedPredicate = mock(GraphQLWebClientRetryErrorFilterPredicate.class);
107+
when(mockedPredicate.test(isA(Throwable.class))).thenReturn(true);
108+
109+
var provider = configuration.graphQLWebClientRetryProvider(mockedPredicate);
110+
assertNotNull(provider);
111+
assertNotNull(provider.get());
112+
assertInstanceOf(RetryBackoffSpec.class, provider.get());
113+
114+
var castedProviderValue = (RetryBackoffSpec) provider.get();
115+
assertEquals(42, castedProviderValue.maxAttempts);
116+
assertEquals(Duration.ofMillis(50), castedProviderValue.minBackoff);
117+
assertEquals(Duration.ofMinutes(30), castedProviderValue.maxBackoff);
118+
119+
castedProviderValue.errorFilter.test(new Throwable());
120+
verify(mockedPredicate, times(1)).test(isA(Throwable.class));
121+
}
122+
123+
@Test
124+
void webClient_graphQLWebClientRetryProvider_fixedDelayStrategy() {
125+
graphQLClientRetryProperties.setStrategy(GraphQLClientRetryProperties.RetryStrategy.FIXED_DELAY);
126+
graphQLClientRetryProperties.getFixedDelay().setMaxAttempts(42);
127+
graphQLClientRetryProperties.getFixedDelay().setDelay(Duration.ofMillis(50));
128+
129+
var mockedPredicate = mock(GraphQLWebClientRetryErrorFilterPredicate.class);
130+
when(mockedPredicate.test(isA(Throwable.class))).thenReturn(true);
131+
132+
var provider = configuration.graphQLWebClientRetryProvider(mockedPredicate);
133+
assertNotNull(provider);
134+
assertNotNull(provider.get());
135+
assertInstanceOf(RetryBackoffSpec.class, provider.get());
136+
137+
var castedProviderValue = (RetryBackoffSpec) provider.get();
138+
assertEquals(42, castedProviderValue.maxAttempts);
139+
assertEquals(Duration.ofMillis(50), castedProviderValue.minBackoff);
140+
141+
castedProviderValue.errorFilter.test(new Throwable());
142+
verify(mockedPredicate, times(1)).test(isA(Throwable.class));
143+
}
144+
145+
@Test
146+
void webClient_graphQLWebClientRetryProvider_indefinitelyStrategy() {
147+
graphQLClientRetryProperties.setStrategy(GraphQLClientRetryProperties.RetryStrategy.INDEFINITELY);
148+
149+
var mockedPredicate = mock(GraphQLWebClientRetryErrorFilterPredicate.class);
150+
when(mockedPredicate.test(isA(Throwable.class))).thenReturn(true);
151+
152+
var provider = configuration.graphQLWebClientRetryProvider(mockedPredicate);
153+
assertNotNull(provider);
154+
assertNotNull(provider.get());
155+
assertInstanceOf(RetrySpec.class, provider.get());
156+
157+
var castedProviderValue = (RetrySpec) provider.get();
158+
159+
castedProviderValue.errorFilter.test(new Throwable());
160+
verify(mockedPredicate, times(1)).test(isA(Throwable.class));
161+
}
162+
163+
@Test
164+
void webClient_graphQLWebClientRetryProvider_maxStrategy() {
165+
graphQLClientRetryProperties.setStrategy(GraphQLClientRetryProperties.RetryStrategy.MAX);
166+
graphQLClientRetryProperties.getMax().setMaxAttempts(42);
167+
168+
var mockedPredicate = mock(GraphQLWebClientRetryErrorFilterPredicate.class);
169+
when(mockedPredicate.test(isA(Throwable.class))).thenReturn(true);
170+
171+
var provider = configuration.graphQLWebClientRetryProvider(mockedPredicate);
172+
assertNotNull(provider);
173+
assertNotNull(provider.get());
174+
assertInstanceOf(RetrySpec.class, provider.get());
175+
176+
var castedProviderValue = (RetrySpec) provider.get();
177+
assertEquals(42, castedProviderValue.maxAttempts);
178+
179+
castedProviderValue.errorFilter.test(new Throwable());
180+
verify(mockedPredicate, times(1)).test(isA(Throwable.class));
181+
}
182+
183+
@Test
184+
void webClient_graphQLWebClientRetryProvider_maxInRowStrategy() {
185+
graphQLClientRetryProperties.setStrategy(GraphQLClientRetryProperties.RetryStrategy.MAX_IN_ROW);
186+
graphQLClientRetryProperties.getMaxInRow().setMaxAttempts(42);
187+
188+
var mockedPredicate = mock(GraphQLWebClientRetryErrorFilterPredicate.class);
189+
when(mockedPredicate.test(isA(Throwable.class))).thenReturn(true);
190+
191+
var provider = configuration.graphQLWebClientRetryProvider(mockedPredicate);
192+
assertNotNull(provider);
193+
assertNotNull(provider.get());
194+
assertInstanceOf(RetrySpec.class, provider.get());
195+
196+
var castedProviderValue = (RetrySpec) provider.get();
197+
assertEquals(42, castedProviderValue.maxAttempts);
198+
199+
castedProviderValue.errorFilter.test(new Throwable());
200+
verify(mockedPredicate, times(1)).test(isA(Throwable.class));
72201
}
73202

74203
}

graphql-webclient/src/main/java/graphql/kickstart/spring/webclient/boot/GraphQLWebClient.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55
import org.springframework.web.reactive.function.client.WebClient;
66
import reactor.core.publisher.Flux;
77
import reactor.core.publisher.Mono;
8+
import reactor.util.retry.Retry;
89

910
public interface GraphQLWebClient {
1011

1112
static GraphQLWebClient newInstance(WebClient webClient, ObjectMapper objectMapper) {
12-
return new GraphQLWebClientImpl(webClient, objectMapper);
13+
return GraphQLWebClient.newInstance(webClient, objectMapper, null);
14+
}
15+
16+
static GraphQLWebClient newInstance(WebClient webClient, ObjectMapper objectMapper, Retry retry) {
17+
return new GraphQLWebClientImpl(webClient, objectMapper, retry);
1318
}
1419

1520
<T> Mono<T> post(String resource, Class<T> returnType);

0 commit comments

Comments
 (0)