Skip to content

Commit c18c2ee

Browse files
Correct retry handling for repeatable requests
Previously, the Retryer instance provided to the Builder was simply reused, but the instance keeps per-request state, causing repeatable requests to stop being retried. Fixes OpenFeign#236
1 parent 0bc994e commit c18c2ee

5 files changed

Lines changed: 68 additions & 3 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
### Version 8.4
2+
* Correct Retryer bug that prevented it from retrying requests after the first 5 retry attempts.
3+
* **Note:** If you have a custom `feign.Retryer` implementation you now must now implement `public Retryer clone()`.
4+
It is suggested that you simply return a new instance of your Retryer class.
5+
16
### Version 8.3
27
* Adds client implementation for Apache Http Client
38

‎core/src/main/java/feign/Retryer.java‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,26 @@
1818
import static java.util.concurrent.TimeUnit.SECONDS;
1919

2020
/**
21-
* Created for each invocation to {@link Client#execute(Request, feign.Request.Options)}.
21+
* Cloned for each invocation to {@link Client#execute(Request, feign.Request.Options)}.
2222
* Implementations may keep state to determine if retry operations should continue or not.
2323
*/
24-
public interface Retryer {
24+
public interface Retryer extends Cloneable {
2525

2626
/**
2727
* if retry is permitted, return (possibly after sleeping). Otherwise propagate the exception.
2828
*/
2929
void continueOrPropagate(RetryableException e);
3030

31+
Retryer clone();
32+
3133
public static class Default implements Retryer {
3234

3335
private final int maxAttempts;
3436
private final long period;
3537
private final long maxPeriod;
3638
int attempt;
3739
long sleptForMillis;
40+
3841
public Default() {
3942
this(100, SECONDS.toMillis(1), 5);
4043
}
@@ -87,5 +90,10 @@ long nextMaxInterval() {
8790
long interval = (long) (period * Math.pow(1.5, attempt - 1));
8891
return interval > maxPeriod ? maxPeriod : interval;
8992
}
93+
94+
@Override
95+
public Retryer clone() {
96+
return new Default(period, maxPeriod, maxAttempts);
97+
}
9098
}
9199
}

‎core/src/main/java/feign/SynchronousMethodHandler.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ private SynchronousMethodHandler(Target<?> target, Client client, Retryer retrye
6565
@Override
6666
public Object invoke(Object[] argv) throws Throwable {
6767
RequestTemplate template = buildTemplateFromArgs.create(argv);
68-
Retryer retryer = this.retryer;
68+
Retryer retryer = this.retryer.clone();
6969
while (true) {
7070
try {
7171
return executeAndDecode(template);

‎core/src/test/java/feign/FeignTest.java‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,50 @@ public Object decode(Response response, Type type) throws IOException {
301301
api.post();
302302
}
303303

304+
@Test
305+
public void ensureRetryerClonesItself() {
306+
server.enqueue(new MockResponse().setResponseCode(503).setBody("foo 1"));
307+
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo 2"));
308+
server.enqueue(new MockResponse().setResponseCode(503).setBody("foo 3"));
309+
server.enqueue(new MockResponse().setResponseCode(200).setBody("foo 4"));
310+
311+
MockRetryer retryer = new MockRetryer();
312+
313+
TestInterface api = Feign.builder()
314+
.retryer(retryer)
315+
.errorDecoder(new ErrorDecoder()
316+
{
317+
@Override
318+
public Exception decode(String methodKey, Response response)
319+
{
320+
return new RetryableException("play it again sam!", null);
321+
}
322+
}).target(TestInterface.class, "http://localhost:" + server.getPort());
323+
324+
api.response();
325+
api.response(); // if retryer instance was reused, this statement will throw an exception
326+
assertEquals(4, server.getRequestCount());
327+
}
328+
329+
private static class MockRetryer implements Retryer
330+
{
331+
boolean tripped;
332+
333+
@Override
334+
public void continueOrPropagate(RetryableException e) {
335+
if (tripped) {
336+
throw new RuntimeException("retryer instance should never be reused");
337+
}
338+
tripped = true;
339+
return;
340+
}
341+
342+
@Override
343+
public Retryer clone() {
344+
return new MockRetryer();
345+
}
346+
}
347+
304348
@Test
305349
public void okIfDecodeRootCauseHasNoMessage() throws Exception {
306350
server.enqueue(new MockResponse().setBody("success!"));

‎core/src/test/java/feign/LoggerTest.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,9 @@ public void unknownHostEmits() throws IOException, InterruptedException {
214214
public void continueOrPropagate(RetryableException e) {
215215
throw e;
216216
}
217+
@Override public Retryer clone() {
218+
return this;
219+
}
217220
})
218221
.target(SendsStuff.class, "http://robofu.abc");
219222

@@ -264,6 +267,11 @@ public void continueOrPropagate(RetryableException e) {
264267
}
265268
throw e;
266269
}
270+
271+
@Override
272+
public Retryer clone() {
273+
return this;
274+
}
267275
})
268276
.target(SendsStuff.class, "http://robofu.abc");
269277

0 commit comments

Comments
 (0)