Skip to content

Commit 21d2e9a

Browse files
authored
FFM-6442 - Java SDK - java.lang.IllegalStateException logged by okhttp (#134)
* FFM-6442 - Java SDK - java.lang.IllegalStateException logged by okhttp What - Fixed RetryInterceptor to close response & remove old fix - Fixed 403 re-authentication logic to correctly restart auth service - Added new tests for returning and asserting 4xx codes - ConnectionException to now stores HTTP code and reason for better logging + removed some excessive logging Why java.lang.IllegalStateException "cannot make a new request because the previous response is still open" is seen when the endpoint returns a 4xx error. Due to connection not being closed correctly in retry interceptor. Testing New unit tests added in CfClientTest for testing 4xx reponses + manual testing
1 parent 1d97142 commit 21d2e9a

File tree

17 files changed

+406
-120
lines changed

17 files changed

+406
-120
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ The first step is to install the FF SDK as a dependency in your application usin
6969

7070
Refer to the [Harness Feature Flag Java Server SDK](https://mvnrepository.com/artifact/io.harness/ff-java-server-sdk) to identify the latest version for your build automation tool.
7171

72-
This section lists dependencies for Maven and Gradle and uses the 1.1.9 version as an example:
72+
This section lists dependencies for Maven and Gradle and uses the 1.1.10 version as an example:
7373

7474
#### Maven
7575

@@ -78,14 +78,14 @@ Add the following Maven dependency in your project's pom.xml file:
7878
<dependency>
7979
<groupId>io.harness</groupId>
8080
<artifactId>ff-java-server-sdk</artifactId>
81-
<version>1.1.9</version>
81+
<version>1.1.10</version>
8282
</dependency>
8383
```
8484

8585
#### Gradle
8686

8787
```
88-
implementation group: 'io.harness', name: 'ff-java-server-sdk', version: '1.1.9'
88+
implementation group: 'io.harness', name: 'ff-java-server-sdk', version: '1.1.10'
8989
```
9090

9191
### Code Sample

examples/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>io.harness.featureflags</groupId>
88
<artifactId>examples</artifactId>
9-
<version>1.1.10-SNAPSHOT</version>
9+
<version>1.1.10</version>
1010

1111
<properties>
1212
<maven.compiler.source>8</maven.compiler.source>
@@ -33,7 +33,7 @@
3333
<dependency>
3434
<groupId>io.harness</groupId>
3535
<artifactId>ff-java-server-sdk</artifactId>
36-
<version>1.1.10-SNAPSHOT</version>
36+
<version>1.1.10</version>
3737
</dependency>
3838

3939
<dependency>

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>io.harness</groupId>
88
<artifactId>ff-java-server-sdk</artifactId>
9-
<version>1.1.10-SNAPSHOT</version>
9+
<version>1.1.10</version>
1010
<packaging>jar</packaging>
1111
<name>Harness Feature Flag Java Server SDK</name>
1212
<description>Harness Feature Flag Java Server SDK</description>

src/main/java/io/harness/cf/client/api/InnerClient.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package io.harness.cf.client.api;
22

3+
import static com.google.common.util.concurrent.Service.State.*;
4+
35
import com.google.common.base.Strings;
46
import com.google.common.util.concurrent.Service;
57
import com.google.gson.JsonObject;
@@ -132,7 +134,17 @@ protected void onUnauthorized() {
132134
if (options.isAnalyticsEnabled()) {
133135
metricsProcessor.stop();
134136
}
135-
authService.startAsync();
137+
138+
if (authService.state() == TERMINATED || authService.state() == FAILED) {
139+
authService.close();
140+
authService = new AuthService(this.connector, options.getPollIntervalInSeconds(), this);
141+
}
142+
143+
if (authService.state() != RUNNING) {
144+
authService.startAsync();
145+
}
146+
147+
log.info("Finished re-auth, auth service state={}", authService.state());
136148
}
137149

138150
@Override

src/main/java/io/harness/cf/client/api/PollingProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public CompletableFuture<List<Segment>> retrieveSegments() {
7272
completableFuture.complete(segments);
7373
} catch (ConnectorException e) {
7474
log.error(
75-
"Exception was raised when fetching flags data with the message {}", e.getMessage());
75+
"Exception was raised when fetching flags data with the message {}", e.getMessage(), e);
7676
completableFuture.completeExceptionally(e);
7777
}
7878
return completableFuture;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,26 @@
11
package io.harness.cf.client.connector;
22

33
public class ConnectorException extends Exception {
4+
5+
private final int httpCode;
6+
private final String httpReason;
7+
48
public ConnectorException(String message) {
59
super(message);
10+
this.httpCode = 0;
11+
this.httpReason = "";
12+
}
13+
14+
public ConnectorException(String message, int httpCode, String httpMsg) {
15+
super(message);
16+
this.httpCode = httpCode;
17+
this.httpReason = httpMsg;
18+
}
19+
20+
@Override
21+
public String getMessage() {
22+
final String httpInfo =
23+
(httpCode == 0) ? "" : String.format(" - httpCode=%d httpReason=%s", httpCode, httpReason);
24+
return super.getMessage() + httpInfo;
625
}
726
}

src/main/java/io/harness/cf/client/connector/HarnessConnector.java

Lines changed: 65 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@
88
import io.harness.cf.client.dto.Claim;
99
import io.harness.cf.client.logger.LogUtil;
1010
import io.harness.cf.model.*;
11-
import java.lang.ref.WeakReference;
11+
import java.io.IOException;
1212
import java.nio.charset.StandardCharsets;
1313
import java.util.*;
1414
import lombok.NonNull;
1515
import lombok.extern.slf4j.Slf4j;
16+
import okhttp3.Interceptor;
1617
import okhttp3.Request;
1718
import okhttp3.Response;
18-
import okhttp3.internal.Util;
1919
import org.slf4j.MDC;
2020

2121
@Slf4j
@@ -25,8 +25,6 @@ public class HarnessConnector implements Connector, AutoCloseable {
2525
private final MetricsApi metricsApi;
2626
private final String apiKey;
2727
private final HarnessConfig options;
28-
private WeakReference<Response> responseReference;
29-
private WeakReference<Response> metricsResponseReference;
3028

3129
private String token;
3230
private String environment;
@@ -48,12 +46,12 @@ public HarnessConnector(@NonNull String apiKey) {
4846
public HarnessConnector(@NonNull final String apiKey, @NonNull final HarnessConfig options) {
4947
this.apiKey = apiKey;
5048
this.options = options;
51-
this.api = new ClientApi(makeApiClient());
52-
this.metricsApi = new MetricsApi(makeMetricsApiClient());
49+
this.api = new ClientApi(makeApiClient(2000));
50+
this.metricsApi = new MetricsApi(makeMetricsApiClient(2000));
5351
log.info("Connector initialized, with options " + options);
5452
}
5553

56-
protected ApiClient makeApiClient() {
54+
ApiClient makeApiClient(int retryBackOfDelay) {
5755
final ApiClient apiClient = new ApiClient();
5856
apiClient.setBasePath(options.getConfigUrl());
5957
apiClient.setConnectTimeout(options.getConnectionTimeout());
@@ -66,45 +64,28 @@ protected ApiClient makeApiClient() {
6664
apiClient
6765
.getHttpClient()
6866
.newBuilder()
69-
.addInterceptor(
70-
chain -> {
71-
final Request request =
72-
chain
73-
.request()
74-
.newBuilder()
75-
.addHeader("X-Request-ID", getRequestID())
76-
.build();
77-
log.info("interceptor: requesting url {}", request.url().url());
78-
Response response;
79-
if (responseReference != null) {
80-
response = responseReference.get();
81-
if (response != null) {
82-
if (response.isSuccessful()) {
83-
log.debug(
84-
"interceptor: closing the response url={}", response.request().url());
85-
} else {
86-
log.warn(
87-
"interceptor: closing the faulty response url={}",
88-
response.request().url());
89-
}
90-
Util.closeQuietly(response);
91-
}
92-
}
93-
response = chain.proceed(request);
94-
responseReference = new WeakReference<>(response);
95-
if (response.code() == 403 && onUnauthorized != null) {
96-
onUnauthorized.run();
97-
}
98-
99-
return response;
100-
})
101-
.addInterceptor(new RetryInterceptor(3, 2000))
67+
.addInterceptor(this::interceptor)
68+
.addInterceptor(new RetryInterceptor(3, retryBackOfDelay))
10269
.build());
10370
log.info("apiClient definition complete");
10471
return apiClient;
10572
}
10673

107-
protected ApiClient makeMetricsApiClient() {
74+
private Response interceptor(Interceptor.Chain chain) throws IOException {
75+
final Request request =
76+
chain.request().newBuilder().addHeader("X-Request-ID", getRequestID()).build();
77+
log.info("interceptor: requesting url {}", request.url().url());
78+
79+
Response response = chain.proceed(request);
80+
81+
if (response.code() == 403 && onUnauthorized != null) {
82+
onUnauthorized.run();
83+
}
84+
85+
return response;
86+
}
87+
88+
ApiClient makeMetricsApiClient(int retryBackoffDelay) {
10889
final int maxTimeout = 30 * 60 * 1000;
10990
final ApiClient apiClient = new ApiClient();
11091
apiClient.setBasePath(options.getEventUrl());
@@ -117,41 +98,21 @@ protected ApiClient makeMetricsApiClient() {
11798
apiClient
11899
.getHttpClient()
119100
.newBuilder()
120-
.addInterceptor(
121-
chain -> {
122-
final Request request =
123-
chain
124-
.request()
125-
.newBuilder()
126-
.addHeader("X-Request-ID", getRequestID())
127-
.build();
128-
log.info("metrics interceptor: requesting url {}", request.url().url());
129-
Response response;
130-
if (metricsResponseReference != null) {
131-
response = metricsResponseReference.get();
132-
if (response != null) {
133-
if (response.isSuccessful()) {
134-
log.debug(
135-
"metrics interceptor: closing the response url={}",
136-
response.request().url());
137-
} else {
138-
log.warn(
139-
"metrics interceptor: closing the faulty response url={}",
140-
response.request().url());
141-
}
142-
Util.closeQuietly(response);
143-
}
144-
}
145-
response = chain.proceed(request);
146-
metricsResponseReference = new WeakReference<>(response);
147-
return response;
148-
})
149-
.addInterceptor(new RetryInterceptor(3, 2000))
101+
.addInterceptor(this::metricsInterceptor)
102+
.addInterceptor(new RetryInterceptor(3, retryBackoffDelay))
150103
.build());
151104
log.info("metricsApiClient definition complete");
152105
return apiClient;
153106
}
154107

108+
private Response metricsInterceptor(Interceptor.Chain chain) throws IOException {
109+
final Request request =
110+
chain.request().newBuilder().addHeader("X-Request-ID", getRequestID()).build();
111+
log.info("metrics interceptor: requesting url {}", request.url().url());
112+
113+
return chain.proceed(request);
114+
}
115+
155116
protected String getRequestID() {
156117
String requestId = MDC.get(REQUEST_ID_KEY);
157118
if (requestId == null) {
@@ -176,10 +137,11 @@ public String authenticate() throws ConnectorException {
176137
if (apiException.getCode() == 401 || apiException.getCode() == 403) {
177138
String errorMsg = String.format("Invalid apiKey %s. SDK will serve default values", apiKey);
178139
log.error(errorMsg);
179-
throw new ConnectorException(errorMsg);
140+
throw new ConnectorException(errorMsg, apiException.getCode(), apiException.getMessage());
180141
}
181142
log.error("Failed to get auth token", apiException);
182-
throw new ConnectorException(apiException.getMessage());
143+
throw new ConnectorException(
144+
apiException.getMessage(), apiException.getCode(), apiException.getMessage());
183145
} catch (Throwable ex) {
184146
log.error("Unexpected exception", ex);
185147
throw ex;
@@ -224,15 +186,17 @@ public List<FeatureConfig> getFlags() throws ConnectorException {
224186
featureConfig.size(),
225187
this.environment,
226188
this.cluster);
227-
log.info("Got the following features: " + featureConfig);
189+
if (log.isTraceEnabled()) {
190+
log.trace("Got the following features: " + featureConfig);
191+
}
228192
return featureConfig;
229193
} catch (ApiException e) {
230194
log.error(
231195
"Exception was raised while fetching the flags on env {} and cluster {}",
232196
this.environment,
233197
this.cluster,
234198
e);
235-
throw new ConnectorException(e.getMessage());
199+
throw new ConnectorException(e.getMessage(), e.getCode(), e.getMessage());
236200
} finally {
237201
MDC.remove(REQUEST_ID_KEY);
238202
}
@@ -260,7 +224,7 @@ public FeatureConfig getFlag(@NonNull final String identifier) throws ConnectorE
260224
this.environment,
261225
this.cluster,
262226
e);
263-
throw new ConnectorException(e.getMessage());
227+
throw new ConnectorException(e.getMessage(), e.getCode(), e.getMessage());
264228
} finally {
265229
MDC.remove(REQUEST_ID_KEY);
266230
}
@@ -283,11 +247,13 @@ public List<Segment> getSegments() throws ConnectorException {
283247
return allSegments;
284248
} catch (ApiException e) {
285249
log.error(
286-
"Exception was raised while fetching the target groups on env {} and cluster {}",
250+
"Exception was raised while fetching the target groups on env {} and cluster {} : httpCode={} message={}",
287251
this.environment,
288252
this.cluster,
253+
e.getCode(),
254+
e.getMessage(),
289255
e);
290-
throw new ConnectorException(e.getMessage());
256+
throw new ConnectorException(e.getMessage(), e.getCode(), e.getMessage());
291257
} finally {
292258
MDC.remove(REQUEST_ID_KEY);
293259
}
@@ -317,7 +283,7 @@ public Segment getSegment(@NonNull final String identifier) throws ConnectorExce
317283
this.environment,
318284
this.cluster,
319285
e);
320-
throw new ConnectorException(e.getMessage());
286+
throw new ConnectorException(e.getMessage(), e.getCode(), e.getMessage());
321287
} finally {
322288
MDC.remove(REQUEST_ID_KEY);
323289
}
@@ -340,7 +306,7 @@ public void postMetrics(@NonNull final Metrics metrics) throws ConnectorExceptio
340306
this.environment,
341307
this.cluster,
342308
e);
343-
throw new ConnectorException(e.getMessage());
309+
throw new ConnectorException(e.getMessage(), e.getCode(), e.getMessage());
344310
} finally {
345311
MDC.remove(REQUEST_ID_KEY);
346312
}
@@ -365,28 +331,29 @@ public Service stream(@NonNull final Updater updater) {
365331

366332
@Override
367333
public void close() {
368-
log.info("closing connector");
369-
if (responseReference != null) {
370-
final Response response = responseReference.get();
371-
if (response != null) {
372-
Util.closeQuietly(response);
373-
}
374-
responseReference = null;
375-
}
376-
if (metricsResponseReference != null) {
377-
final Response response = metricsResponseReference.get();
378-
if (response != null) {
379-
Util.closeQuietly(response);
380-
}
381-
metricsResponseReference = null;
382-
}
334+
log.debug("closing connector");
383335
api.getApiClient().getHttpClient().connectionPool().evictAll();
384-
log.info("All apiClient connections evicted");
336+
log.debug("All apiClient connections evicted");
385337
metricsApi.getApiClient().getHttpClient().connectionPool().evictAll();
386-
log.info("All metricsApiClient connections evicted");
338+
log.debug("All metricsApiClient connections evicted");
387339
if (eventSource != null) {
388340
eventSource.close();
389341
}
390-
log.info("connector closed!");
342+
log.debug("connector closed!");
343+
}
344+
345+
/* package private - should not be used outside of tests */
346+
347+
HarnessConnector(
348+
@NonNull final String apiKey, @NonNull final HarnessConfig options, int retryBackOffDelay) {
349+
this.apiKey = apiKey;
350+
this.options = options;
351+
this.api = new ClientApi(makeApiClient(retryBackOffDelay));
352+
this.metricsApi = new MetricsApi(makeMetricsApiClient(retryBackOffDelay));
353+
log.info(
354+
"Connector initialized, with options "
355+
+ options
356+
+ " and retry backoff delay "
357+
+ retryBackOffDelay);
391358
}
392359
}

0 commit comments

Comments
 (0)