Skip to content

Commit

Permalink
feat: Added the possibility to add a custom startupExceptionHandler (#…
Browse files Browse the repository at this point in the history
Christopher Kolstad authored Nov 27, 2023
1 parent db65acb commit beff1b8
Showing 4 changed files with 141 additions and 93 deletions.
18 changes: 14 additions & 4 deletions src/main/java/io/getunleash/repository/FeatureRepository.java
Original file line number Diff line number Diff line change
@@ -95,9 +95,15 @@ private void initCollections(UnleashScheduledExecutor executor) {
}

if (unleashConfig.isSynchronousFetchOnInitialisation()) {
updateFeatures(e -> {
throw e;
}).run();
if (this.unleashConfig.getStartupExceptionHandler() != null) {
updateFeatures(this.unleashConfig.getStartupExceptionHandler()).run();
} else {
updateFeatures(
e -> {
throw e;
})
.run();
}
}

if (!unleashConfig.isDisablePolling()) {
@@ -128,7 +134,11 @@ private Runnable updateFeatures(final Consumer<UnleashException> handler) {
featureBackupHandler.write(featureCollection);
} else if (response.getStatus() == ClientFeaturesResponse.Status.UNAVAILABLE) {
if (!ready && unleashConfig.isSynchronousFetchOnInitialisation()) {
throw new UnleashException(String.format("Could not initialize Unleash, got response code %d", response.getHttpStatusCode()), null);
throw new UnleashException(
String.format(
"Could not initialize Unleash, got response code %d",
response.getHttpStatusCode()),
null);
}
if (ready) {
throttler.handleHttpErrorCodes(response.getHttpStatusCode());
43 changes: 32 additions & 11 deletions src/main/java/io/getunleash/util/UnleashConfig.java
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
import io.getunleash.CustomHttpHeadersProvider;
import io.getunleash.DefaultCustomHttpHeadersProviderImpl;
import io.getunleash.UnleashContextProvider;
import io.getunleash.UnleashException;
import io.getunleash.event.NoOpSubscriber;
import io.getunleash.event.UnleashSubscriber;
import io.getunleash.lang.Nullable;
@@ -14,19 +15,15 @@
import io.getunleash.strategy.Strategy;
import java.io.File;
import java.math.BigInteger;
import java.net.Authenticator;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.URI;
import java.net.UnknownHostException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;

public class UnleashConfig {

@@ -71,6 +68,7 @@ public class UnleashConfig {
@Nullable private final Strategy fallbackStrategy;
@Nullable private final ToggleBootstrapProvider toggleBootstrapProvider;
@Nullable private final Proxy proxy;
@Nullable private final Consumer<UnleashException> startupExceptionHandler;

private UnleashConfig(
@Nullable URI unleashAPI,
@@ -101,7 +99,8 @@ private UnleashConfig(
@Nullable Strategy fallbackStrategy,
@Nullable ToggleBootstrapProvider unleashBootstrapProvider,
@Nullable Proxy proxy,
@Nullable Authenticator proxyAuthenticator) {
@Nullable Authenticator proxyAuthenticator,
@Nullable Consumer<UnleashException> startupExceptionHandler) {

if (appName == null) {
throw new IllegalStateException("You are required to specify the unleash appName");
@@ -165,6 +164,7 @@ private UnleashConfig(
this.metricSenderFactory = metricSenderFactory;
this.clientSpecificationVersion =
UnleashProperties.getProperty("client.specification.version");
this.startupExceptionHandler = startupExceptionHandler;
}

public static Builder builder() {
@@ -334,6 +334,11 @@ public UnleashFeatureFetcherFactory getUnleashFeatureFetcherFactory() {
return this.unleashFeatureFetcherFactory;
}

@Nullable
public Consumer<UnleashException> getStartupExceptionHandler() {
return startupExceptionHandler;
}

static class SystemProxyAuthenticator extends Authenticator {
@Override
protected @Nullable PasswordAuthentication getPasswordAuthentication() {
@@ -427,6 +432,8 @@ public static class Builder {
private @Nullable Proxy proxy;
private @Nullable Authenticator proxyAuthenticator;

private @Nullable Consumer<UnleashException> startupExceptionHandler;

private static String getHostname() {
String hostName = System.getProperty("hostname");
if (hostName == null || hostName.isEmpty()) {
@@ -657,6 +664,19 @@ public Builder apiKey(String apiKey) {
return this;
}

/**
* Used to handle exceptions when starting up synchronously. Allows user the option to
* choose how errors should be handled.
*
* @param startupExceptionHandler - a lambda taking the Exception and doing what it wants to
* the system.
*/
public Builder startupExceptionHandler(
@Nullable Consumer<UnleashException> startupExceptionHandler) {
this.startupExceptionHandler = startupExceptionHandler;
return this;
}

public UnleashConfig build() {
return new UnleashConfig(
unleashAPI,
@@ -688,7 +708,8 @@ public UnleashConfig build() {
fallbackStrategy,
toggleBootstrapProvider,
proxy,
proxyAuthenticator);
proxyAuthenticator,
startupExceptionHandler);
}

public String getDefaultSdkVersion() {
63 changes: 32 additions & 31 deletions src/test/java/io/getunleash/DefaultUnleashTest.java
Original file line number Diff line number Diff line change
@@ -14,13 +14,11 @@
import io.getunleash.event.EventDispatcher;
import io.getunleash.event.UnleashReady;
import io.getunleash.event.UnleashSubscriber;
import io.getunleash.integration.TestDefinition;
import io.getunleash.metric.UnleashMetricService;
import io.getunleash.repository.*;
import io.getunleash.strategy.DefaultStrategy;
import io.getunleash.strategy.Strategy;
import io.getunleash.util.UnleashConfig;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
@@ -40,10 +38,10 @@ class DefaultUnleashTest {

@RegisterExtension
static WireMockExtension serverMock =
WireMockExtension.newInstance()
.configureStaticDsl(true)
.options(wireMockConfig().dynamicPort().dynamicHttpsPort())
.build();
WireMockExtension.newInstance()
.configureStaticDsl(true)
.options(wireMockConfig().dynamicPort().dynamicHttpsPort())
.build();

@BeforeEach
public void setup() {
@@ -258,49 +256,51 @@ public void synchronous_fetch_on_initialisation_fails_on_initialization() {

@ParameterizedTest
@ValueSource(ints = {401, 403, 404, 500})
public void synchronous_fetch_on_initialisation_fails_on_non_200_response(int code) throws URISyntaxException {
public void synchronous_fetch_on_initialisation_fails_on_non_200_response(int code)
throws URISyntaxException {
mockUnleashAPI(code);
IsReadyTestSubscriber readySubscriber = new IsReadyTestSubscriber();
UnleashConfig config =
UnleashConfig.builder()
.unleashAPI(new URI("http://localhost:" + serverMock.getPort() + "/api/"))
.appName("wrong_upstream")
.apiKey("default:development:1234567890123456")
.instanceId("non-200")
.synchronousFetchOnInitialisation(true)
.subscriber(readySubscriber)
.build();
UnleashConfig.builder()
.unleashAPI(new URI("http://localhost:" + serverMock.getPort() + "/api/"))
.appName("wrong_upstream")
.apiKey("default:development:1234567890123456")
.instanceId("non-200")
.synchronousFetchOnInitialisation(true)
.subscriber(readySubscriber)
.build();

assertThatThrownBy(() -> new DefaultUnleash(config)).isInstanceOf(UnleashException.class);
assertThat(readySubscriber.ready).isFalse();
}

@Test
public void synchronous_fetch_on_initialisation_switches_to_ready_on_200() throws URISyntaxException {
public void synchronous_fetch_on_initialisation_switches_to_ready_on_200()
throws URISyntaxException {
mockUnleashAPI(200);
IsReadyTestSubscriber readySubscriber = new IsReadyTestSubscriber();
UnleashConfig config =
UnleashConfig.builder()
.unleashAPI(new URI("http://localhost:" + serverMock.getPort() + "/api/"))
.appName("wrong_upstream")
.apiKey("default:development:1234567890123456")
.instanceId("with-success-response")
.synchronousFetchOnInitialisation(true)
.subscriber(readySubscriber)
.build();
UnleashConfig.builder()
.unleashAPI(new URI("http://localhost:" + serverMock.getPort() + "/api/"))
.appName("wrong_upstream")
.apiKey("default:development:1234567890123456")
.instanceId("with-success-response")
.synchronousFetchOnInitialisation(true)
.subscriber(readySubscriber)
.build();
new DefaultUnleash(config);
assertThat(readySubscriber.ready).isTrue();
}

private void mockUnleashAPI(int featuresStatusCode) {
stubFor(
get(urlEqualTo("/api/client/features"))
.withHeader("Accept", equalTo("application/json"))
.willReturn(
aResponse()
.withStatus(featuresStatusCode)
.withHeader("Content-Type", "application/json")
.withBody("{\"features\": []}")));
get(urlEqualTo("/api/client/features"))
.withHeader("Accept", equalTo("application/json"))
.willReturn(
aResponse()
.withStatus(featuresStatusCode)
.withHeader("Content-Type", "application/json")
.withBody("{\"features\": []}")));
stubFor(post(urlEqualTo("/api/client/register")).willReturn(aResponse().withStatus(200)));
}

@@ -344,6 +344,7 @@ public void client_identifier_handles_api_key_being_null() {

private static class IsReadyTestSubscriber implements UnleashSubscriber {
public boolean ready = false;

public void onReady(UnleashReady unleashReady) {
this.ready = true;
}
Loading

0 comments on commit beff1b8

Please sign in to comment.