Skip to content

Commit 3112fac

Browse files
authored
FFM-7214 - When FF SDK is not supplied API Key it retries causing timeout for Client APIs (#144)
* FFM-7214 - When FF SDK is not supplied API Key it retries causing timeout for Client APIs What If authentication fails the SDK will no longer attempt to retry constantly. HarnessConnector will now throw MissingSdkKeyException if a blank/empty key is given. Why Platform team are reporting authentication getting stuck in a loop when a blank SDK key is given Testing New unit tests to ensure xVariations methods return default values after authentication has failed
1 parent a09b86f commit 3112fac

File tree

14 files changed

+216
-27
lines changed

14 files changed

+216
-27
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.11 version as an example:
72+
This section lists dependencies for Maven and Gradle and uses the 1.2.1 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.11</version>
81+
<version>1.2.1</version>
8282
</dependency>
8383
```
8484

8585
#### Gradle
8686

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

9191
### Code Sample

examples/src/main/java/io/harness/ff/code_cleanup_examples/README.md renamed to examples/code_cleanup_examples/README.md

File renamed without changes.

examples/src/main/java/io/harness/ff/code_cleanup_examples/SampleClass.java renamed to examples/code_cleanup_examples/SampleClass.java

File renamed without changes.

examples/src/main/java/io/harness/ff/code_cleanup_examples/config/rules.toml renamed to examples/code_cleanup_examples/config/rules.toml

File renamed without changes.

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.2.0</version>
9+
<version>1.2.1</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.2.0</version>
36+
<version>1.2.1</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.2.0</version>
9+
<version>1.2.1</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: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import static com.google.common.util.concurrent.Service.State.*;
44

5-
import com.google.common.base.Strings;
65
import com.google.common.util.concurrent.Service;
76
import com.google.gson.JsonObject;
87
import io.harness.cf.client.connector.Connector;
@@ -65,10 +64,6 @@ public InnerClient(@NonNull final String sdkKey) {
6564

6665
@Deprecated
6766
public InnerClient(@NonNull final String sdkKey, @NonNull final Config options) {
68-
if (Strings.isNullOrEmpty(sdkKey)) {
69-
log.error(MISSING_SDK_KEY);
70-
throw new IllegalArgumentException(MISSING_SDK_KEY);
71-
}
7267
HarnessConfig config =
7368
HarnessConfig.builder()
7469
.configUrl(options.getConfigUrl())
@@ -82,11 +77,6 @@ public InnerClient(@NonNull final String sdkKey, @NonNull final Config options)
8277
}
8378

8479
public InnerClient(@NonNull final String sdkKey, @NonNull final BaseConfig options) {
85-
if (Strings.isNullOrEmpty(sdkKey)) {
86-
log.error(MISSING_SDK_KEY);
87-
throw new IllegalArgumentException(MISSING_SDK_KEY);
88-
}
89-
9080
HarnessConfig config = HarnessConfig.builder().build();
9181
HarnessConnector harnessConnector = new HarnessConnector(sdkKey, config);
9282
setUp(harnessConnector, options);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package io.harness.cf.client.api;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
5+
@Slf4j
6+
public class MissingSdkKeyException extends RuntimeException {
7+
8+
private static final String MISSING_SDK_KEY = "SDK key cannot be empty!";
9+
10+
public MissingSdkKeyException() {
11+
super(MISSING_SDK_KEY);
12+
log.error(MISSING_SDK_KEY);
13+
}
14+
15+
public MissingSdkKeyException(Exception ex) {
16+
super(MISSING_SDK_KEY, ex);
17+
log.error(MISSING_SDK_KEY, ex);
18+
}
19+
}

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

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import io.harness.cf.ApiException;
66
import io.harness.cf.api.ClientApi;
77
import io.harness.cf.api.MetricsApi;
8+
import io.harness.cf.client.api.MissingSdkKeyException;
89
import io.harness.cf.client.dto.Claim;
910
import io.harness.cf.client.logger.LogUtil;
1011
import io.harness.cf.model.*;
@@ -48,6 +49,10 @@ public HarnessConnector(@NonNull String apiKey) {
4849
}
4950

5051
public HarnessConnector(@NonNull final String apiKey, @NonNull final HarnessConfig options) {
52+
if (isNullOrEmpty(apiKey)) {
53+
throw new MissingSdkKeyException();
54+
}
55+
5156
this.apiKey = apiKey;
5257
this.options = options;
5358
this.api = new ClientApi(makeApiClient(2000));
@@ -152,9 +157,12 @@ public String authenticate() throws ConnectorException {
152157
return token;
153158
} catch (ApiException apiException) {
154159
if (apiException.getCode() == 401 || apiException.getCode() == 403) {
155-
String errorMsg = String.format("Invalid apiKey %s. SDK will serve default values", apiKey);
160+
String errorMsg =
161+
String.format(
162+
"HTTP error code %d returned for authentication endpoint. Check API key. SDK will serve default values",
163+
apiException.getCode());
156164
log.error(errorMsg);
157-
throw new ConnectorException(errorMsg, apiException.getCode(), apiException.getMessage());
165+
throw new ConnectorException(errorMsg, false, apiException);
158166
}
159167
log.error("Failed to get auth token", apiException);
160168
throw new ConnectorException(
@@ -382,10 +390,19 @@ private void setupTls(ApiClient apiClient) {
382390
}
383391
}
384392

393+
private static boolean isNullOrEmpty(String string) {
394+
return string == null || string.trim().isEmpty();
395+
}
396+
385397
/* package private - should not be used outside of tests */
386398

387399
HarnessConnector(
388400
@NonNull final String apiKey, @NonNull final HarnessConfig options, int retryBackOffDelay) {
401+
402+
if (isNullOrEmpty(apiKey)) {
403+
throw new MissingSdkKeyException();
404+
}
405+
389406
this.apiKey = apiKey;
390407
this.options = options;
391408
this.api = new ClientApi(makeApiClient(retryBackOffDelay));

src/test/java/io/harness/cf/client/api/CfClientTest.java

Lines changed: 98 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static io.harness.cf.client.api.TestUtils.*;
55
import static io.harness.cf.client.api.dispatchers.CannedResponses.makeDummyJwtToken;
66
import static io.harness.cf.client.api.dispatchers.CannedResponses.makeMockJsonResponse;
7+
import static io.harness.cf.client.api.dispatchers.Endpoints.AUTH_ENDPOINT;
78
import static io.harness.cf.client.connector.HarnessConnectorUtils.*;
89
import static org.junit.jupiter.api.Assertions.*;
910

@@ -31,6 +32,7 @@
3132
import org.junit.jupiter.params.ParameterizedTest;
3233
import org.junit.jupiter.params.provider.Arguments;
3334
import org.junit.jupiter.params.provider.MethodSource;
35+
import org.junit.jupiter.params.provider.ValueSource;
3436

3537
class CfClientTest {
3638

@@ -116,8 +118,101 @@ void testConstructors() throws IOException {
116118
void shouldFailIfEmptySdkKeyGiven() {
117119
Exception thrown =
118120
assertThrows(
119-
IllegalArgumentException.class, () -> new CfClient(""), "Exception was not thrown");
120-
assertInstanceOf(IllegalArgumentException.class, thrown);
121+
MissingSdkKeyException.class, () -> new CfClient(""), "Exception was not thrown");
122+
assertInstanceOf(MissingSdkKeyException.class, thrown);
123+
}
124+
125+
@Test
126+
void shouldFailIfNullSdkKeyGiven() {
127+
Exception thrown =
128+
assertThrows(
129+
NullPointerException.class,
130+
() -> new CfClient((String) null),
131+
"Exception was not thrown");
132+
assertInstanceOf(NullPointerException.class, thrown);
133+
}
134+
135+
@ParameterizedTest
136+
@ValueSource(booleans = {true, false})
137+
void shouldAllowEvaluationsToContinueWhenAuthFails(boolean shouldWaitForInit)
138+
throws InterruptedException, IOException {
139+
140+
BaseConfig config =
141+
BaseConfig.builder()
142+
.pollIntervalInSeconds(1)
143+
.analyticsEnabled(false)
144+
.streamEnabled(true)
145+
.build();
146+
147+
Http403OnAuthDispatcher webserverDispatcher = new Http403OnAuthDispatcher(4); // auth error
148+
149+
try (MockWebServer mockSvr = new MockWebServer()) {
150+
mockSvr.setDispatcher(webserverDispatcher);
151+
mockSvr.start();
152+
153+
try (CfClient client =
154+
new CfClient(
155+
makeConnectorWithMinimalRetryBackOff(mockSvr.getHostName(), mockSvr.getPort()),
156+
config)) {
157+
158+
if (shouldWaitForInit) {
159+
try {
160+
client.waitForInitialization();
161+
} catch (FeatureFlagInitializeException ex) {
162+
// ignore
163+
}
164+
}
165+
166+
// Iterate a few times, check we're getting defaults and not blocking
167+
for (int i = 0; i < 1000; i++) {
168+
if (i % 100 == 0)
169+
System.out.printf(
170+
"Test that xVariation doesn't block and serves default when auth fails, iteration #%d%n",
171+
i);
172+
173+
boolean b1 = client.boolVariation("test", null, true);
174+
boolean b2 =
175+
client.boolVariation("test", Target.builder().identifier("test").build(), true);
176+
assertTrue(b1);
177+
assertTrue(b2);
178+
179+
String s1 = client.stringVariation("test", null, "def");
180+
String s2 =
181+
client.stringVariation("test", Target.builder().identifier("test").build(), "def");
182+
assertEquals("def", s1);
183+
assertEquals("def", s2);
184+
185+
double n1 = client.numberVariation("test", null, 321);
186+
double n2 =
187+
client.numberVariation("test", Target.builder().identifier("test").build(), 321);
188+
assertEquals(321, n1);
189+
assertEquals(321, n2);
190+
191+
JsonObject json = new JsonObject();
192+
json.addProperty("prop", "val");
193+
194+
JsonObject j1 = client.jsonVariation("test", null, json);
195+
JsonObject j2 =
196+
client.jsonVariation("test", Target.builder().identifier("test").build(), json);
197+
assertEquals("val", j1.get("prop").getAsString());
198+
assertEquals("val", j2.get("prop").getAsString());
199+
}
200+
201+
webserverDispatcher.waitForAllConnections(15);
202+
203+
assertEquals(
204+
3 + 1,
205+
webserverDispatcher.getUrlMap().get(AUTH_ENDPOINT),
206+
"not enough authentication attempts");
207+
208+
assertEquals(
209+
1, webserverDispatcher.getUrlMap().size(), "not enough authentication attempts");
210+
211+
assertTrue(
212+
webserverDispatcher.getUrlMap().containsKey(AUTH_ENDPOINT),
213+
"only auth endpoint should have been called");
214+
}
215+
}
121216
}
122217

123218
@Test
@@ -366,7 +461,7 @@ void shouldRetryThenReAuthenticateWithoutThrowingIllegalStateException() throws
366461

367462
client.waitForInitialization();
368463

369-
// First 3 attempts to connect to auth endpoint will return a 4xx, followed by a 200 success
464+
// First 3 attempts to connect to auth endpoint will return a 408, followed by a 200 success
370465
webserverDispatcher.waitForAllConnections(15);
371466

372467
final int expectedAuths = 3 + 3 + 1; // 3+3 failed retries (4xx), 1 success (200)

0 commit comments

Comments
 (0)