Skip to content

Commit ecfdba6

Browse files
committed
Refactor backoff logic into separate class
Calculating the backoff delay has been moved into a new BackoffCounter class. Sleeping the current thread was also abstracted into a Time class so that unit tests don't actually have to wait for a delay.
1 parent 6620b9a commit ecfdba6

File tree

5 files changed

+122
-48
lines changed

5 files changed

+122
-48
lines changed

build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ dependencies {
1212
testCompile 'junit:junit:4.11'
1313
testCompile 'org.hamcrest:hamcrest-library:1.3'
1414
testCompile 'com.github.tomakehurst:wiremock:1.47'
15+
testCompile 'org.mockito:mockito-core:1.9.5'
1516
}
1617

1718
tasks.withType(JavaCompile) {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.box.sdk;
2+
3+
import java.util.logging.Level;
4+
import java.util.logging.Logger;
5+
6+
class BackoffCounter {
7+
private static final Logger LOGGER = Logger.getLogger(BackoffCounter.class.getName());
8+
private static final int MIN_EXPONENT = 10;
9+
private static final int MAX_EXPONENT = 16;
10+
11+
private final Time time;
12+
13+
private int maxAttempts;
14+
private int attemptsRemaining;
15+
16+
public BackoffCounter() {
17+
this.time = Time.getInstance();
18+
}
19+
20+
public BackoffCounter(Time time) {
21+
this.time = time;
22+
}
23+
24+
public int getAttemptsRemaining() {
25+
return this.attemptsRemaining;
26+
}
27+
28+
public void waitBackoff() throws InterruptedException {
29+
int delay = this.calculateDelay();
30+
if (this.attemptsRemaining > 1) {
31+
LOGGER.log(Level.WARNING, String.format("Backing off for %d seconds before retrying %d more times.",
32+
(delay / 1000), this.attemptsRemaining));
33+
} else {
34+
LOGGER.log(Level.WARNING, String.format("Backing off for %d seconds before retrying %d more time.",
35+
(delay / 1000), this.attemptsRemaining));
36+
}
37+
38+
this.time.waitDuration(delay);
39+
}
40+
41+
public boolean decrement() {
42+
this.attemptsRemaining--;
43+
return (this.attemptsRemaining > 0);
44+
}
45+
46+
public void reset(int maxAttempts) {
47+
this.maxAttempts = maxAttempts;
48+
this.attemptsRemaining = maxAttempts;
49+
}
50+
51+
private int calculateDelay() {
52+
int exponent = (MIN_EXPONENT + (this.maxAttempts - (this.attemptsRemaining + 1)));
53+
if (exponent > MAX_EXPONENT) {
54+
exponent = MAX_EXPONENT;
55+
}
56+
57+
return (2 << exponent);
58+
}
59+
}

src/main/java/com/box/sdk/BoxAPIRequest.java

Lines changed: 34 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,14 @@
1515
import java.util.logging.Logger;
1616

1717
public class BoxAPIRequest {
18-
private static final Logger LOGGER = Logger.getLogger(BoxFolder.class.getName());
19-
private static final int MAX_BACKOFF_MILLISECONDS = 60000;
20-
private static final int INITIAL_BACKOFF_EXPONENT = 10;
18+
private static final Logger LOGGER = Logger.getLogger(BoxAPIRequest.class.getName());
2119

2220
private final BoxAPIConnection api;
2321
private final URL url;
2422
private final List<RequestHeader> headers;
2523
private final String method;
2624

25+
private BackoffCounter backoffCounter;
2726
private int timeout;
2827
private InputStream body;
2928
private long bodyLength;
@@ -38,6 +37,7 @@ public BoxAPIRequest(BoxAPIConnection api, URL url, String method) {
3837
this.url = url;
3938
this.method = method;
4039
this.headers = new ArrayList<RequestHeader>();
40+
this.backoffCounter = new BackoffCounter(new Time());
4141

4242
this.addHeader("Accept-Encoding", "gzip");
4343
this.addHeader("Accept-Charset", "utf-8");
@@ -66,10 +66,35 @@ public void setBody(String body) {
6666

6767
public BoxAPIResponse send() {
6868
if (this.api == null) {
69-
return this.send(BoxAPIConnection.DEFAULT_MAX_ATTEMPTS, INITIAL_BACKOFF_EXPONENT);
69+
this.backoffCounter.reset(BoxAPIConnection.DEFAULT_MAX_ATTEMPTS);
7070
} else {
71-
return this.send(this.api.getMaxAttempts(), INITIAL_BACKOFF_EXPONENT);
71+
this.backoffCounter.reset(this.api.getMaxAttempts());
7272
}
73+
74+
while (this.backoffCounter.getAttemptsRemaining() > 0) {
75+
try {
76+
return this.trySend();
77+
} catch (BoxAPIException apiException) {
78+
if (!this.backoffCounter.decrement() || !isResponseRetryable(apiException.getResponseCode())) {
79+
throw apiException;
80+
}
81+
82+
try {
83+
this.resetBody();
84+
} catch (IOException ioException) {
85+
throw apiException;
86+
}
87+
88+
try {
89+
this.backoffCounter.waitBackoff();
90+
} catch (InterruptedException interruptedException) {
91+
Thread.currentThread().interrupt();
92+
throw apiException;
93+
}
94+
}
95+
}
96+
97+
throw new RuntimeException();
7398
}
7499

75100
@Override
@@ -101,6 +126,10 @@ public String toString() {
101126
return builder.toString();
102127
}
103128

129+
void setBackoffCounter(BackoffCounter counter) {
130+
this.backoffCounter = counter;
131+
}
132+
104133
protected String bodyToString() {
105134
return null;
106135
}
@@ -130,46 +159,6 @@ protected void resetBody() throws IOException {
130159
}
131160
}
132161

133-
private BoxAPIResponse send(int maxAttempts, int backoffExp) {
134-
BoxAPIResponse response = null;
135-
try {
136-
response = this.trySend();
137-
} catch (BoxAPIException apiException) {
138-
int remainingAttempts = (maxAttempts - 1);
139-
if (remainingAttempts <= 0 || !isResponseRetryable(apiException.getResponseCode())) {
140-
throw apiException;
141-
}
142-
143-
int backoffMilliseconds = 2 << backoffExp;
144-
int nextBackoffExp;
145-
if (MAX_BACKOFF_MILLISECONDS < backoffMilliseconds) {
146-
backoffMilliseconds = MAX_BACKOFF_MILLISECONDS;
147-
nextBackoffExp = backoffExp;
148-
} else {
149-
nextBackoffExp = backoffExp + 1;
150-
}
151-
152-
try {
153-
this.resetBody();
154-
} catch (IOException ioException) {
155-
throw apiException;
156-
}
157-
158-
LOGGER.log(Level.WARNING, String.format("Waiting %d seconds before retrying %d more time(s).",
159-
(backoffMilliseconds / 1000), remainingAttempts));
160-
try {
161-
Thread.sleep(backoffMilliseconds);
162-
} catch (InterruptedException interruptedException) {
163-
Thread.currentThread().interrupt();
164-
throw apiException;
165-
}
166-
167-
this.send(remainingAttempts, nextBackoffExp);
168-
}
169-
170-
return response;
171-
}
172-
173162
private BoxAPIResponse trySend() {
174163
HttpURLConnection connection = this.createConnection();
175164
if (this.bodyLength > 0) {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package com.box.sdk;
2+
3+
class Time {
4+
private static final ThreadLocal<Time> THREAD_LOCAL_INSTANCE = new ThreadLocal<Time>() {
5+
@Override
6+
protected Time initialValue() {
7+
return new Time();
8+
}
9+
};
10+
11+
static Time getInstance() {
12+
return THREAD_LOCAL_INSTANCE.get();
13+
}
14+
15+
void waitDuration(int milliseconds) throws InterruptedException {
16+
Thread.sleep(milliseconds);
17+
}
18+
}

src/test/java/com/box/sdk/BoxAPIRequestTest.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import static org.hamcrest.Matchers.*;
77
import static org.junit.Assert.*;
8+
import static org.mockito.Mockito.*;
89

910
import org.junit.Rule;
1011
import org.junit.Test;
@@ -18,15 +19,21 @@ public class BoxAPIRequestTest {
1819
@Rule
1920
public WireMockRule wireMockRule = new WireMockRule(8080);
2021

21-
@Test(expected = BoxAPIException.class)
22+
@Test
2223
@Category(UnitTest.class)
2324
public void requestRetriesTheDefaultNumberOfTimesWhenServerReturns500() throws MalformedURLException {
2425
stubFor(get(urlEqualTo("/")).willReturn(aResponse().withStatus(500)));
26+
Time mockTime = mock(Time.class);
27+
BackoffCounter backoffCounter = new BackoffCounter(mockTime);
2528

2629
URL url = new URL("http://localhost:8080/");
2730
BoxAPIRequest request = new BoxAPIRequest(url, "GET");
28-
request.send();
31+
request.setBackoffCounter(backoffCounter);
2932

30-
verify(BoxAPIConnection.DEFAULT_MAX_ATTEMPTS, getRequestedFor(urlEqualTo("/")));
33+
try {
34+
request.send();
35+
} catch (BoxAPIException e) {
36+
verify(BoxAPIConnection.DEFAULT_MAX_ATTEMPTS, getRequestedFor(urlEqualTo("/")));
37+
}
3138
}
3239
}

0 commit comments

Comments
 (0)