Skip to content

Commit 3b85f55

Browse files
authored
PLAT-5219: Better retries with concurrent requests (#52)
* Better retries with concurrent requests Previously, if two or more threads completed with 401/403 errors concurrently before the `updateAccessToken` happened, only one of them would be retried. Now, they'll all be retried. That multiple requests have to be retried due to the same expired token is an unavoidable side-effect of the fact that we allow multiple concurrent requests (and that we only know that the token is expired as a result from the request itself), and we can live with that tradeoff. [PLAT-5219](https://vertexvis.atlassian.net/browse/PLAT-5219) * version update * and another one, i guess
1 parent e97ecff commit 3b85f55

File tree

6 files changed

+105
-29
lines changed

6 files changed

+105
-29
lines changed

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,21 @@ The client can be used with Java 1.8+ and pulled into Maven or Gradle projects.
1717
<dependency>
1818
<groupId>com.vertexvis</groupId>
1919
<artifactId>api-client-java</artifactId>
20-
<version>0.8.2</version>
20+
<version>0.8.3</version>
2121
<scope>compile</scope>
2222
</dependency>
2323
```
2424

2525
### Gradle
2626

2727
```groovy
28-
compile "com.vertexvis:api-client-java:0.8.2"
28+
compile "com.vertexvis:api-client-java:0.8.3"
2929
```
3030

3131
### Sbt
3232

3333
```sbt
34-
libraryDependencies += "com.vertexvis" % "api-client-java" % "0.8.2"
34+
libraryDependencies += "com.vertexvis" % "api-client-java" % "0.8.3"
3535
```
3636

3737
### Others
@@ -44,7 +44,7 @@ mvn clean package
4444

4545
Then manually install the following JARs.
4646

47-
- `target/api-client-java-0.8.2.jar`
47+
- `target/api-client-java-0.8.3.jar`
4848
- `target/lib/*.jar`
4949

5050
## Usage

build.gradle

+13-12
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ plugins {
77
}
88

99
group = 'com.vertexvis'
10-
version = '0.8.2'
10+
version = '0.8.3'
1111

1212
repositories {
1313
mavenCentral()
@@ -76,19 +76,20 @@ javadoc {
7676
}
7777

7878
dependencies {
79-
implementation 'io.swagger:swagger-annotations:1.6.3'
79+
implementation 'io.swagger:swagger-annotations:1.6.14'
8080
implementation "com.google.code.findbugs:jsr305:3.0.2"
81-
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
82-
implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3'
83-
implementation 'com.google.code.gson:gson:2.8.9'
84-
implementation 'io.gsonfire:gson-fire:1.8.5'
81+
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
82+
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
83+
implementation 'com.google.code.gson:gson:2.10'
84+
implementation 'io.gsonfire:gson-fire:1.9.0'
8585
implementation 'org.apache.oltu.oauth2:org.apache.oltu.oauth2.client:1.0.1' // 1.0.2 doesn't work with okkhttp3
86-
implementation 'org.apache.commons:commons-lang3:3.12.0'
87-
implementation 'javax.annotation:javax.annotation-api:1.3.2'
88-
implementation 'org.openapitools:jackson-databind-nullable:0.2.2'
89-
implementation 'info.picocli:picocli:4.7.0'
90-
testImplementation(platform('org.junit:junit-bom:5.8.2'))
91-
testImplementation('org.junit.jupiter:junit-jupiter:5.8.2')
86+
implementation 'org.apache.commons:commons-lang3:3.15.0'
87+
implementation 'javax.annotation:javax.annotation-api:1.3'
88+
implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
89+
implementation 'info.picocli:picocli:4.7.6'
90+
testImplementation(platform('org.junit:junit-bom:5.10.3'))
91+
testImplementation('org.junit.jupiter:junit-jupiter:5.10.3')
92+
testImplementation("com.squareup.okhttp3:mockwebserver:4.12.0")
9293
}
9394

9495
test {

src/main/java/com/vertexvis/ApiClient.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ private void init() {
207207
json = new JSON();
208208

209209
// Set default User-Agent.
210-
setUserAgent("vertex-api-client-java/0.8.1");
210+
setUserAgent("vertex-api-client-java/0.8.3");
211211

212212
authentications = new HashMap<String, Authentication>();
213213
}

src/main/java/com/vertexvis/auth/OAuthOkHttpClient.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import java.util.Map.Entry;
1919

2020
public class OAuthOkHttpClient implements HttpClient {
21-
private OkHttpClient client;
21+
private final OkHttpClient client;
2222

2323
public OAuthOkHttpClient() {
2424
this.client = new OkHttpClient();

src/main/java/com/vertexvis/auth/RetryingOAuth.java

+7-11
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,9 @@ private Response retryingIntercept(Chain chain, boolean updateTokenAndRetryOnAut
104104
updateTokenAndRetryOnAuthorizationFailure
105105
) {
106106
try {
107-
if (updateAccessToken(requestAccessToken)) {
108-
response.body().close();
109-
return retryingIntercept(chain, false);
110-
}
107+
updateAccessToken(requestAccessToken);
108+
response.body().close();
109+
return retryingIntercept(chain, false);
111110
} catch (Exception e) {
112111
response.body().close();
113112
throw e;
@@ -119,24 +118,21 @@ private Response retryingIntercept(Chain chain, boolean updateTokenAndRetryOnAut
119118
}
120119
}
121120

122-
/*
123-
* Returns true if the access token has been updated
124-
*/
125-
public synchronized boolean updateAccessToken(String requestAccessToken) throws IOException {
121+
private synchronized void updateAccessToken(String requestAccessToken) throws IOException {
122+
// if we don't have a token at all, we go get one
123+
// if we are here and requestAccessToken is not equal to getAccessToken(), that means
124+
// some other thread has already updated the token and we can just continue on to retry with the new token.
126125
if (getAccessToken() == null || getAccessToken().equals(requestAccessToken)) {
127126
try {
128127
OAuthJSONAccessTokenResponse accessTokenResponse =
129128
oAuthClient.accessToken(tokenRequestBuilder.buildBodyMessage());
130129
if (accessTokenResponse != null && accessTokenResponse.getAccessToken() != null) {
131130
setAccessToken(accessTokenResponse.getAccessToken());
132-
return !getAccessToken().equals(requestAccessToken);
133131
}
134132
} catch (OAuthSystemException | OAuthProblemException e) {
135133
throw new IOException(e);
136134
}
137135
}
138-
139-
return false;
140136
}
141137

142138
public TokenRequestBuilder getTokenRequestBuilder() {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.vertexvis.auth;
2+
3+
import com.vertexvis.ApiClient;
4+
import com.vertexvis.ApiException;
5+
import com.vertexvis.api.PartRevisionsApi;
6+
import okhttp3.Call;
7+
import okhttp3.Request;
8+
import okhttp3.mockwebserver.Dispatcher;
9+
import okhttp3.mockwebserver.MockResponse;
10+
import okhttp3.mockwebserver.MockWebServer;
11+
import okhttp3.mockwebserver.RecordedRequest;
12+
import org.jetbrains.annotations.NotNull;
13+
import org.junit.jupiter.api.Assertions;
14+
import org.junit.jupiter.api.Test;
15+
16+
import java.io.IOException;
17+
import java.net.HttpURLConnection;
18+
import java.util.HashMap;
19+
import java.util.Objects;
20+
import java.util.UUID;
21+
import java.util.concurrent.atomic.AtomicInteger;
22+
23+
public class RetryingOAuthTest {
24+
final static int numThreads = 10;
25+
private static void startServer(MockWebServer server) throws IOException {
26+
server.setDispatcher(new Dispatcher()
27+
{
28+
static final AtomicInteger numCalls = new AtomicInteger();
29+
30+
@NotNull
31+
@Override
32+
public MockResponse dispatch(@NotNull RecordedRequest recordedRequest) throws InterruptedException {
33+
Thread.sleep(200);
34+
var token = numCalls.incrementAndGet() > (numThreads / 2) ? "1" : "0";
35+
if (Objects.equals(recordedRequest.getPath(), "/oauth2/token")) {
36+
return new MockResponse().addHeader("Content-Type", "application/json").setBody("{\"access_token\": \"" + token + "\"}");
37+
}
38+
if (!recordedRequest.getHeaders().get("Authorization").endsWith(token)) {
39+
return new MockResponse().setResponseCode(HttpURLConnection.HTTP_UNAUTHORIZED).setBody("{\"error\": \"invalid_token\"}");
40+
}
41+
return new MockResponse().setBody("{\"data\": {\"id\": \"" + UUID.randomUUID().toString() + "\"}}");
42+
}
43+
});
44+
server.start();
45+
}
46+
47+
@Test
48+
public void multithreading() throws ApiException, IOException {
49+
final AtomicInteger numFails = new AtomicInteger();
50+
try (var server = new MockWebServer()) {
51+
startServer(server);
52+
53+
final String baseUrl = server.url("/api").toString();
54+
var client = new ApiClient(baseUrl, "clientid", "clientsecret", new HashMap<>());
55+
var prs = new PartRevisionsApi(client);
56+
Thread[] threads = new Thread[numThreads];
57+
for (var i = 0; i < numThreads; i++) {
58+
threads[i] = new Thread(() -> {
59+
try {
60+
prs.deletePartRevision(UUID.randomUUID());
61+
} catch (Exception e) {
62+
numFails.incrementAndGet();
63+
}
64+
});
65+
threads[i].start();
66+
}
67+
68+
for (var i = 0; i < numThreads; i++) {
69+
threads[i].join();
70+
}
71+
72+
Assertions.assertEquals(0, numFails.get());
73+
}
74+
catch (InterruptedException e) {
75+
throw new RuntimeException(e);
76+
}
77+
78+
}
79+
}

0 commit comments

Comments
 (0)